More work on audio system, started adding carrier icon

This commit is contained in:
Davide Passoni 2024-10-07 18:00:44 +02:00
parent fc00a4eac4
commit 01897019b0
26 changed files with 488 additions and 255 deletions

View File

@ -15,7 +15,9 @@
"description": "",
"abilities": "",
"canTargetPoint": false,
"canRearm": false
"canRearm": false,
"carrierFilename": "nimitz.svg",
"length": 300
},
"CVN_72": {
"name": "CVN_72",
@ -33,7 +35,9 @@
"description": "",
"abilities": "",
"canTargetPoint": false,
"canRearm": false
"canRearm": false,
"carrierFilename": "nimitz.svg",
"length": 300
},
"CVN_73": {
"name": "CVN_73",
@ -51,7 +55,9 @@
"description": "",
"abilities": "",
"canTargetPoint": false,
"canRearm": false
"canRearm": false,
"carrierFilename": "nimitz.svg",
"length": 300
},
"CVN_75": {
"name": "CVN_75",
@ -91,7 +97,9 @@
"description": "",
"abilities": "",
"canTargetPoint": false,
"canRearm": false
"canRearm": false,
"carrierFilename": "nimitz.svg",
"length": 300
},
"CV_1143_5": {
"name": "CV_1143_5",

Binary file not shown.

After

Width:  |  Height:  |  Size: 862 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 65 KiB

View File

@ -86,6 +86,7 @@ export class AudioManager {
});
} else {
this.#SRSClientUnitIDs = JSON.parse(new TextDecoder().decode(packetUint8Array.slice(1))).unitIDs;
document.dispatchEvent(new CustomEvent("SRSClientsUpdated"));
}
}
});

View File

@ -7,26 +7,39 @@ import { AudioUnitPipeline } from "./audiounitpipeline";
scramble calls and so on. Ideally, one may want to move this code to the backend*/
export class UnitSink extends AudioSink {
#unit: Unit;
#unitPipelines: {[key: string]: AudioUnitPipeline} = {};
#unitPipelines: { [key: string]: AudioUnitPipeline } = {};
constructor(sourceUnit: Unit) {
constructor(unit: Unit) {
super();
this.#unit = sourceUnit;
this.setName(`${sourceUnit.getUnitName()} - ${sourceUnit.getName()}`);
this.#unit = unit;
this.setName(`${unit.getUnitName()} - ${unit.getName()}`);
/* TODO as of now, any client connecting after the sink was created will not receive the sound. Add ability to add new pipelines */
getApp()
.getAudioManager()
.getSRSClientsUnitIDs()
.forEach((unitID) => {
if (unitID !== 0) {
this.#unitPipelines[unitID] = new AudioUnitPipeline(sourceUnit, unitID, this.getInputNode());
}
});
document.addEventListener("SRSClientsUpdated", () => {
this.#updatePipelines();
});
this.#updatePipelines();
}
getUnit() {
return this.#unit;
}
#updatePipelines() {
getApp()
.getAudioManager()
.getSRSClientsUnitIDs()
.forEach((unitID) => {
if (unitID !== 0 && !(unitID in this.#unitPipelines)) {
this.#unitPipelines[unitID] = new AudioUnitPipeline(this.#unit, unitID, this.getInputNode());
}
});
Object.keys(this.#unitPipelines).forEach((unitID) => {
if (!(unitID in getApp().getAudioManager().getSRSClientsUnitIDs())) {
delete this.#unitPipelines[unitID];
}
});
}
}

View File

@ -237,6 +237,7 @@ export const defaultMapMirrors = {};
export const defaultMapLayers = {};
/* Map constants */
export const NOT_INITIALIZED = "Not initialized";
export const IDLE = "Idle";
export const SPAWN_UNIT = "Spawn unit";
export const CONTEXT_ACTION = "Context action";

View File

