From fa215142ad15e573125c5d31827c437452326e21 Mon Sep 17 00:00:00 2001 From: Davide Passoni Date: Wed, 13 Nov 2024 17:13:30 +0100 Subject: [PATCH] Fixed radios not working, added mouse coordinates panel, started readding tips --- frontend/react/src/audio/audiomanager.ts | 19 ++- frontend/react/src/audio/radiosink.ts | 16 +++ frontend/react/src/events.ts | 107 +++++++++------ frontend/react/src/map/map.ts | 37 +++--- frontend/react/src/mission/missionmanager.ts | 4 +- .../ui/panels/components/radiosinkpanel.tsx | 33 +++-- .../react/src/ui/panels/controlspanel.tsx | 116 ++++++++++++----- .../react/src/ui/panels/coordinatespanel.tsx | 106 +++++++++++++++ frontend/react/src/ui/panels/minimappanel.tsx | 19 +-- frontend/react/src/ui/panels/sidebar.tsx | 2 +- frontend/react/src/ui/ui.css | 4 + frontend/react/src/ui/ui.tsx | 25 ++-- frontend/react/src/unit/unit.ts | 122 ++++++++---------- frontend/server/src/app.ts | 4 + frontend/server/src/audio/srshandler.ts | 13 ++ 15 files changed, 419 insertions(+), 208 deletions(-) create mode 100644 frontend/react/src/ui/panels/coordinatespanel.tsx diff --git a/frontend/react/src/audio/audiomanager.ts b/frontend/react/src/audio/audiomanager.ts index 0450876f..589a3cc9 100644 --- a/frontend/react/src/audio/audiomanager.ts +++ b/frontend/react/src/audio/audiomanager.ts @@ -35,16 +35,13 @@ export class AudioManager { #socket: WebSocket | null = null; #guid: string = makeID(22); #SRSClientUnitIDs: number[] = []; + #syncInterval: number; constructor() { ConfigLoadedEvent.on((config: OlympusConfig) => { config.audio.WSPort ? this.setPort(config.audio.WSPort) : this.setEndpoint(config.audio.WSEndpoint); }); - setInterval(() => { - this.#syncRadioSettings(); - }, 1000); - let PTTKeys = ["KeyZ", "KeyX", "KeyC", "KeyV", "KeyB", "KeyN", "KeyM", "KeyK", "KeyL"]; PTTKeys.forEach((key, idx) => { getApp() @@ -59,6 +56,10 @@ export class AudioManager { } start() { + this.#syncInterval = window.setInterval(() => { + this.#syncRadioSettings(); + }, 1000); + this.#running = true; this.#audioContext = new AudioContext({ sampleRate: 16000 }); this.#playbackPipeline = new PlaybackPipeline(); @@ -72,7 +73,9 @@ export class AudioManager { else if (this.#port) this.#socket = new WebSocket(`ws://${wsAddress}:${this.#port}`); else console.error("The audio backend was enabled but no port/endpoint was provided in the configuration"); - this.#socket = new WebSocket(`wss://refugees.dcsolympus.com/audio`); // TODO: remove, used for testing! + if (!this.#socket) return; + + //this.#socket = new WebSocket(`wss://refugees.dcsolympus.com/audio`); // TODO: remove, used for testing! /* Log the opening of the connection */ this.#socket.addEventListener("open", (event) => { @@ -98,7 +101,8 @@ export class AudioManager { /* Extract the frequency value and play it on the speakers if we are listening to it*/ audioPacket.getFrequencies().forEach((frequencyInfo) => { - if (sink.getFrequency() === frequencyInfo.frequency && sink.getModulation() === frequencyInfo.modulation) { + if (sink.getFrequency() === frequencyInfo.frequency && sink.getModulation() === frequencyInfo.modulation && sink.getTuned()) { + sink.setReceiving(true); this.#playbackPipeline.playBuffer(audioPacket.getAudioData().buffer); } }); @@ -133,6 +137,9 @@ export class AudioManager { this.#sinks.forEach((sink) => sink.disconnect()); this.#sources = []; this.#sinks = []; + this.#socket?.close(); + + window.clearInterval(this.#syncInterval); AudioSourcesChangedEvent.dispatch(this.#sources); AudioSinksChangedEvent.dispatch(this.#sinks); diff --git a/frontend/react/src/audio/radiosink.ts b/frontend/react/src/audio/radiosink.ts index e9335ff4..32be9798 100644 --- a/frontend/react/src/audio/radiosink.ts +++ b/frontend/react/src/audio/radiosink.ts @@ -16,6 +16,8 @@ export class RadioSink extends AudioSink { #ptt = false; #tuned = false; #volume = 0.5; + #receiving = false; + #clearReceivingTimeout: number; constructor() { super(); @@ -99,6 +101,20 @@ export class RadioSink extends AudioSink { return this.#volume; } + setReceiving(receiving) { + // Only do it if actually changed + if (receiving !== this.#receiving) AudioSinksChangedEvent.dispatch(getApp().getAudioManager().getSinks()); + if (receiving) { + window.clearTimeout(this.#clearReceivingTimeout); + this.#clearReceivingTimeout = window.setTimeout(() => this.setReceiving(false), 500); + } + this.#receiving = receiving; + } + + getReceiving() { + return this.#receiving; + } + handleEncodedData(encodedAudioChunk: EncodedAudioChunk) { let arrayBuffer = new ArrayBuffer(encodedAudioChunk.byteLength); encodedAudioChunk.copyTo(arrayBuffer); diff --git a/frontend/react/src/events.ts b/frontend/react/src/events.ts index 55cf6f5d..891eba29 100644 --- a/frontend/react/src/events.ts +++ b/frontend/react/src/events.ts @@ -5,11 +5,13 @@ import { CommandModeOptions, OlympusConfig, ServerStatus, SpawnRequestTable } fr import { CoalitionCircle } from "./map/coalitionarea/coalitioncircle"; import { CoalitionPolygon } from "./map/coalitionarea/coalitionpolygon"; import { Airbase } from "./mission/airbase"; +import { Bullseye } from "./mission/bullseye"; import { Shortcut } from "./shortcut/shortcut"; import { MapHiddenTypes, MapOptions } from "./types/types"; import { ContextAction } from "./unit/contextaction"; import { ContextActionSet } from "./unit/contextactionset"; import { Unit } from "./unit/unit"; +import { LatLng } from "leaflet"; export class BaseOlympusEvent { static on(callback: () => void) { @@ -34,7 +36,7 @@ export class BaseUnitEvent { static dispatch(unit: Unit) { document.dispatchEvent(new CustomEvent(this.name, { detail: { unit } })); console.log(`Event ${this.name} dispatched`); - console.log(unit) + console.log(unit); } } @@ -62,9 +64,9 @@ export class ConfigLoadedEvent { } static dispatch(config: OlympusConfig) { - document.dispatchEvent(new CustomEvent(this.name, {detail: config})); + document.dispatchEvent(new CustomEvent(this.name, { detail: config })); console.log(`Event ${this.name} dispatched`); - console.log(config) + console.log(config); } } @@ -91,7 +93,7 @@ export class InfoPopupEvent { } static dispatch(messages: string[]) { - document.dispatchEvent(new CustomEvent(this.name, {detail: {messages}})); + document.dispatchEvent(new CustomEvent(this.name, { detail: { messages } })); console.log(`Event ${this.name} dispatched`); } } @@ -104,20 +106,20 @@ export class HideMenuEvent { } static dispatch(hidden: boolean) { - document.dispatchEvent(new CustomEvent(this.name, {detail: {hidden}})); + document.dispatchEvent(new CustomEvent(this.name, { detail: { hidden } })); console.log(`Event ${this.name} dispatched`); } } export class ShortcutsChangedEvent { - static on(callback: (shortcuts: {[key: string]: Shortcut}) => void) { + static on(callback: (shortcuts: { [key: string]: Shortcut }) => void) { document.addEventListener(this.name, (ev: CustomEventInit) => { callback(ev.detail.shortcuts); }); } - static dispatch(shortcuts: {[key: string]: Shortcut}) { - document.dispatchEvent(new CustomEvent(this.name, {detail: {shortcuts}})); + static dispatch(shortcuts: { [key: string]: Shortcut }) { + document.dispatchEvent(new CustomEvent(this.name, { detail: { shortcuts } })); console.log(`Event ${this.name} dispatched`); } } @@ -130,7 +132,7 @@ export class ShortcutChangedEvent { } static dispatch(shortcut: Shortcut) { - document.dispatchEvent(new CustomEvent(this.name, {detail: {shortcut}})); + document.dispatchEvent(new CustomEvent(this.name, { detail: { shortcut } })); console.log(`Event ${this.name} dispatched`); } } @@ -143,7 +145,7 @@ export class BindShortcutRequestEvent { } static dispatch(shortcut: Shortcut) { - document.dispatchEvent(new CustomEvent(this.name, {detail: {shortcut}})); + document.dispatchEvent(new CustomEvent(this.name, { detail: { shortcut } })); console.log(`Event ${this.name} dispatched`); } } @@ -157,7 +159,7 @@ export class HiddenTypesChangedEvent { } static dispatch(hiddenTypes: MapHiddenTypes) { - document.dispatchEvent(new CustomEvent(this.name, {detail: {hiddenTypes}})); + document.dispatchEvent(new CustomEvent(this.name, { detail: { hiddenTypes } })); console.log(`Event ${this.name} dispatched`); } } @@ -170,7 +172,7 @@ export class MapOptionsChangedEvent { } static dispatch(mapOptions: MapOptions) { - document.dispatchEvent(new CustomEvent(this.name, {detail: {mapOptions}})); + document.dispatchEvent(new CustomEvent(this.name, { detail: { mapOptions } })); console.log(`Event ${this.name} dispatched`); } } @@ -183,7 +185,7 @@ export class MapSourceChangedEvent { } static dispatch(source: string) { - document.dispatchEvent(new CustomEvent(this.name, {detail: {source}})); + document.dispatchEvent(new CustomEvent(this.name, { detail: { source } })); console.log(`Event ${this.name} dispatched`); } } @@ -235,7 +237,7 @@ export class ContextActionSetChangedEvent { } static dispatch(contextActionSet: ContextActionSet | null) { - document.dispatchEvent(new CustomEvent(this.name, {detail: {contextActionSet}})); + document.dispatchEvent(new CustomEvent(this.name, { detail: { contextActionSet } })); console.log(`Event ${this.name} dispatched`); } } @@ -248,7 +250,7 @@ export class ContextActionChangedEvent { } static dispatch(contextAction: ContextAction | null) { - document.dispatchEvent(new CustomEvent(this.name, {detail: {contextAction}})); + document.dispatchEvent(new CustomEvent(this.name, { detail: { contextAction } })); console.log(`Event ${this.name} dispatched`); } } @@ -258,11 +260,11 @@ export class UnitUpdatedEvent extends BaseUnitEvent { document.dispatchEvent(new CustomEvent(this.name, { detail: { unit } })); // Logging disabled since periodic } -}; -export class UnitSelectedEvent extends BaseUnitEvent {}; -export class UnitDeselectedEvent extends BaseUnitEvent {}; -export class UnitDeadEvent extends BaseUnitEvent {}; -export class SelectionClearedEvent extends BaseOlympusEvent {}; +} +export class UnitSelectedEvent extends BaseUnitEvent {} +export class UnitDeselectedEvent extends BaseUnitEvent {} +export class UnitDeadEvent extends BaseUnitEvent {} +export class SelectionClearedEvent extends BaseOlympusEvent {} export class SelectedUnitsChangedEvent { static on(callback: (selectedUnits: Unit[]) => void) { @@ -272,9 +274,9 @@ export class SelectedUnitsChangedEvent { } static dispatch(selectedUnits: Unit[]) { - document.dispatchEvent(new CustomEvent(this.name, {detail: selectedUnits})); + document.dispatchEvent(new CustomEvent(this.name, { detail: selectedUnits })); console.log(`Event ${this.name} dispatched`); - console.log(selectedUnits) + console.log(selectedUnits); } } @@ -286,7 +288,7 @@ export class UnitExplosionRequestEvent { } static dispatch(units: Unit[]) { - document.dispatchEvent(new CustomEvent(this.name, {detail: {units}})); + document.dispatchEvent(new CustomEvent(this.name, { detail: { units } })); console.log(`Event ${this.name} dispatched`); } } @@ -299,7 +301,7 @@ export class FormationCreationRequestEvent { } static dispatch(leader: Unit, wingmen: Unit[]) { - document.dispatchEvent(new CustomEvent(this.name, {detail: {leader, wingmen}})); + document.dispatchEvent(new CustomEvent(this.name, { detail: { leader, wingmen } })); console.log(`Event ${this.name} dispatched`); } } @@ -312,7 +314,7 @@ export class MapContextMenuRequestEvent { } static dispatch(latlng: L.LatLng) { - document.dispatchEvent(new CustomEvent(this.name, {detail: {latlng}})); + document.dispatchEvent(new CustomEvent(this.name, { detail: { latlng } })); console.log(`Event ${this.name} dispatched`); } } @@ -325,7 +327,7 @@ export class UnitContextMenuRequestEvent { } static dispatch(unit: Unit) { - document.dispatchEvent(new CustomEvent(this.name, {detail: {unit}})); + document.dispatchEvent(new CustomEvent(this.name, { detail: { unit } })); console.log(`Event ${this.name} dispatched`); } } @@ -338,33 +340,46 @@ export class StarredSpawnContextMenuRequestEvent { } static dispatch(latlng: L.LatLng) { - document.dispatchEvent(new CustomEvent(this.name, {detail: {latlng}})); + document.dispatchEvent(new CustomEvent(this.name, { detail: { latlng } })); console.log(`Event ${this.name} dispatched`); } } export class HotgroupsChangedEvent { - static on(callback: (hotgroups: {[key: number]: number}) => void) { + static on(callback: (hotgroups: { [key: number]: number }) => void) { document.addEventListener(this.name, (ev: CustomEventInit) => { callback(ev.detail.hotgroups); }); } - static dispatch(hotgroups: {[key: number]: number}) { - document.dispatchEvent(new CustomEvent(this.name, {detail: {hotgroups}})); + static dispatch(hotgroups: { [key: number]: number }) { + document.dispatchEvent(new CustomEvent(this.name, { detail: { hotgroups } })); console.log(`Event ${this.name} dispatched`); } } export class StarredSpawnsChangedEvent { - static on(callback: (starredSpawns: {[key: number]: SpawnRequestTable}) => void) { + static on(callback: (starredSpawns: { [key: number]: SpawnRequestTable }) => void) { document.addEventListener(this.name, (ev: CustomEventInit) => { callback(ev.detail.starredSpawns); }); } - static dispatch(starredSpawns: {[key: number]: SpawnRequestTable}) { - document.dispatchEvent(new CustomEvent(this.name, {detail: {starredSpawns}})); + static dispatch(starredSpawns: { [key: number]: SpawnRequestTable }) { + document.dispatchEvent(new CustomEvent(this.name, { detail: { starredSpawns } })); + console.log(`Event ${this.name} dispatched`); + } +} + +export class MouseMovedEvent { + static on(callback: (latlng: LatLng, elevation: number) => void) { + document.addEventListener(this.name, (ev: CustomEventInit) => { + callback(ev.detail.latlng, ev.detail.elevation); + }); + } + + static dispatch(latlng: LatLng, elevation?: number) { + document.dispatchEvent(new CustomEvent(this.name, { detail: { latlng, elevation } })); console.log(`Event ${this.name} dispatched`); } } @@ -378,7 +393,7 @@ export class CommandModeOptionsChangedEvent { } static dispatch(options: CommandModeOptions) { - document.dispatchEvent(new CustomEvent(this.name, {detail: options})); + document.dispatchEvent(new CustomEvent(this.name, { detail: options })); console.log(`Event ${this.name} dispatched`); } } @@ -392,9 +407,9 @@ export class AudioSourcesChangedEvent { } static dispatch(audioSources: AudioSource[]) { - document.dispatchEvent(new CustomEvent(this.name, {detail: {audioSources}})); + document.dispatchEvent(new CustomEvent(this.name, { detail: { audioSources } })); console.log(`Event ${this.name} dispatched`); - console.log(audioSources) + console.log(audioSources); } } @@ -406,9 +421,9 @@ export class AudioSinksChangedEvent { } static dispatch(audioSinks: AudioSink[]) { - document.dispatchEvent(new CustomEvent(this.name, {detail: {audioSinks}})); + document.dispatchEvent(new CustomEvent(this.name, { detail: { audioSinks } })); console.log(`Event ${this.name} dispatched`); - console.log(audioSinks) + console.log(audioSinks); } } @@ -433,7 +448,21 @@ export class AudioManagerStateChangedEvent { } static dispatch(state: boolean) { - document.dispatchEvent(new CustomEvent(this.name, {detail: {state}})); + document.dispatchEvent(new CustomEvent(this.name, { detail: { state } })); + console.log(`Event ${this.name} dispatched`); + } +} + +/************** Mission data events ***************/ +export class BullseyesDataChanged { + static on(callback: (bullseyes: { [name: string]: Bullseye }) => void) { + document.addEventListener(this.name, (ev: CustomEventInit) => { + callback(ev.detail.bullseyes); + }); + } + + static dispatch(bullseyes: { [name: string]: Bullseye } ) { + document.dispatchEvent(new CustomEvent(this.name, { detail: { bullseyes } })); console.log(`Event ${this.name} dispatched`); } } diff --git a/frontend/react/src/map/map.ts b/frontend/react/src/map/map.ts index 26ddad32..ce1abb35 100644 --- a/frontend/react/src/map/map.ts +++ b/frontend/react/src/map/map.ts @@ -51,6 +51,7 @@ import { MapContextMenuRequestEvent, MapOptionsChangedEvent, MapSourceChangedEvent, + MouseMovedEvent, SelectionClearedEvent, StarredSpawnContextMenuRequestEvent, StarredSpawnsChangedEvent, @@ -517,9 +518,7 @@ export class Map extends L.Map { } getCurrentControls() { - const touch = matchMedia("(hover: none)").matches; - return []; - // TODO, is this a good idea? I never look at the thing + //const touch = matchMedia("(hover: none)").matches; //if (getApp().getState() === IDLE) { // return [ // { @@ -544,7 +543,7 @@ export class Map extends L.Map { // text: "Move map location", // }, // ]; - //} else if (getApp().getState() === SPAWN_UNIT) { + //} else if (getApp().getState() === OlympusState.SPAWN_UNIT) { // return [ // { // actions: [touch ? faHandPointer : "LMB"], @@ -812,7 +811,7 @@ export class Map extends L.Map { } this.#miniMap = new ClickableMiniMap(this.#miniMapLayerGroup, { - position: "topright", + position: "bottomright", width: 192 * 1.5, height: 108 * 1.5, //@ts-ignore Needed because some of the inputs are wrong in the original module interface @@ -999,7 +998,7 @@ export class Map extends L.Map { return; } - if (this.#contextAction?.getTarget() === ContextActionTarget.POINT && e.originalEvent.button === 2) this.#isRotatingDestination = true; + if (this.#contextAction?.getTarget() === ContextActionTarget.POINT && e.originalEvent?.button === 2) this.#isRotatingDestination = true; this.scrollWheelZoom.disable(); this.#shortPressTimer = window.setTimeout(() => { @@ -1043,7 +1042,7 @@ export class Map extends L.Map { /* Do nothing */ } else if (getApp().getState() === OlympusState.SPAWN) { if (getApp().getSubState() === SpawnSubState.SPAWN_UNIT) { - if (e.originalEvent.button != 2 && this.#spawnRequestTable !== null) { + if (e.originalEvent?.button != 2 && this.#spawnRequestTable !== null) { this.#spawnRequestTable.unit.location = pressLocation; getApp() .getUnitsManager() @@ -1060,7 +1059,7 @@ export class Map extends L.Map { ); } } else if (getApp().getSubState() === SpawnSubState.SPAWN_EFFECT) { - if (e.originalEvent.button != 2 && this.#effectRequestTable !== null) { + if (e.originalEvent?.button != 2 && this.#effectRequestTable !== null) { if (this.#effectRequestTable.type === "explosion") { if (this.#effectRequestTable.explosionType === "High explosive") getApp().getServerManager().spawnExplosion(50, "normal", pressLocation); else if (this.#effectRequestTable.explosionType === "Napalm") getApp().getServerManager().spawnExplosion(50, "napalm", pressLocation); @@ -1099,10 +1098,10 @@ export class Map extends L.Map { } } } else if (getApp().getState() === OlympusState.UNIT_CONTROL) { - if (e.type === "touchstart" || e.originalEvent.buttons === 1) { + if (e.type === "touchstart" || e.originalEvent?.buttons === 1) { if (this.#contextAction !== null) this.executeContextAction(null, pressLocation, e.originalEvent); else getApp().setState(OlympusState.IDLE); - } else if (e.originalEvent.buttons === 2) { + } else if (e.originalEvent?.buttons === 2) { this.executeDefaultContextAction(null, pressLocation, e.originalEvent); } } else if (getApp().getState() === OlympusState.JTAC) { @@ -1172,7 +1171,7 @@ export class Map extends L.Map { if (e.type === "touchstart") document.dispatchEvent(new CustomEvent("forceboxselect", { detail: e })); else document.dispatchEvent(new CustomEvent("forceboxselect", { detail: e.originalEvent })); } else if (getApp().getState() === OlympusState.UNIT_CONTROL) { - if (e.originalEvent.button === 2) { + if (e.originalEvent?.button === 2) { if (!this.getContextAction()) { getApp().setState(OlympusState.UNIT_CONTROL, UnitControlSubState.MAP_CONTEXT_MENU); MapContextMenuRequestEvent.dispatch(pressLocation); @@ -1198,6 +1197,11 @@ export class Map extends L.Map { this.#lastMousePosition.y = e.originalEvent.y; this.#lastMouseCoordinates = e.latlng; + MouseMovedEvent.dispatch(e.latlng); + getGroundElevation(e.latlng, (elevation) => { + MouseMovedEvent.dispatch(e.latlng, elevation); + }) + if (this.#currentSpawnMarker) this.#currentSpawnMarker.setLatLng(e.latlng); if (this.#currentEffectMarker) this.#currentEffectMarker.setLatLng(e.latlng); } else { @@ -1269,17 +1273,6 @@ export class Map extends L.Map { #setSlaveDCSCameraAvailable(newSlaveDCSCameraAvailable: boolean) { this.#slaveDCSCameraAvailable = newSlaveDCSCameraAvailable; - let linkButton = document.getElementById("camera-link-control"); - if (linkButton) { - if (!newSlaveDCSCameraAvailable) { - //this.setSlaveDCSCamera(false); // Commented to experiment with usability - linkButton.classList.add("red"); - linkButton.title = "Camera link to DCS is not available"; - } else { - linkButton.classList.remove("red"); - linkButton.title = "Link/Unlink DCS camera with Olympus position"; - } - } } /* Check if the camera control plugin is available. Right now this will only change the color of the button, no changes in functionality */ diff --git a/frontend/react/src/mission/missionmanager.ts b/frontend/react/src/mission/missionmanager.ts index 977ccd3a..4d565abe 100644 --- a/frontend/react/src/mission/missionmanager.ts +++ b/frontend/react/src/mission/missionmanager.ts @@ -7,7 +7,7 @@ import { AirbasesData, BullseyesData, CommandModeOptions, DateAndTime, MissionDa import { Coalition } from "../types/types"; import { Carrier } from "./carrier"; import { NavyUnit } from "../unit/unit"; -import { CommandModeOptionsChangedEvent, InfoPopupEvent } from "../events"; +import { BullseyesDataChanged, CommandModeOptionsChangedEvent, InfoPopupEvent } from "../events"; /** The MissionManager */ export class MissionManager { @@ -56,6 +56,8 @@ export class MissionManager { this.#bullseyes[idx].setLatLng(new LatLng(bullseye.latitude, bullseye.longitude)); this.#bullseyes[idx].setCoalition(bullseye.coalition); } + + BullseyesDataChanged.dispatch(this.#bullseyes) } } diff --git a/frontend/react/src/ui/panels/components/radiosinkpanel.tsx b/frontend/react/src/ui/panels/components/radiosinkpanel.tsx index 7a332c9e..d9d8788b 100644 --- a/frontend/react/src/ui/panels/components/radiosinkpanel.tsx +++ b/frontend/react/src/ui/panels/components/radiosinkpanel.tsx @@ -12,13 +12,15 @@ export const RadioSinkPanel = forwardRef((props: { radio: RadioSink; shortcutKey useEffect(() => { if (props.onExpanded) props.onExpanded(); - }, [expanded]) + }, [expanded]); return (
@@ -37,19 +39,22 @@ export const RadioSinkPanel = forwardRef((props: { radio: RadioSink; shortcutKey data-expanded={expanded} />
- {props.shortcutKey && (<> - - {props.shortcutKey} - + {props.shortcutKey && ( + <> + + {props.shortcutKey} + )} - {props.radio.getName()} {!expanded && `: ${props.radio.getFrequency()/1e6} MHz ${props.radio.getModulation()? "FM": "AM"}`} {} + + {props.radio.getName()} {!expanded && `: ${props.radio.getFrequency() / 1e6} MHz ${props.radio.getModulation() ? "FM" : "AM"}`} {}{" "} +
{ - if (getApp() && controls === null) { - setControls(getApp().getMap().getCurrentControls()); - } - }); - - useEffect(() => { - AppStateChangedEvent.on(() => setControls(getApp().getMap().getCurrentControls())); + AppStateChangedEvent.on((state, subState) => { + setAppState(state); + setAppSubState(subState); + }); + MapOptionsChangedEvent.on((mapOptions) => setMapOptions({ ...mapOptions })); }, []); + useEffect(() => { + const touch = matchMedia("(hover: none)").matches; + let controls: { + actions: (string | number | IconDefinition)[]; + target: IconDefinition; + text: string; + }[] = []; + + if (appState === OlympusState.IDLE) { + controls = [ + { + actions: [touch ? faHandPointer : "LMB"], + target: faJetFighter, + text: "Select unit", + }, + { + actions: [touch ? faHandPointer : "LMB", 2], + target: faMap, + text: "Quick spawn menu", + }, + { + actions: touch ? [faHandPointer, "Drag"] : ["Shift", "LMB", "Drag"], + target: faMap, + text: "Box selection", + }, + { + actions: [touch ? faHandPointer : "LMB", "Drag"], + target: faMap, + text: "Move map location", + }, + ]; + } else if (appState === OlympusState.SPAWN) { + controls = [ + { + actions: [touch ? faHandPointer : "LMB", 2], + target: faMap, + text: appSubState === SpawnSubState.NO_SUBSTATE ? "Close spawn menu" : "Return to spawn menu", + }, + { + actions: touch ? [faHandPointer, "Drag"] : ["Shift", "LMB", "Drag"], + target: faMap, + text: "Box selection", + }, + { + actions: [touch ? faHandPointer : "LMB", "Drag"], + target: faMap, + text: "Move map location", + }, + ]; + if (appSubState === SpawnSubState.SPAWN_UNIT) { + controls.unshift({ + actions: [touch ? faHandPointer : "LMB"], + target: faMap, + text: "Spawn unit", + }); + } else if (appSubState === SpawnSubState.SPAWN_EFFECT) { + controls.unshift({ + actions: [touch ? faHandPointer : "LMB"], + target: faMap, + text: "Spawn effect", + }); + } + } + + setControls(controls); + }, [appState, appSubState]); + return (
{controls?.map((control) => { @@ -53,14 +122,9 @@ export function ControlsPanel(props: {}) { return (
- {typeof action === "string" || typeof action === "number" ? ( - action - ) : ( - - )} + {typeof action === "string" || typeof action === "number" ? action : }
{idx < control.actions.length - 1 && typeof control.actions[idx + 1] === "string" &&
+
} {idx < control.actions.length - 1 && typeof control.actions[idx + 1] === "number" &&
x
} @@ -68,18 +132,6 @@ export function ControlsPanel(props: {}) { ); })}
- {/*} -
on
- -
- -
- {*/}
); })} diff --git a/frontend/react/src/ui/panels/coordinatespanel.tsx b/frontend/react/src/ui/panels/coordinatespanel.tsx new file mode 100644 index 00000000..58b2c6d4 --- /dev/null +++ b/frontend/react/src/ui/panels/coordinatespanel.tsx @@ -0,0 +1,106 @@ +import React, { useEffect, useState } from "react"; +import { OlLocation } from "../components/ollocation"; +import { LatLng } from "leaflet"; +import { FaBullseye, FaChevronDown, FaChevronUp, FaJetFighter, FaMountain } from "react-icons/fa6"; +import { BullseyesDataChanged, MouseMovedEvent } from "../../events"; +import { bearing, mToFt } from "../../other/utils"; +import { Bullseye } from "../../mission/bullseye"; + +export function CoordinatesPanel(props: {}) { + const [open, setOpen] = useState(false); + const [latlng, setLatlng] = useState(new LatLng(0, 0)); + const [elevation, setElevation] = useState(0); + const [bullseyes, setBullseyes] = useState(null as null | { [name: string]: Bullseye }); + + useEffect(() => { + MouseMovedEvent.on((latlng, elevation) => { + setLatlng(latlng); + if (elevation) setElevation(elevation); + }); + + BullseyesDataChanged.on((bullseyes) => setBullseyes(bullseyes)); + }, []); + + return ( +
+ {" "} + {open && ( + <> + {bullseyes && ( +
+
+
+ + + + {(bullseyes[2].getLatLng().distanceTo(latlng) / 1852).toFixed(0)} /{" "} + {bearing(bullseyes[2].getLatLng().lat, bullseyes[2].getLatLng().lng, latlng.lat, latlng.lng).toFixed()}° +
+
+ + + + {(bullseyes[1].getLatLng().distanceTo(latlng) / 1852).toFixed(0)} /{" "} + {bearing(bullseyes[1].getLatLng().lat, bullseyes[1].getLatLng().lng, latlng.lat, latlng.lng).toFixed()}° +
+
+ {/*}
+ + + +
+ {(bullseyes[1].getLatLng().distanceTo(latlng) / 1852).toFixed(0)} /{" "} + {bearing(bullseyes[1].getLatLng().lat, bullseyes[1].getLatLng().lng, latlng.lat, latlng.lng).toFixed()}° +
+
+ {*/} +
+ )} + + )} +
+ + + + +
{mToFt(elevation).toFixed()}ft
+ {open ? ( + setOpen(!open)} /> + ) : ( + setOpen(!open)} /> + )} +
+
+ ); +} diff --git a/frontend/react/src/ui/panels/minimappanel.tsx b/frontend/react/src/ui/panels/minimappanel.tsx index b864e281..41640d68 100644 --- a/frontend/react/src/ui/panels/minimappanel.tsx +++ b/frontend/react/src/ui/panels/minimappanel.tsx @@ -56,10 +56,11 @@ export function MiniMapPanel(props: {}) { onClick={() => setShowMissionTime(!showMissionTime)} className={` absolute right-[10px] - ${mapOptions.showMinimap ? `top-[232px]` : `top-[70px]`} + ${mapOptions.showMinimap ? `bottom-[188px]` : `bottom-[20px]`} flex w-[288px] items-center justify-between - ${mapOptions.showMinimap ? `rounded-b-lg` : `rounded-lg`} - bg-gray-200 p-3 text-sm backdrop-blur-lg backdrop-grayscale + ${mapOptions.showMinimap ? `rounded-t-lg` : `rounded-lg`} + cursor-pointer bg-gray-200 p-3 text-sm backdrop-blur-lg + backdrop-grayscale dark:bg-olympus-800/90 dark:text-gray-200 `} > @@ -94,17 +95,9 @@ export function MiniMapPanel(props: {}) { )} {mapOptions.showMinimap ? ( - { - getApp().getMap().setOption("showMinimap", false); - }} - > + getApp().getMap().setOption("showMinimap", false)} /> ) : ( - { - getApp().getMap().setOption("showMinimap", true); - }} - > + getApp().getMap().setOption("showMinimap", true)} /> )}
); diff --git a/frontend/react/src/ui/panels/sidebar.tsx b/frontend/react/src/ui/panels/sidebar.tsx index bf4b53fd..60390e37 100644 --- a/frontend/react/src/ui/panels/sidebar.tsx +++ b/frontend/react/src/ui/panels/sidebar.tsx @@ -37,7 +37,7 @@ export function SideBar() { onClick={() => { getApp().setState(appState !== OlympusState.SPAWN ? OlympusState.SPAWN : OlympusState.IDLE); }} - checked={appState === OlympusState.SPAWN && appSubState === SpawnSubState.NO_SUBSTATE} + checked={appState === OlympusState.SPAWN} icon={faPlusSquare} tooltip="Hide/show unit spawn menu" > diff --git a/frontend/react/src/ui/ui.css b/frontend/react/src/ui/ui.css index 084c9598..62f01669 100644 --- a/frontend/react/src/ui/ui.css +++ b/frontend/react/src/ui/ui.css @@ -1,3 +1,7 @@ +* { + user-select: none; /* Standard syntax */ +} + /* width */ ::-webkit-scrollbar { width: 10px; diff --git a/frontend/react/src/ui/ui.tsx b/frontend/react/src/ui/ui.tsx index d59cc827..968e3f2d 100644 --- a/frontend/react/src/ui/ui.tsx +++ b/frontend/react/src/ui/ui.tsx @@ -29,6 +29,7 @@ import { GameMasterMenu } from "./panels/gamemastermenu"; import { InfoBar } from "./panels/infobar"; import { HotGroupBar } from "./panels/hotgroupsbar"; import { StarredSpawnContextMenu } from "./contextmenus/starredspawncontextmenu"; +import { CoordinatesPanel } from "./panels/coordinatespanel"; export type OlympusUIState = { mainMenuVisible: boolean; @@ -65,11 +66,9 @@ export function UI() {
{appState === OlympusState.LOGIN && ( <> -
+ `}>
)} @@ -78,13 +77,10 @@ export function UI() {
- getApp().setState(OlympusState.IDLE)} /> - getApp().setState(OlympusState.IDLE)} - /> - getApp().setState(OlympusState.IDLE)} /> + getApp().setState(OlympusState.IDLE)} /> + getApp().setState(OlympusState.IDLE)} /> + getApp().setState(OlympusState.IDLE)} /> getApp().setState(OlympusState.IDLE)} /> - getApp().setState(OlympusState.IDLE)} /> getApp().setState(OlympusState.IDLE)} /> getApp().setState(OlympusState.IDLE)} /> getApp().setState(OlympusState.IDLE)} /> - getApp().setState(OlympusState.IDLE)} @@ -110,12 +104,15 @@ export function UI() { + + - - + + +
); diff --git a/frontend/react/src/unit/unit.ts b/frontend/react/src/unit/unit.ts index 1bf5ba6c..a935dd71 100644 --- a/frontend/react/src/unit/unit.ts +++ b/frontend/react/src/unit/unit.ts @@ -38,8 +38,8 @@ import { OlympusState, JTACSubState, UnitControlSubState, - ContextActionType, ContextActions, + ContextActionTarget, } from "../constants/constants"; import { DataExtractor } from "../server/dataextractor"; import { Weapon } from "../weapon/weapon"; @@ -48,40 +48,17 @@ import { RangeCircle } from "../map/rangecircle"; import { Group } from "./group"; import { ContextActionSet } from "./contextactionset"; import * as turf from "@turf/turf"; -import { - olButtonsContextSimulateFireFight, - olButtonsContextFollow, - olButtonsContextLandAtPoint, - olButtonsContextAttack, - olButtonsContextRefuel, -} from "../ui/components/olicons"; -import { - faExplosion, - faHand, - faLocationCrosshairs, - faLocationDot, - faMapLocation, - faPeopleGroup, - faPlaneArrival, - faQuestionCircle, - faRoute, - faTrash, - faXmarksLines, -} from "@fortawesome/free-solid-svg-icons"; import { Carrier } from "../mission/carrier"; import { ContactsUpdatedEvent, - FormationCreationRequestEvent, HiddenTypesChangedEvent, MapOptionsChangedEvent, UnitContextMenuRequestEvent, UnitDeadEvent, UnitDeselectedEvent, - UnitExplosionRequestEvent, UnitSelectedEvent, UnitUpdatedEvent, } from "../events"; -import { ContextAction } from "./contextaction"; var pathIcon = new Icon({ iconUrl: "/vite/images/markers/marker-icon.png", @@ -328,7 +305,7 @@ export abstract class Unit extends CustomMarker { } constructor(ID: number) { - super(new LatLng(0, 0), { riseOnHover: true, keyboard: false, bubblingMouseEvents: false }); + super(new LatLng(0, 0), { riseOnHover: true, keyboard: false, bubblingMouseEvents: true }); this.ID = ID; @@ -370,7 +347,8 @@ export abstract class Unit extends CustomMarker { this.on("mouseup", (e) => this.#onMouseUp(e)); this.on("dblclick", (e) => this.#onDoubleClick(e)); this.on("mouseover", () => { - if (this.belongsToCommandedCoalition()) this.setHighlighted(true); + if (this.belongsToCommandedCoalition() && (getApp().getState() === OlympusState.IDLE || getApp().getState() === OlympusState.UNIT_CONTROL)) + this.setHighlighted(true); }); this.on("mouseout", () => this.setHighlighted(false)); getApp() @@ -1304,56 +1282,61 @@ export abstract class Unit extends CustomMarker { /***********************************************/ #onMouseUp(e: any) { - this.#isMouseDown = false; + if (getApp().getState() === OlympusState.IDLE || getApp().getState() === OlympusState.UNIT_CONTROL) { + this.#isMouseDown = false; - if (getApp().getMap().isSelecting()) return; + if (getApp().getMap().isSelecting()) return; - DomEvent.stop(e); - DomEvent.preventDefault(e); - e.originalEvent.stopImmediatePropagation(); + DomEvent.stop(e); + DomEvent.preventDefault(e); + e.originalEvent.stopImmediatePropagation(); - e.originalEvent.stopPropagation(); + e.originalEvent.stopPropagation(); - window.clearTimeout(this.#longPressTimer); + window.clearTimeout(this.#longPressTimer); - this.#isMouseOnCooldown = true; - this.#mouseCooldownTimer = window.setTimeout(() => { - this.#isMouseOnCooldown = false; - }, 200); + this.#isMouseOnCooldown = true; + this.#mouseCooldownTimer = window.setTimeout(() => { + this.#isMouseOnCooldown = false; + }, 200); + } } #onMouseDown(e: any) { - this.#isMouseDown = true; + if (getApp().getState() === OlympusState.IDLE || getApp().getState() === OlympusState.UNIT_CONTROL) { + this.#isMouseDown = true; - DomEvent.stop(e); - DomEvent.preventDefault(e); - e.originalEvent.stopImmediatePropagation(); + DomEvent.stop(e); + DomEvent.preventDefault(e); + e.originalEvent.stopImmediatePropagation(); - if (this.#isMouseOnCooldown) { - return; + if (this.#isMouseOnCooldown) { + return; + } + + this.#shortPressTimer = window.setTimeout(() => { + /* If the mouse is no longer being pressed, execute the short press action */ + if (!this.#isMouseDown) this.#onShortPress(e); + }, 200); + + this.#longPressTimer = window.setTimeout(() => { + /* If the mouse is still being pressed, execute the long press action */ + if (this.#isMouseDown) this.#onLongPress(e); + }, 350); } - - this.#shortPressTimer = window.setTimeout(() => { - /* If the mouse is no longer being pressed, execute the short press action */ - if (!this.#isMouseDown) this.#onShortPress(e); - }, 200); - - this.#longPressTimer = window.setTimeout(() => { - /* If the mouse is still being pressed, execute the long press action */ - if (this.#isMouseDown) this.#onLongPress(e); - }, 350); } #onShortPress(e: LeafletMouseEvent) { console.log(`Short press on ${this.getUnitName()}`); - if (getApp().getState() !== OlympusState.UNIT_CONTROL || e.originalEvent.ctrlKey) { + if (getApp().getState() === OlympusState.IDLE) { if (!e.originalEvent.ctrlKey) getApp().getUnitsManager().deselectAllUnits(); this.setSelected(!this.getSelected()); } else if (getApp().getState() === OlympusState.UNIT_CONTROL) { - if (getApp().getMap().getContextAction()) getApp().getMap().executeContextAction(this, null, e.originalEvent); - else { - getApp().getUnitsManager().deselectAllUnits(); + if (getApp().getMap().getContextAction() && getApp().getMap().getContextAction()?.getTarget() === ContextActionTarget.UNIT) { + getApp().getMap().executeContextAction(this, null, e.originalEvent); + } else { + if (!e.originalEvent.ctrlKey) getApp().getUnitsManager().deselectAllUnits(); this.setSelected(!this.getSelected()); } } else if (getApp().getState() === OlympusState.JTAC && getApp().getSubState() === JTACSubState.SELECT_TARGET) { @@ -1365,23 +1348,30 @@ export abstract class Unit extends CustomMarker { #onLongPress(e: any) { console.log(`Long press on ${this.getUnitName()}`); - if (e.originalEvent.button === 2 && !getApp().getMap().getContextAction()) { + if (getApp().getState() === OlympusState.UNIT_CONTROL && e.originalEvent.button === 2 && !getApp().getMap().getContextAction()) { getApp().setState(OlympusState.UNIT_CONTROL, UnitControlSubState.UNIT_CONTEXT_MENU); UnitContextMenuRequestEvent.dispatch(this); } } #onDoubleClick(e: any) { - console.log(`Double click on ${this.getUnitName()}`); + if (getApp().getState() === OlympusState.IDLE || getApp().getState() === OlympusState.UNIT_CONTROL) { + DomEvent.stop(e); + DomEvent.preventDefault(e); - window.clearTimeout(this.#shortPressTimer); - window.clearTimeout(this.#longPressTimer); + console.log(`Double click on ${this.getUnitName()}`); - /* Select all matching units in the viewport */ - const unitsManager = getApp().getUnitsManager(); - Object.values(unitsManager.getUnits()).forEach((unit: Unit) => { - if (unit.getAlive() === true && unit.getName() === this.getName() && unit.isInViewport()) unitsManager.selectUnit(unit.ID, false); - }); + window.clearTimeout(this.#shortPressTimer); + window.clearTimeout(this.#longPressTimer); + + /* Select all matching units in the viewport */ + if (getApp().getState() === OlympusState.IDLE || getApp().getState() === OlympusState.UNIT_CONTROL) { + const unitsManager = getApp().getUnitsManager(); + Object.values(unitsManager.getUnits()).forEach((unit: Unit) => { + if (unit.getAlive() === true && unit.getName() === this.getName() && unit.isInViewport()) unitsManager.selectUnit(unit.ID, false); + }); + } + } } #updateMarker() { diff --git a/frontend/server/src/app.ts b/frontend/server/src/app.ts index 59384e48..9fca49d7 100644 --- a/frontend/server/src/app.ts +++ b/frontend/server/src/app.ts @@ -76,6 +76,10 @@ module.exports = function (configLocation, viteProxy) { app.use("/api/elevation", elevationRouter); app.use("/api/databases", databasesRouter); app.use("/resources", resourcesRouter); + app.use("/express/api/airbases", airbasesRouter); + app.use("/express/api/elevation", elevationRouter); + app.use("/express/api/databases", databasesRouter); + app.use("/express/resources", resourcesRouter); /* Set default index */ if (!viteProxy) { diff --git a/frontend/server/src/audio/srshandler.ts b/frontend/server/src/audio/srshandler.ts index 5d3eed32..32ae3b81 100644 --- a/frontend/server/src/audio/srshandler.ts +++ b/frontend/server/src/audio/srshandler.ts @@ -29,6 +29,12 @@ export class SRSHandler { this.decodeData(data); }); this.ws.on("close", () => { + let CLIENT_DISCONNECT = { + Client: this.data, + MsgType: 5, + Version: SRS_VERSION, + }; + this.tcp.write(`${JSON.stringify(CLIENT_DISCONNECT)}\n`); this.tcp.end(); }); @@ -113,6 +119,13 @@ export class SRSHandler { this.data.RadioInfo.radios[idx].freq = setting.frequency; this.data.RadioInfo.radios[idx].modulation = setting.modulation; }); + + let RADIO_UPDATE = { + Client: this.data, + MsgType: 3, + Version: SRS_VERSION, + }; + this.tcp.write(`${JSON.stringify(RADIO_UPDATE)}\n`); break; default: break;