Fixed radios not working, added mouse coordinates panel, started readding tips

This commit is contained in:
Davide Passoni 2024-11-13 17:13:30 +01:00
parent b97713f8cc
commit fa215142ad
15 changed files with 419 additions and 208 deletions

View File

@ -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);

View File

@ -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);

View File

@ -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`);
}
}

View File

@ -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 */

View File

@ -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)
}
}

View File

@ -12,13 +12,15 @@ export const RadioSinkPanel = forwardRef((props: { radio: RadioSink; shortcutKey
useEffect(() => {
if (props.onExpanded) props.onExpanded();
}, [expanded])
}, [expanded]);
return (
<div
data-receiving={props.radio.getReceiving()}
className={`
flex flex-col content-center justify-between gap-2 rounded-md
bg-olympus-200/30 px-4 py-3
box-border flex flex-col content-center justify-between gap-2 rounded-md
border-2 border-transparent bg-olympus-200/30 px-4 py-3
data-[receiving='true']:border-white
`}
ref={ref}
>
@ -37,19 +39,22 @@ export const RadioSinkPanel = forwardRef((props: { radio: RadioSink; shortcutKey
data-expanded={expanded}
/>
</div>
{props.shortcutKey && (<>
<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>
{props.shortcutKey && (
<>
<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>
</>
)}
<span className="my-auto w-full">{props.radio.getName()} {!expanded && `: ${props.radio.getFrequency()/1e6} MHz ${props.radio.getModulation()? "FM": "AM"}`} {} </span>
<span className="my-auto w-full">
{props.radio.getName()} {!expanded && `: ${props.radio.getFrequency() / 1e6} MHz ${props.radio.getModulation() ? "FM" : "AM"}`} {}{" "}
</span>
<div
className={`
mb-auto ml-auto aspect-square cursor-pointer rounded-md p-2

View File

@ -1,8 +1,8 @@
import React, { useEffect, useState } from "react";
import { getApp } from "../../olympusapp";
import { IconDefinition } from "@fortawesome/free-solid-svg-icons";
import { faHandPointer, faJetFighter, faMap, IconDefinition } from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { AppStateChangedEvent } from "../../events";
import { MAP_OPTIONS_DEFAULTS, NO_SUBSTATE, OlympusState, OlympusSubState, SpawnSubState } from "../../constants/constants";
import { AppStateChangedEvent, MapOptionsChangedEvent } from "../../events";
export function ControlsPanel(props: {}) {
const [controls, setControls] = useState(
@ -14,22 +14,91 @@ export function ControlsPanel(props: {}) {
}[]
| null
);
const [appState, setAppState] = useState(OlympusState.NOT_INITIALIZED);
const [appSubState, setAppSubState] = useState(NO_SUBSTATE as OlympusSubState);
const [mapOptions, setMapOptions] = useState(MAP_OPTIONS_DEFAULTS);
useEffect(() => {
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 (
<div
className={`
absolute bottom-[20px] right-[10px] flex w-80 flex-col items-center
justify-between gap-1 p-3 text-sm
absolute right-[0px]
${mapOptions.showMinimap ? `bottom-[233px]` : `bottom-[65px]`}
flex w-[310px] flex-col items-center justify-between gap-1 p-3 text-sm
`}
>
{controls?.map((control) => {
@ -53,14 +122,9 @@ export function ControlsPanel(props: {}) {
return (
<div key={idx} className="flex gap-1">
<div>
{typeof action === "string" || typeof action === "number" ? (
action
) : (
<FontAwesomeIcon
icon={action}
className={`my-auto ml-auto`}
/>
)}
{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>}
@ -68,18 +132,6 @@ export function ControlsPanel(props: {}) {
);
})}
</div>
{/*}
<div className="my-auto">on</div>
<div
className={`
flex gap-1 rounded-full bg-olympus-500 px-2 py-0.5 text-sm
font-bold text-white
`}
>
<FontAwesomeIcon icon={control.target} className="my-auto" />
</div>
{*/}
</div>
);
})}

View File

@ -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 (
<div
className={`
absolute bottom-[20px] right-[310px] flex w-[380px] flex-col
items-center justify-between gap-2 rounded-lg bg-gray-200 p-3 text-sm
backdrop-blur-lg backdrop-grayscale
dark:bg-olympus-800/90 dark:text-gray-200
`}
>
{" "}
{open && (
<>
{bullseyes && (
<div className="flex w-full items-center justify-start">
<div
className={`
flex min-w-64 max-w-64 items-center justify-between gap-2
`}
>
<div className="flex justify-start gap-2">
<span
className={`
rounded-sm bg-blue-500 px-1 py-1 text-center font-bold
text-olympus-700
`}
>
<FaBullseye />
</span>
{(bullseyes[2].getLatLng().distanceTo(latlng) / 1852).toFixed(0)} /{" "}
{bearing(bullseyes[2].getLatLng().lat, bullseyes[2].getLatLng().lng, latlng.lat, latlng.lng).toFixed()}°
</div>
<div className="flex w-[50%] justify-start gap-2">
<span
className={`
rounded-sm bg-red-500 px-1 py-1 text-center font-bold
text-olympus-700
`}
>
<FaBullseye />
</span>
{(bullseyes[1].getLatLng().distanceTo(latlng) / 1852).toFixed(0)} /{" "}
{bearing(bullseyes[1].getLatLng().lat, bullseyes[1].getLatLng().lng, latlng.lat, latlng.lng).toFixed()}°
</div>
</div>
{/*}<div className="flex justify-start gap-2">
<span
className={`
rounded-sm bg-white px-1 py-1 text-center font-bold
text-olympus-700
`}
>
<FaJetFighter />
</span>
<div>
{(bullseyes[1].getLatLng().distanceTo(latlng) / 1852).toFixed(0)} /{" "}
{bearing(bullseyes[1].getLatLng().lat, bullseyes[1].getLatLng().lng, latlng.lat, latlng.lng).toFixed()}°
</div>
</div>
{*/}
</div>
)}
</>
)}
<div className="flex w-full items-center justify-between">
<OlLocation className="!min-w-64 !max-w-64 bg-transparent !p-0" location={latlng} />
<span
className={`
mr-2 rounded-sm bg-white px-1 py-1 text-center font-bold
text-olympus-700
`}
>
<FaMountain />
</span>
<div className="min-w-12">{mToFt(elevation).toFixed()}ft</div>
{open ? (
<FaChevronDown className="w-10 cursor-pointer" onClick={() => setOpen(!open)} />
) : (
<FaChevronUp className="w-10 cursor-pointer" onClick={() => setOpen(!open)} />
)}
</div>
</div>
);
}

View File

@ -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 ? (
<FaChevronUp
onClick={() => {
getApp().getMap().setOption("showMinimap", false);
}}
></FaChevronUp>
<FaChevronDown onClick={() => getApp().getMap().setOption("showMinimap", false)} />
) : (
<FaChevronDown
onClick={() => {
getApp().getMap().setOption("showMinimap", true);
}}
></FaChevronDown>
<FaChevronUp onClick={() => getApp().getMap().setOption("showMinimap", true)} />
)}
</div>
);