@ -29,6 +29,7 @@ interface CustomEventMap {
audioSourcesUpdated: CustomEvent<any>;
audioSinksUpdated: CustomEvent<any>;
audioManagerStateChanged: CustomEvent<any>;
SRSClientsUpdated: CustomEvent<any>;
}
declare global {

View File

@ -19,6 +19,7 @@ import {
MAP_HIDDEN_TYPES_DEFAULTS,
COALITIONAREA_EDIT,
COALITIONAREA_DRAW_CIRCLE,
NOT_INITIALIZED,
} from "../constants/constants";
import { CoalitionPolygon } from "./coalitionarea/coalitionpolygon";
import { MapHiddenTypes, MapOptions } from "../types/types";
@ -46,7 +47,7 @@ export class Map extends L.Map {
#hiddenTypes: MapHiddenTypes = MAP_HIDDEN_TYPES_DEFAULTS;
/* State machine */
#state: string;
#state: string = NOT_INITIALIZED;
/* Map layers */
#theatre: string = "";
@ -140,7 +141,7 @@ export class Map extends L.Map {
this.#miniMapPolyline.addTo(this.#miniMapLayerGroup);
/* Init the state machine */
this.setState(IDLE);
setTimeout(() => this.setState(IDLE), 100);
/* Register event handles */
this.on("zoomstart", (e: any) => this.#onZoomStart(e));

View File

@ -14,11 +14,13 @@ export class Airbase extends CustomMarker {
#coalition: string = "";
#properties: string[] = [];
#parkings: string[] = [];
#img: HTMLImageElement;
constructor(options: AirbaseOptions) {
super(options.position, { riseOnHover: true });
this.#name = options.name;
this.#img = document.createElement("img");
}
createIcon() {
@ -32,10 +34,10 @@ export class Airbase extends CustomMarker {
var el = document.createElement("div");
el.classList.add("airbase-icon");
el.setAttribute("data-object", "airbase");
var img = document.createElement("img");
img.src = "/vite/images/markers/airbase.svg";
img.onload = () => SVGInjector(img);
el.appendChild(img);
this.#img.src = "/vite/images/markers/airbase.svg";
this.#img.onload = () => SVGInjector(this.#img);
el.appendChild(this.#img);
this.getElement()?.appendChild(el);
el.addEventListener("mouseover", (ev) => {
document.dispatchEvent(new CustomEvent("airbasemouseover", { detail: this }));
@ -86,4 +88,8 @@ export class Airbase extends CustomMarker {
getParkings() {
return this.#parkings;
}
getImg() {
return this.#img;
}
}

View File

@ -0,0 +1,58 @@
import { DivIcon, LatLng, Map } from "leaflet";
import { Airbase } from "./airbase";
export class Carrier extends Airbase {
#heading: number = 0;
createIcon() {
var icon = new DivIcon({
className: "leaflet-airbase-marker",
iconSize: [40, 40],
iconAnchor: [20, 20],
}); // Set the marker, className must be set to avoid white square
this.setIcon(icon);
var el = document.createElement("div");
el.classList.add("airbase-icon");
el.setAttribute("data-object", "airbase");
this.getImg().src = "/vite/images/carriers/nimitz.png";
this.getImg().style.width = `0px`; // Make the image immediately small to avoid giant carriers
el.appendChild(this.getImg());
this.getElement()?.appendChild(el);
el.addEventListener("mouseover", (ev) => {
document.dispatchEvent(new CustomEvent("airbasemouseover", { detail: this }));
});
el.addEventListener("mouseout", (ev) => {
document.dispatchEvent(new CustomEvent("airbasemouseout", { detail: this }));
});
el.dataset.coalition = this.getCoalition();
}
onAdd(map: Map): this {
super.onAdd(map);
//this._map.on("zoomstart", (e: any) => this.updateSize());
return this;
}
onRemove(map: Map): this {
super.onRemove(map);
//this._map.off("zoomstart", (e: any) => this.updateSize());
return this;
}
setHeading(heading: number) {
this.#heading = heading;
this.getImg().style.transform = `rotate(${heading - 3.14 / 2}rad)`;
}
updateSize() {
if (this._map) {
const y = this._map.getSize().y;
const x = this._map.getSize().x;
const maxMeters = this._map.containerPointToLatLng([0, y]).distanceTo(this._map.containerPointToLatLng([x, y]));
const meterPerPixel = maxMeters / x;
this.getImg().style.width = `${Math.round(333 / meterPerPixel)}px`;
}
}
}

View File

@ -12,11 +12,13 @@ import { navyUnitDatabase } from "../unit/databases/navyunitdatabase";
//import { Popup } from "../popups/popup";
import { AirbasesData, BullseyesData, CommandModeOptions, DateAndTime, MissionData } from "../interfaces";
import { Coalition } from "../types/types";
import { Carrier } from "./carrier";
import { NavyUnit } from "../unit/unit";
/** The MissionManager */
export class MissionManager {
#bullseyes: { [name: string]: Bullseye } = {};
#airbases: { [name: string]: Airbase } = {};
#airbases: { [name: string]: Airbase | Carrier } = {};
#theatre: string = "";
#dateAndTime: DateAndTime = {
date: { Year: 0, Month: 0, Day: 0 },
@ -82,18 +84,21 @@ export class MissionManager {
updateAirbases(data: AirbasesData) {
for (let idx in data.airbases) {
var airbase = data.airbases[idx];
if (this.#airbases[airbase.callsign] === undefined && airbase.callsign != "") {
this.#airbases[airbase.callsign] = new Airbase({
position: new LatLng(airbase.latitude, airbase.longitude),
name: airbase.callsign,
}).addTo(getApp().getMap());
this.#airbases[airbase.callsign].on("click", (e) => this.#onAirbaseClick(e));
this.#loadAirbaseChartData(airbase.callsign);
var airbaseCallsign = airbase.callsign !== ""? airbase.callsign: `carrier-${airbase.unitId}`
if (this.#airbases[airbaseCallsign] === undefined) {
if (airbase.callsign != "") {
this.#airbases[airbaseCallsign] = new Airbase({
position: new LatLng(airbase.latitude, airbase.longitude),
name: airbaseCallsign,
}).addTo(getApp().getMap());
this.#airbases[airbaseCallsign].on("click", (e) => this.#onAirbaseClick(e));
this.#loadAirbaseChartData(airbaseCallsign);
}
}
if (this.#airbases[airbase.callsign] != undefined && airbase.latitude && airbase.longitude && airbase.coalition) {
this.#airbases[airbase.callsign].setLatLng(new LatLng(airbase.latitude, airbase.longitude));
this.#airbases[airbase.callsign].setCoalition(airbase.coalition);
if (this.#airbases[airbaseCallsign] != undefined && airbase.latitude && airbase.longitude && airbase.coalition) {
this.#airbases[airbaseCallsign].setLatLng(new LatLng(airbase.latitude, airbase.longitude));
this.#airbases[airbaseCallsign].setCoalition(airbase.coalition);
}
}
}

View File

@ -81,7 +81,7 @@ export class OlympusApp {
getMissionManager() {
return this.#missionManager as MissionManager;
}
getAudioManager() {
return this.#audioManager as AudioManager;
}

View File

@ -1,3 +1,4 @@
import { RadioSink } from "../audio/radiosink";
import { DEFAULT_CONTEXT } from "../constants/constants";
import { ShortcutKeyboardOptions, ShortcutMouseOptions } from "../interfaces";
import { getApp } from "../olympusapp";
@ -119,7 +120,45 @@ export class ShortcutManager {
shiftKey: false,
});
["KeyW", "KeyA", "KeyS", "KeyD", "ArrowLeft", "ArrowRight", "ArrowUp", "ArrowDown"].forEach((code) => {
let PTTKeys = ["KeyZ", "KeyX", "KeyC", "KeyV", "KeyB", "KeyN", "KeyM", "KeyK", "KeyL"];
PTTKeys.forEach((key, idx) => {
this.addKeyboardShortcut(`PTT${idx}Active`, {
altKey: false,
callback: () => {
getApp()
.getAudioManager()
.getSinks()
.filter((sink) => {
return sink instanceof RadioSink;
})
[idx]?.setPtt(true);
},
code: key,
context: DEFAULT_CONTEXT,
ctrlKey: false,
shiftKey: false,
event: "keydown",
}).addKeyboardShortcut(`PTT${idx}Active`, {
altKey: false,
callback: () => {
getApp()
.getAudioManager()
.getSinks()
.filter((sink) => {
return sink instanceof RadioSink;
})
[idx]?.setPtt(false);
},
code: key,
context: DEFAULT_CONTEXT,
ctrlKey: false,
shiftKey: false,
event: "keyup",
});
});
let panKeys = ["KeyW", "KeyA", "KeyS", "KeyD", "ArrowLeft", "ArrowRight", "ArrowUp", "ArrowDown"];
panKeys.forEach((code) => {
this.addKeyboardShortcut(`pan${code}keydown`, {
altKey: false,
callback: (ev: KeyboardEvent) => {

View File

@ -4,11 +4,14 @@ import { getApp } from "../../olympusapp";
import { FaQuestionCircle } from "react-icons/fa";
import { AudioSourcePanel } from "./components/sourcepanel";
import { AudioSource } from "../../audio/audiosource";
import { FaVolumeHigh } from "react-icons/fa6";
import { FaVolumeHigh, FaX } from "react-icons/fa6";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { faClose } from "@fortawesome/free-solid-svg-icons";
export function AudioMenu(props: { open: boolean; onClose: () => void; children?: JSX.Element | JSX.Element[] }) {
const [sources, setSources] = useState([] as AudioSource[]);
const [audioManagerEnabled, setAudioManagerEnabled] = useState(false);
const [showTip, setShowTip] = useState(true);
useEffect(() => {
/* Force a rerender */
@ -29,40 +32,56 @@ export function AudioMenu(props: { open: boolean; onClose: () => void; children?
return (
<Menu title="Audio sources" open={props.open} showBackButton={false} onClose={props.onClose}>
<div className="p-4 text-sm text-gray-400">The audio source panel allows you to add and manage audio sources.</div>
<div className="mx-6 flex rounded-lg bg-olympus-400 p-4 text-sm">
{audioManagerEnabled && (
<>
<div>
<FaQuestionCircle className="my-4 ml-2 mr-6 text-gray-400" />
</div>
<div className="flex flex-col gap-1">
<div className="text-gray-100">Use the controls to apply effects and start/stop the playback of an audio source.</div>
<div className="text-gray-400">Sources can be connected to your radios, or attached to a unit to be played on loudspeakers.</div>
</div>
</>
<>
{showTip && (
<div className="mx-6 flex rounded-lg bg-olympus-400 p-4 text-sm">
{audioManagerEnabled ? (
<>
<div className="my-auto">
<FaQuestionCircle className="my-auto ml-2 mr-6 text-gray-400" />
</div>
<div className="flex flex-col gap-1">
<div className="text-gray-100">Use the controls to apply effects and start/stop the playback of an audio source.</div>
<div className="text-gray-400">Sources can be connected to your radios, or attached to a unit to be played on loudspeakers.</div>
</div>
<div>
<FontAwesomeIcon
onClick={() => setShowTip(false)}
icon={faClose}
className={`
ml-2 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
`}
/>
</div>
</>
) : (
<>
<div className="my-auto">
<FaQuestionCircle className="my-auto ml-2 mr-6 text-gray-400" />
</div>
<div className="flex flex-col gap-1">
<div className="text-gray-100">
To enable the audio menu, first start the audio backend with the{" "}
<span
className={`
mx-1 mt-[-7px] inline-block translate-y-2 rounded-full
border-[1px] border-white p-1
`}
>
<FaVolumeHigh />
</span>{" "}
button on the navigation header.
</div>
</div>
</>
)}
</div>
)}
{!audioManagerEnabled && (
<>
<div>
<FaQuestionCircle className="my-4 ml-2 mr-6 text-gray-400" />
</div>
<div className="flex flex-col gap-1">
<div className="text-gray-100">
To enable the audio menu, first start the audio backend with the{" "}
<span
className={`
mx-1 mt-[-7px] inline-block translate-y-2 rounded-full
border-[1px] border-white p-1
`}
>
<FaVolumeHigh />
</span>{" "}
button on the navigation header.
</div>
</div>
</>
)}
</div>
</>
<div
className={`
@ -71,8 +90,8 @@ export function AudioMenu(props: { open: boolean; onClose: () => void; children?
`}
>
<>
{sources.map((source) => {
return <AudioSourcePanel source={source} />;
{sources.map((source, idx) => {
return <AudioSourcePanel key={idx} source={source} />;
})}
</>
{audioManagerEnabled && (

View File

@ -30,9 +30,10 @@ export function Menu(props: {
<div
data-hide={hide}
className={`
pointer-events-auto h-[calc(100vh-58px-2rem)] overflow-y-auto
overflow-x-hidden no-scrollbar backdrop-blur-lg backdrop-grayscale
transition-transform
pointer-events-auto
h-[calc(100vh-58px${props.canBeHidden ? "-2rem" : ""})]
overflow-y-auto overflow-x-hidden no-scrollbar backdrop-blur-lg
backdrop-grayscale transition-transform
dark:bg-olympus-800/90
data-[hide='true']:translate-y-[calc(100vh-58px)]
`}
@ -81,9 +82,7 @@ export function Menu(props: {
<FaChevronUp className="mx-auto my-auto text-gray-400" />
) : (
<FaChevronDown
className={`
mx-auto my-auto text-gray-400
`}
className={`mx-auto my-auto text-gray-400`}
/>
)}
</div>

View File

@ -7,7 +7,7 @@ import { faEarListen, faMicrophoneLines } from "@fortawesome/free-solid-svg-icon
import { RadioSink } from "../../../audio/radiosink";
import { getApp } from "../../../olympusapp";
export function RadioPanel(props: { radio: RadioSink }) {
export function RadioPanel(props: { radio: RadioSink; shortcutKey: string }) {
return (
<div
className={`
@ -17,14 +17,19 @@ export function RadioPanel(props: { radio: RadioSink }) {
>
<div className="flex content-center justify-between">
<span className="my-auto">{props.radio.getName()}</span>
<div className="cursor-pointer rounded-md bg-red-800 p-2" onClick={() => {getApp().getAudioManager().removeSink(props.radio);}}>
<div
className="cursor-pointer rounded-md bg-red-800 p-2"
onClick={() => {
getApp().getAudioManager().removeSink(props.radio);
}}
>
<FaTrash className={`text-gray-50`}></FaTrash>
</div>
</div>
<OlFrequencyInput
value={props.radio.getFrequency()}
onChange={(value) => {
props.radio.setFrequency(value)
props.radio.setFrequency(value);
}}
/>
<div className="flex flex-row gap-2">
@ -37,8 +42,17 @@ export function RadioPanel(props: { radio: RadioSink }) {
}}
></OlLabelToggle>
<kbd
className={`
my-auto ml-auto rounded-lg border border-gray-200 bg-gray-100 px-2
py-1.5 text-xs font-semibold text-gray-800
dark:border-gray-500 dark:bg-gray-600 dark:text-gray-100
`}
>
{props.shortcutKey}
</kbd>
<OlStateButton
className="ml-auto"
checked={props.radio.getPtt()}
icon={faMicrophoneLines}
onClick={() => {

View File

@ -54,7 +54,7 @@ export function AudioSourcePanel(props: { source: AudioSource }) {
checked={false}
icon={props.source.getPlaying() ? faPause : faPlay}
onClick={() => {
if (props.source instanceof FileSource) props.source.getPlaying() ? props.source.stop() : props.source.play();
if (props.source instanceof FileSource) props.source.getPlaying() ? props.source.pause() : props.source.play();
}}
tooltip="Play file"
></OlStateButton>
@ -106,9 +106,10 @@ export function AudioSourcePanel(props: { source: AudioSource }) {
<span className="text-sm">Connected to:</span>
<div className="flex flex-col gap-1">
{props.source.getConnectedTo().map((sink) => {
{props.source.getConnectedTo().map((sink, idx) => {
return (
<div
key={idx}
className={`
flex justify-start gap-2 rounded-full bg-olympus-400 px-4 py-1
text-sm
@ -123,9 +124,10 @@ export function AudioSourcePanel(props: { source: AudioSource }) {
</div>
{availabileSinks.length > 0 && (
<OlDropdown label="Connect to:">
{availabileSinks.map((sink) => {
{availabileSinks.map((sink, idx) => {
return (
<OlDropdownItem
key={idx}
onClick={() => {
props.source.connect(sink);
}}

View File

@ -33,6 +33,7 @@ export function ControlsPanel(props: {}) {
{controls.map((control) => {
return (
<div
key={control.text}
className={`
flex w-full justify-between gap-2 rounded-full py-1 pl-4 pr-1
backdrop-blur-lg
@ -48,15 +49,15 @@ export function ControlsPanel(props: {}) {
>
{control.actions.map((action, idx) => {
return (
<>
<div className={``}>
<div key={idx} className="flex gap-1">
<div>
{typeof action === "string" || typeof action === "number" ? action : <FontAwesomeIcon icon={action} className={`
my-auto ml-auto
`} />}
</div>
{idx < control.actions.length - 1 && typeof control.actions[idx + 1] === "string" && <div>+</div>}
{idx < control.actions.length - 1 && typeof control.actions[idx + 1] === "number" && <div>x</div>}
</>
</div>
);
})}
</div>

View File

@ -27,22 +27,27 @@ export function DrawingMenu(props: { open: boolean; onClose: () => void }) {
const [erasSelection, setErasSelection] = useState({});
const [rangesSelection, setRangesSelection] = useState({});
const [showPolygonTip, setShowPolygonTip] = useState(true);
const [showCircleTip, setShowCircleTip] = useState(true);
useEffect(() => {
/* If we are not in polygon drawing mode, force the draw polygon button off */
if (drawingPolygon && getApp().getMap().getState() !== COALITIONAREA_DRAW_POLYGON) setDrawingPolygon(false);
if (getApp()) {
/* If we are not in polygon drawing mode, force the draw polygon button off */
if (drawingPolygon && getApp().getMap().getState() !== COALITIONAREA_DRAW_POLYGON) setDrawingPolygon(false);
/* If we are not in circle drawing mode, force the draw circle button off */
if (drawingCircle && getApp().getMap().getState() !== COALITIONAREA_DRAW_CIRCLE) setDrawingCircle(false);
/* If we are not in circle drawing mode, force the draw circle button off */
if (drawingCircle && getApp().getMap().getState() !== COALITIONAREA_DRAW_CIRCLE) setDrawingCircle(false);
/* If we are not in any drawing mode, force the map in edit mode */
if (props.open && !drawingPolygon && !drawingCircle) getApp().getMap().setState(COALITIONAREA_EDIT);
/* If we are not in any drawing mode, force the map in edit mode */
if (props.open && !drawingPolygon && !drawingCircle) getApp().getMap().setState(COALITIONAREA_EDIT);
/* Align the state of the coalition toggle to the coalition of the area */
if (activeCoalitionArea && activeCoalitionArea?.getCoalition() !== areaCoalition) setAreaCoalition(activeCoalitionArea?.getCoalition());
/* Align the state of the coalition toggle to the coalition of the area */
if (activeCoalitionArea && activeCoalitionArea?.getCoalition() !== areaCoalition) setAreaCoalition(activeCoalitionArea?.getCoalition());
if (!props.open) {
if ([COALITIONAREA_EDIT, COALITIONAREA_DRAW_CIRCLE, COALITIONAREA_DRAW_POLYGON].includes(getApp()?.getMap()?.getState()))
getApp().getMap().setState(IDLE);
if (!props.open) {
if ([COALITIONAREA_EDIT, COALITIONAREA_DRAW_CIRCLE, COALITIONAREA_DRAW_POLYGON].includes(getApp().getMap().getState()))
getApp().getMap().setState(IDLE);
}
}
});
@ -79,8 +84,8 @@ export function DrawingMenu(props: { open: boolean; onClose: () => void }) {
The draw tool allows you to quickly draw areas on the map and use these areas to spawn units and activate triggers.
</div>
<div className="mx-6 flex rounded-lg bg-olympus-400 p-4 text-sm">
<div>
<FaQuestionCircle className="my-4 ml-2 mr-6 text-gray-400" />
<div className="my-auto">
<FaQuestionCircle className="my-auto ml-2 mr-6 text-gray-400" />
</div>
<div className="flex flex-col gap-1">
<div className="text-gray-100">Use the polygon or circle tool to draw areas on the map.</div>
@ -217,14 +222,14 @@ export function DrawingMenu(props: { open: boolean; onClose: () => void }) {
{getApp()
.getGroundUnitDatabase()
.getTypes()
.map((type) => {
.map((type, idx) => {
if (!(type in typesSelection)) {
typesSelection[type] = true;
setTypesSelection(JSON.parse(JSON.stringify(typesSelection)));
}
return (
<OlDropdownItem className={`flex gap-4`}>
<OlDropdownItem key={idx} className={`flex gap-4`}>
<OlCheckbox
checked={typesSelection[type]}
onChange={(ev) => {

View File

@ -5,10 +5,15 @@ import { RadioPanel } from "./components/radiopanel";
import { FaQuestionCircle } from "react-icons/fa";
import { RadioSink } from "../../audio/radiosink";
import { FaVolumeHigh } from "react-icons/fa6";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { faClose } from "@fortawesome/free-solid-svg-icons";
let shortcutKeys = ["Z", "X", "C", "V", "B", "N", "M", "K", "L"];
export function RadioMenu(props: { open: boolean; onClose: () => void; children?: JSX.Element | JSX.Element[] }) {
const [radios, setRadios] = useState([] as RadioSink[]);
const [audioManagerEnabled, setAudioManagerEnabled] = useState(false);
const [showTip, setShowTip] = useState(true);
useEffect(() => {
/* Force a rerender */
@ -30,40 +35,56 @@ export function RadioMenu(props: { open: boolean; onClose: () => void; children?
return (
<Menu title="Radio" open={props.open} showBackButton={false} onClose={props.onClose}>
<div className="p-4 text-sm text-gray-400">The radio menu allows you to talk on radio to the players online using SRS.</div>
<div className="mx-6 flex rounded-lg bg-olympus-400 p-4 text-sm">
{audioManagerEnabled && (
<>
<div>
<FaQuestionCircle className="my-4 ml-2 mr-6 text-gray-400" />
</div>
<div className="flex flex-col gap-1">
<div className="text-gray-100">Use the radio controls to tune to a frequency, then click on the PTT button to talk. </div>
<div className="text-gray-400">You can add up to 10 radios. Use the audio effects menu to play audio tracks or to add background noises.</div>
</div>
</>
<>
{showTip && (
<div className="mx-6 flex rounded-lg bg-olympus-400 p-4 text-sm">
{audioManagerEnabled ? (
<>
<div className="my-auto">
<FaQuestionCircle className="my-auto ml-2 mr-6 text-gray-400" />
</div>
<div className="flex flex-col gap-1">
<div className="text-gray-100">Use the radio controls to tune to a frequency, then click on the PTT button to talk. </div>
<div className="text-gray-400">You can add up to 10 radios. Use the audio effects menu to play audio tracks or to add background noises.</div>
</div>
<div>
<FontAwesomeIcon
onClick={() => setShowTip(false)}
icon={faClose}
className={`
ml-2 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
`}
/>
</div>
</>
) : (
<>
<div>
<FaQuestionCircle className="my-4 ml-2 mr-6 text-gray-400" />
</div>
<div className="flex flex-col gap-1">
<div className="text-gray-100">
To enable the radio menu, first start the audio backend with the{" "}
<span
className={`
mx-1 mt-[-7px] inline-block translate-y-2 rounded-full
border-[1px] border-white p-1
`}
>
<FaVolumeHigh />
</span>{" "}
button on the navigation header.
</div>
</div>
</>
)}
</div>
)}
{!audioManagerEnabled && (
<>
<div>
<FaQuestionCircle className="my-4 ml-2 mr-6 text-gray-400" />
</div>
<div className="flex flex-col gap-1">
<div className="text-gray-100">
To enable the radio menu, first start the audio backend with the{" "}
<span
className={`
mx-1 mt-[-7px] inline-block translate-y-2 rounded-full
border-[1px] border-white p-1
`}
>
<FaVolumeHigh />
</span>{" "}
button on the navigation header.
</div>
</div>
</>
)}
</div>
</>
<div
className={`
@ -71,8 +92,8 @@ export function RadioMenu(props: { open: boolean; onClose: () => void; children?
dark:text-white
`}
>
{radios.map((radio) => {
return <RadioPanel radio={radio}></RadioPanel>;
{radios.map((radio, idx) => {
return <RadioPanel shortcutKey={shortcutKeys[idx]} key={radio.getName()} radio={radio}></RadioPanel>;
})}
{audioManagerEnabled && radios.length < 10 && (
<button

View File

@ -23,8 +23,8 @@ export function SpawnMenu(props: { open: boolean; onClose: () => void; children?
const [filteredAircraft, filteredHelicopters, filteredAirDefense, filteredGroundUnits, filteredNavyUnits] = getUnitsByLabel(filterString);
useEffect(() => {
if (!props.open) {
if (getApp()?.getMap()?.getState() === SPAWN_UNIT) getApp().getMap().setState(IDLE);
if (!props.open && getApp()) {
if (getApp().getMap().getState() === SPAWN_UNIT) getApp().getMap().setState(IDLE);
if (blueprint !== null) setBlueprint(null);
}
});

View File

@ -38,7 +38,7 @@ import {
olButtonsVisibilityOlympus,
} from "../components/olicons";
import { Coalition } from "../../types/types";
import { ftToM, getUnitDatabaseByCategory, getUnitsByLabel, knotsToMs, mToFt, msToKnots } from "../../other/utils";
import { ftToM, getUnitsByLabel, knotsToMs, mToFt, msToKnots } from "../../other/utils";
import { FaCog, FaGasPump, FaSignal, FaTag } from "react-icons/fa";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { OlSearchBar } from "../components/olsearchbar";
@ -272,9 +272,9 @@ export function UnitControlMenu(props: { open: boolean; onClose: () => void }) {
human: ["Human", olButtonsVisibilityHuman],
olympus: ["Olympus controlled", olButtonsVisibilityOlympus],
dcs: ["From DCS mission", olButtonsVisibilityDcs],
}).map((entry) => {
}).map((entry, idx) => {
return (
<div className="flex justify-between">
<div className="flex justify-between" key={idx}>
<span className="font-light text-white">{entry[1][0] as string}</span>
<OlToggle
key={entry[0]}
@ -297,81 +297,83 @@ export function UnitControlMenu(props: { open: boolean; onClose: () => void }) {
Types and coalitions
</div>
<table>
<tr>
<td></td>
<td className="pb-4 text-center font-bold text-blue-500">BLUE</td>
<td className="pb-4 text-center font-bold text-gray-500">NEUTRAL</td>
<td className="pb-4 text-center font-bold text-red-500">RED</td>
</tr>
{selectionBlueprint === null &&
Object.entries({
aircraft: olButtonsVisibilityAircraft,
helicopter: olButtonsVisibilityHelicopter,
"groundunit-sam": olButtonsVisibilityGroundunitSam,
groundunit: olButtonsVisibilityGroundunit,
navyunit: olButtonsVisibilityNavyunit,
}).map((entry) => {
return (
<tr>
<td className="text-lg text-gray-200">
<FontAwesomeIcon icon={entry[1]} />
</td>
{["blue", "neutral", "red"].map((coalition) => {
return (
<td className="text-center">
<OlCheckbox
checked={selectionFilter[coalition][entry[0]]}
disabled={selectionBlueprint !== null}
onChange={() => {
selectionFilter[coalition][entry[0]] = !selectionFilter[coalition][entry[0]];
setSelectionFilter(JSON.parse(JSON.stringify(selectionFilter)));
}}
/>
</td>
);
})}
</tr>
);
})}
<tr>
<td className="text-gray-200"></td>
<td className="text-center">
<OlCheckbox
checked={Object.values(selectionFilter["blue"]).some((value) => value)}
onChange={() => {
const newValue = !Object.values(selectionFilter["blue"]).some((value) => value);
Object.keys(selectionFilter["blue"]).forEach((key) => {
selectionFilter["blue"][key] = newValue;
});
setSelectionFilter(JSON.parse(JSON.stringify(selectionFilter)));
}}
/>
</td>
<td className="text-center">
<OlCheckbox
checked={Object.values(selectionFilter["neutral"]).some((value) => value)}
onChange={() => {
const newValue = !Object.values(selectionFilter["neutral"]).some((value) => value);
Object.keys(selectionFilter["neutral"]).forEach((key) => {
selectionFilter["neutral"][key] = newValue;
});
setSelectionFilter(JSON.parse(JSON.stringify(selectionFilter)));
}}
/>
</td>
<td className="text-center">
<OlCheckbox
checked={Object.values(selectionFilter["red"]).some((value) => value)}
onChange={() => {
const newValue = !Object.values(selectionFilter["red"]).some((value) => value);
Object.keys(selectionFilter["red"]).forEach((key) => {
selectionFilter["red"][key] = newValue;
});
setSelectionFilter(JSON.parse(JSON.stringify(selectionFilter)));
}}
/>
</td>
</tr>
<tbody>
<tr>
<td></td>
<td className="pb-4 text-center font-bold text-blue-500">BLUE</td>
<td className="pb-4 text-center font-bold text-gray-500">NEUTRAL</td>
<td className="pb-4 text-center font-bold text-red-500">RED</td>
</tr>
{selectionBlueprint === null &&
Object.entries({
aircraft: olButtonsVisibilityAircraft,
helicopter: olButtonsVisibilityHelicopter,
"groundunit-sam": olButtonsVisibilityGroundunitSam,
groundunit: olButtonsVisibilityGroundunit,
navyunit: olButtonsVisibilityNavyunit,
}).map((entry, idx) => {
return (
<tr key={idx}>
<td className="text-lg text-gray-200">
<FontAwesomeIcon icon={entry[1]} />
</td>
{["blue", "neutral", "red"].map((coalition) => {
return (
<td className="text-center" key={coalition}>
<OlCheckbox
checked={selectionFilter[coalition][entry[0]]}
disabled={selectionBlueprint !== null}
onChange={() => {
selectionFilter[coalition][entry[0]] = !selectionFilter[coalition][entry[0]];
setSelectionFilter(JSON.parse(JSON.stringify(selectionFilter)));
}}
/>
</td>
);
})}
</tr>
);
})}
<tr>
<td className="text-gray-200"></td>
<td className="text-center">
<OlCheckbox
checked={Object.values(selectionFilter["blue"]).some((value) => value)}
onChange={() => {
const newValue = !Object.values(selectionFilter["blue"]).some((value) => value);
Object.keys(selectionFilter["blue"]).forEach((key) => {
selectionFilter["blue"][key] = newValue;
});
setSelectionFilter(JSON.parse(JSON.stringify(selectionFilter)));
}}
/>
</td>
<td className="text-center">
<OlCheckbox
checked={Object.values(selectionFilter["neutral"]).some((value) => value)}
onChange={() => {
const newValue = !Object.values(selectionFilter["neutral"]).some((value) => value);
Object.keys(selectionFilter["neutral"]).forEach((key) => {
selectionFilter["neutral"][key] = newValue;
});
setSelectionFilter(JSON.parse(JSON.stringify(selectionFilter)));
}}
/>
</td>
<td className="text-center">
<OlCheckbox
checked={Object.values(selectionFilter["red"]).some((value) => value)}
onChange={() => {
const newValue = !Object.values(selectionFilter["red"]).some((value) => value);
Object.keys(selectionFilter["red"]).forEach((key) => {
selectionFilter["red"][key] = newValue;
});
setSelectionFilter(JSON.parse(JSON.stringify(selectionFilter)));
}}
/>
</td>
</tr>
</tbody>
</table>
<div>
<div ref={searchBarRef}>
@ -466,9 +468,10 @@ export function UnitControlMenu(props: { open: boolean; onClose: () => void }) {
{
<>
{["blue", "red", "neutral"].map((coalition) => {
return Object.keys(unitOccurences[coalition]).map((name) => {
return Object.keys(unitOccurences[coalition]).map((name, idx) => {
return (
<div
key={idx}
data-coalition={coalition}
className={`
flex content-center justify-between border-l-4
@ -648,6 +651,7 @@ export function UnitControlMenu(props: { open: boolean; onClose: () => void }) {
{[olButtonsRoeHold, olButtonsRoeReturn, olButtonsRoeDesignated, olButtonsRoeFree].map((icon, idx) => {
return (
<OlButtonGroupItem
key={idx}
onClick={() => {
selectedUnits.forEach((unit) => {
unit.setROE(ROEs[idx]);
@ -685,6 +689,7 @@ export function UnitControlMenu(props: { open: boolean; onClose: () => void }) {
{[olButtonsThreatNone, olButtonsThreatPassive, olButtonsThreatManoeuvre, olButtonsThreatEvade].map((icon, idx) => {
return (
<OlButtonGroupItem
key={idx}
onClick={() => {
selectedUnits.forEach((unit) => {
unit.setReactionToThreat(reactionsToThreat[idx]);
@ -716,6 +721,7 @@ export function UnitControlMenu(props: { open: boolean; onClose: () => void }) {
{[olButtonsEmissionsSilent, olButtonsEmissionsDefend, olButtonsEmissionsAttack, olButtonsEmissionsFree].map((icon, idx) => {
return (
<OlButtonGroupItem
key={idx}
onClick={() => {
selectedUnits.forEach((unit) => {
unit.setEmissionsCountermeasures(emissionsCountermeasures[idx]);
@ -847,6 +853,7 @@ export function UnitControlMenu(props: { open: boolean; onClose: () => void }) {
{[olButtonsScatter1, olButtonsScatter2, olButtonsScatter3].map((icon, idx) => {
return (
<OlButtonGroupItem
key={idx}
onClick={() => {
selectedUnits.forEach((unit) => {
unit.setShotsScatter(idx + 1);
@ -878,6 +885,7 @@ export function UnitControlMenu(props: { open: boolean; onClose: () => void }) {
{[olButtonsIntensity1, olButtonsIntensity2, olButtonsIntensity3].map((icon, idx) => {
return (
<OlButtonGroupItem
key={idx}
onClick={() => {
selectedUnits.forEach((unit) => {
unit.setShotsIntensity(idx + 1);
@ -992,6 +1000,7 @@ export function UnitControlMenu(props: { open: boolean; onClose: () => void }) {
{["Overlord", "Magic", "Wizard", "Focus", "Darkstar"].map((name, idx) => {
return (
<OlDropdownItem
key={idx}
onClick={() => {
if (activeAdvancedSettings) activeAdvancedSettings.radio.callsign = idx + 1;
setActiveAdvancedSettings(JSON.parse(JSON.stringify(activeAdvancedSettings)));
@ -1010,6 +1019,7 @@ export function UnitControlMenu(props: { open: boolean; onClose: () => void }) {
{["Texaco", "Arco", "Shell"].map((name, idx) => {
return (
<OlDropdownItem
key={idx}
onClick={() => {
if (activeAdvancedSettings) activeAdvancedSettings.radio.callsign = idx + 1;
setActiveAdvancedSettings(JSON.parse(JSON.stringify(activeAdvancedSettings)));
@ -1071,6 +1081,7 @@ export function UnitControlMenu(props: { open: boolean; onClose: () => void }) {
my-auto w-20
`}>
<OlDropdownItem
key={"X"}
onClick={() => {
if (activeAdvancedSettings) activeAdvancedSettings.TACAN.XY = "X";
setActiveAdvancedSettings(JSON.parse(JSON.stringify(activeAdvancedSettings)));
@ -1079,6 +1090,7 @@ export function UnitControlMenu(props: { open: boolean; onClose: () => void }) {
X
</OlDropdownItem>
<OlDropdownItem
key={"Y"}
onClick={() => {
if (activeAdvancedSettings) activeAdvancedSettings.TACAN.XY = "Y";
setActiveAdvancedSettings(JSON.parse(JSON.stringify(activeAdvancedSettings)));
@ -1113,12 +1125,15 @@ export function UnitControlMenu(props: { open: boolean; onClose: () => void }) {
<div className="text-sm text-gray-200">Radio frequency</div>
<div className="flex content-center gap-2">
<OlFrequencyInput value={activeAdvancedSettings? activeAdvancedSettings.radio.frequency: 251000000} onChange={(value) => {
if (activeAdvancedSettings) {
activeAdvancedSettings.radio.frequency = value;
setActiveAdvancedSettings(JSON.parse(JSON.stringify(activeAdvancedSettings)));
}
}}/>
<OlFrequencyInput
value={activeAdvancedSettings ? activeAdvancedSettings.radio.frequency : 251000000}
onChange={(value) => {
if (activeAdvancedSettings) {
activeAdvancedSettings.radio.frequency = value;
setActiveAdvancedSettings(JSON.parse(JSON.stringify(activeAdvancedSettings)));
}
}}
/>
</div>
<div className="flex pt-8">

View File

@ -18,8 +18,7 @@ export function UnitMouseControlBar(props: {}) {
/* Initialize the "scroll" position of the element */
var scrollRef = useRef(null);
useEffect(() => {
if (scrollRef.current)
onScroll(scrollRef.current);
if (scrollRef.current) onScroll(scrollRef.current);
});
useEffect(() => {
@ -94,9 +93,10 @@ export function UnitMouseControlBar(props: {}) {
/>
)}
<div className="flex gap-2 overflow-x-auto no-scrollbar p-2" onScroll={(ev) => onScroll(ev.target)} ref={scrollRef}>
{Object.values(contextActionsSet.getContextActions()).map((contextAction) => {
{Object.values(contextActionsSet.getContextActions()).map((contextAction: ContextAction) => {
return (
<OlStateButton
key={contextAction.getId()}
checked={contextAction === activeContextAction}
icon={contextAction.getIcon()}
tooltip={contextAction.getLabel()}
@ -109,13 +109,13 @@ export function UnitMouseControlBar(props: {}) {
setActiveContextAction(contextAction);
getApp().getMap().setState(CONTEXT_ACTION, {
contextAction: contextAction,
defaultContextAction: contextActionsSet.getDefaultContextAction()
defaultContextAction: contextActionsSet.getDefaultContextAction(),
});
} else {
setActiveContextAction(null);
getApp().getMap().setState(CONTEXT_ACTION, {
contextAction: null,
defaultContextAction: contextActionsSet.getDefaultContextAction()
defaultContextAction: contextActionsSet.getDefaultContextAction(),
});
}
}

View File

@ -56,7 +56,7 @@ export function UnitSpawnMenu(props: { blueprint: UnitBlueprint; spawnAtLocation
},
});
} else {
if (getApp()?.getMap()?.getState() === SPAWN_UNIT) getApp().getMap().setState(IDLE);
if (getApp().getMap().getState() === SPAWN_UNIT) getApp().getMap().setState(IDLE);
}
}
});

View File

@ -77,8 +77,7 @@ import {
faVolumeHigh,
faXmarksLines,
} from "@fortawesome/free-solid-svg-icons";
import { FaXmarksLines } from "react-icons/fa6";
import { ContextAction } from "./contextaction";
import { Carrier } from "../mission/carrier";
var pathIcon = new Icon({
iconUrl: "/vite/images/markers/marker-icon.png",
@ -850,7 +849,7 @@ export abstract class Unit extends CustomMarker {
if (targetPosition) getApp().getUnitsManager().addDestination(targetPosition, false, 0, units);
}
);
contextActionSet.addContextAction(
this,
"speaker",
@ -864,20 +863,12 @@ export abstract class Unit extends CustomMarker {
{ executeImmediately: true }
);
contextActionSet.addDefaultContextAction(
this,
"default",
"Set destination",
"",
faRoute,
null,
(units: Unit[], targetUnit, targetPosition) => {
if (targetPosition) {
getApp().getUnitsManager().clearDestinations(units);
getApp().getUnitsManager().addDestination(targetPosition, false, 0, units);
}
contextActionSet.addDefaultContextAction(this, "default", "Set destination", "", faRoute, null, (units: Unit[], targetUnit, targetPosition) => {
if (targetPosition) {
getApp().getUnitsManager().clearDestinations(units);
getApp().getUnitsManager().addDestination(targetPosition, false, 0, units);
}
)
});
}
drawLines() {
@ -1335,7 +1326,8 @@ export abstract class Unit extends CustomMarker {
"Line abreast (LH)",
"Follow unit in line abreast left formation",
olButtonsContextLineAbreast,
null, () => this.applyFollowOptions("line-abreast-lh", units)
null,
() => this.applyFollowOptions("line-abreast-lh", units)
);
contextActionSet.addContextAction(
this,
@ -1343,9 +1335,12 @@ export abstract class Unit extends CustomMarker {
"Line abreast (RH)",
"Follow unit in line abreast right formation",
olButtonsContextLineAbreast,
null, () => this.applyFollowOptions("line-abreast-rh", units)
null,
() => this.applyFollowOptions("line-abreast-rh", units)
);
contextActionSet.addContextAction(this, "front", "Front", "Fly in front of unit", olButtonsContextFront, null, () =>
this.applyFollowOptions("front", units)
);
contextActionSet.addContextAction(this, "front", "Front", "Fly in front of unit", olButtonsContextFront, null, () => this.applyFollowOptions("front", units));
contextActionSet.addContextAction(this, "diamond", "Diamond", "Follow unit in diamond formation", olButtonsContextDiamond, null, () =>
this.applyFollowOptions("diamond", units)
);
@ -1443,7 +1438,7 @@ export abstract class Unit extends CustomMarker {
#onLongPress(e: any) {
console.log(`Long press on ${this.getUnitName()}`);
if (e.originalEvent.button === 2) {
document.dispatchEvent(new CustomEvent("showUnitContextMenu", { detail: e }));
}
@ -1866,7 +1861,7 @@ export abstract class AirUnit extends Unit {
"Refuel at tanker",
"Refuel units at the nearest AAR Tanker. If no tanker is available the unit will RTB",
olButtonsContextRefuel,
null,
null,
(units: Unit[]) => {
getApp().getUnitsManager().refuel(units);
},
@ -1878,7 +1873,7 @@ export abstract class AirUnit extends Unit {
"Center map",
"Center the map on the unit and follow it",
faMapLocation,
null,
null,
(units: Unit[]) => {
getApp().getMap().centerOnUnit(units[0]);
},
@ -2158,6 +2153,8 @@ export class GroundUnit extends Unit {
}
export class NavyUnit extends Unit {
#carrier: Carrier;
constructor(ID: number) {
super(ID);
}
@ -2251,4 +2248,31 @@ export class NavyUnit extends Unit {
getDefaultMarker() {
return "navyunit";
}
setData(dataExtractor: DataExtractor) {
super.setData(dataExtractor);
if (this.#carrier) {
this.#carrier.setLatLng(this.getPosition());
this.#carrier.setHeading(this.getHeading());
this.#carrier.updateSize();
}
}
onAdd(map: Map): this {
super.onAdd(map);
if (this.getBlueprint()?.type === "Aircraft Carrier")
this.#carrier = new Carrier({
position: this.getPosition(),
name: this.getUnitName(),
}).addTo(getApp().getMap());
return this;
}
onRemove(map: Map): this {
super.onRemove(map);
if (this.#carrier)
this.#carrier.removeFrom(getApp().getMap())
return this;
}
}

View File

@ -1,7 +1,7 @@
{
"backend": {
"address": "localhost",
"port": 3001
"address": "88.99.250.188",
"port": 3000
},
"authentication": {
"gameMasterPassword": "4b8823ed9e5c2392ab4a791913bb8ce41956ea32e308b760eefb97536746dd33",