View File

@ -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"
></OlStateButton>

View File

@ -1,3 +1,7 @@
* {
user-select: none; /* Standard syntax */
}
/* width */
::-webkit-scrollbar {
width: 10px;

View File

@ -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() {
<div className="flex h-full w-full flex-row-reverse">
{appState === OlympusState.LOGIN && (
<>
<div
className={`
<div className={`
fixed left-0 top-0 z-30 h-full w-full bg-[#111111]/95
`}
></div>
`}></div>
<LoginModal />
</>
)}
@ -78,13 +77,10 @@ export function UI() {
<KeybindModal open={appState === OlympusState.OPTIONS && appSubState === OptionsSubstate.KEYBIND} />
<div id="map-container" className="z-0 h-full w-screen" />
<MainMenu open={appState === OlympusState.MAIN_MENU} onClose={() => getApp().setState(OlympusState.IDLE)} />
<SpawnMenu
open={appState === OlympusState.SPAWN && [SpawnSubState.NO_SUBSTATE, SpawnSubState.SPAWN_UNIT].includes(appSubState as SpawnSubState)}
onClose={() => getApp().setState(OlympusState.IDLE)}
/>
<OptionsMenu open={appState === OlympusState.OPTIONS} onClose={() => getApp().setState(OlympusState.IDLE)} />
<MainMenu open={appState === OlympusState.MAIN_MENU} onClose={() => getApp().setState(OlympusState.IDLE)} />
<SpawnMenu open={appState === OlympusState.SPAWN} onClose={() => getApp().setState(OlympusState.IDLE)} />
<OptionsMenu open={appState === OlympusState.OPTIONS} onClose={() => getApp().setState(OlympusState.IDLE)} />
<UnitControlMenu
open={
appState === OlympusState.UNIT_CONTROL &&
@ -96,12 +92,10 @@ export function UI() {
open={appState === OlympusState.UNIT_CONTROL && appSubState === UnitControlSubState.FORMATION}
onClose={() => getApp().setState(OlympusState.IDLE)}
/>
<DrawingMenu open={appState === OlympusState.DRAW} onClose={() => getApp().setState(OlympusState.IDLE)} />
<AirbaseMenu open={appState === OlympusState.AIRBASE} onClose={() => getApp().setState(OlympusState.IDLE)} />
<AudioMenu open={appState === OlympusState.AUDIO} onClose={() => getApp().setState(OlympusState.IDLE)} />
<GameMasterMenu open={appState === OlympusState.GAME_MASTER} onClose={() => getApp().setState(OlympusState.IDLE)} />
<UnitExplosionMenu
open={appState === OlympusState.UNIT_CONTROL && appSubState === UnitControlSubState.UNIT_EXPLOSION_MENU}
onClose={() => getApp().setState(OlympusState.IDLE)}
@ -110,12 +104,15 @@ export function UI() {
<MiniMapPanel />
<ControlsPanel />
<CoordinatesPanel />
<UnitControlBar />
<MapContextMenu />
<StarredSpawnContextMenu />
<SideBar />
<InfoBar />
<HotGroupBar />
<MapContextMenu />
<StarredSpawnContextMenu />
</div>
</div>
);

View File

@ -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() {

View File

@ -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) {

View File

@ -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;