From 7f5873b5b84e651ff383d4a55262a6e4b71dc96c Mon Sep 17 00:00:00 2001 From: Pax1601 Date: Mon, 28 Oct 2024 07:53:09 +0100 Subject: [PATCH] More refactoring of events --- frontend/react/src/audio/audiomanager.ts | 25 +- frontend/react/src/audio/audiosink.ts | 3 +- frontend/react/src/audio/audiosource.ts | 7 +- frontend/react/src/audio/filesource.ts | 9 +- frontend/react/src/audio/microphonesource.ts | 3 +- frontend/react/src/audio/radiosink.ts | 11 +- frontend/react/src/audio/unitsink.ts | 7 +- frontend/react/src/events.ts | 280 ++++++++++ .../src/map/coalitionarea/coalitioncircle.ts | 9 +- .../src/map/coalitionarea/coalitionpolygon.ts | 9 +- frontend/react/src/map/map.ts | 173 +++--- frontend/react/src/mission/airbase.ts | 9 +- frontend/react/src/mission/carrier.ts | 6 - frontend/react/src/mission/missionmanager.ts | 21 +- frontend/react/src/olympusapp.ts | 29 +- frontend/react/src/server/servermanager.ts | 23 +- frontend/react/src/statecontext.tsx | 15 +- .../src/ui/contextmenus/mapcontextmenu.tsx | 3 +- frontend/react/src/ui/panels/audiomenu.tsx | 7 +- .../react/src/ui/panels/controlspanel.tsx | 3 +- frontend/react/src/ui/panels/drawingmenu.tsx | 499 +++++++++--------- frontend/react/src/ui/panels/jtacmenu.tsx | 21 +- frontend/react/src/ui/panels/minimappanel.tsx | 66 +-- frontend/react/src/ui/panels/spawnmenu.tsx | 5 +- .../react/src/ui/panels/unitcontrolmenu.tsx | 271 ++++------ .../src/ui/panels/unitmousecontrolbar.tsx | 75 +-- frontend/react/src/ui/ui.tsx | 215 +++++--- frontend/react/src/unit/group.ts | 5 +- frontend/react/src/unit/unit.ts | 33 +- frontend/react/src/unit/unitsmanager.ts | 51 +- frontend/react/src/weapon/weapon.ts | 5 +- frontend/react/src/weapon/weaponsmanager.ts | 3 +- 32 files changed, 1010 insertions(+), 891 deletions(-) create mode 100644 frontend/react/src/events.ts diff --git a/frontend/react/src/audio/audiomanager.ts b/frontend/react/src/audio/audiomanager.ts index c6a49212..ac679f3e 100644 --- a/frontend/react/src/audio/audiomanager.ts +++ b/frontend/react/src/audio/audiomanager.ts @@ -11,6 +11,7 @@ import { AudioSink } from "./audiosink"; import { Unit } from "../unit/unit"; import { UnitSink } from "./unitsink"; import { AudioPacket, MessageType } from "./audiopacket"; +import { AudioManagerStateChangedEvent, AudioSinksChangedEvent, AudioSourcesChangedEvent, ConfigLoadedEvent, SRSClientsChangedEvent } from "../events"; export class AudioManager { #audioContext: AudioContext; @@ -35,7 +36,7 @@ export class AudioManager { #SRSClientUnitIDs: number[] = []; constructor() { - document.addEventListener("configLoaded", () => { + ConfigLoadedEvent.on(() => { let config = getApp().getConfig(); if (config["WSPort"]) { this.setPort(config["WSPort"]); @@ -95,7 +96,7 @@ export class AudioManager { }); } else { this.#SRSClientUnitIDs = JSON.parse(new TextDecoder().decode(packetUint8Array.slice(1))).unitIDs; - document.dispatchEvent(new CustomEvent("SRSClientsUpdated")); + SRSClientsChangedEvent.dispatch(); } } }); @@ -108,13 +109,13 @@ export class AudioManager { if (sink instanceof RadioSink) microphoneSource.connect(sink); }); this.#sources.push(microphoneSource); - document.dispatchEvent(new CustomEvent("audioSourcesUpdated")); + AudioSourcesChangedEvent.dispatch(getApp().getAudioManager().getSources()); /* Add two default radios */ this.addRadio(); this.addRadio(); }); - document.dispatchEvent(new CustomEvent("audioManagerStateChanged")); + AudioManagerStateChangedEvent.dispatch(this.#running); } stop() { @@ -128,9 +129,9 @@ export class AudioManager { this.#sources = []; this.#sinks = []; - document.dispatchEvent(new CustomEvent("audioSourcesUpdated")); - document.dispatchEvent(new CustomEvent("audioSinksUpdated")); - document.dispatchEvent(new CustomEvent("audioManagerStateChanged")); + AudioSourcesChangedEvent.dispatch(this.#sources); + AudioSinksChangedEvent.dispatch(this.#sinks); + AudioManagerStateChangedEvent.dispatch(this.#running); } setAddress(address) { @@ -153,7 +154,7 @@ export class AudioManager { } const newSource = new FileSource(file); this.#sources.push(newSource); - document.dispatchEvent(new CustomEvent("audioSourcesUpdated")); + AudioSourcesChangedEvent.dispatch(getApp().getAudioManager().getSources()); } getSources() { @@ -168,7 +169,7 @@ export class AudioManager { } source.disconnect(); this.#sources = this.#sources.filter((v) => v != source); - document.dispatchEvent(new CustomEvent("audioSourcesUpdated")); + AudioSourcesChangedEvent.dispatch(this.#sources); } addUnitSink(unit: Unit) { @@ -178,7 +179,7 @@ export class AudioManager { return; } this.#sinks.push(new UnitSink(unit)); - document.dispatchEvent(new CustomEvent("audioSinksUpdated")); + AudioSinksChangedEvent.dispatch(this.#sinks); } addRadio() { @@ -191,7 +192,7 @@ export class AudioManager { this.#sinks.push(newRadio); newRadio.setName(`Radio ${this.#sinks.length}`); this.#sources[0].connect(newRadio); - document.dispatchEvent(new CustomEvent("audioSinksUpdated")); + AudioSinksChangedEvent.dispatch(this.#sinks); } getSinks() { @@ -210,7 +211,7 @@ export class AudioManager { this.#sinks.forEach((sink) => { if (sink instanceof RadioSink) sink.setName(`Radio ${idx++}`); }); - document.dispatchEvent(new CustomEvent("audioSinksUpdated")); + AudioSinksChangedEvent.dispatch(getApp().getAudioManager().getSinks()); this.#sources.forEach((source) => { if (source.getConnectedTo().includes(sink)) source.disconnect(sink) diff --git a/frontend/react/src/audio/audiosink.ts b/frontend/react/src/audio/audiosink.ts index 5aea5edb..f9949796 100644 --- a/frontend/react/src/audio/audiosink.ts +++ b/frontend/react/src/audio/audiosink.ts @@ -1,3 +1,4 @@ +import { AudioSinksChangedEvent } from "../events"; import { getApp } from "../olympusapp"; /* Base audio sink class */ @@ -19,7 +20,7 @@ export abstract class AudioSink { disconnect() { this.getInputNode().disconnect(); - document.dispatchEvent(new CustomEvent("audioSinksUpdated")); + AudioSinksChangedEvent.dispatch(getApp().getAudioManager().getSinks()); } getInputNode() { diff --git a/frontend/react/src/audio/audiosource.ts b/frontend/react/src/audio/audiosource.ts index 7072cbe8..175832ff 100644 --- a/frontend/react/src/audio/audiosource.ts +++ b/frontend/react/src/audio/audiosource.ts @@ -1,3 +1,4 @@ +import { AudioSourcesChangedEvent } from "../events"; import { getApp } from "../olympusapp"; import { AudioSink } from "./audiosink"; import { WebAudioPeakMeter } from "web-audio-peak-meter"; @@ -21,7 +22,7 @@ export abstract class AudioSource { if (!this.#connectedTo.includes(sink)) { this.getOutputNode().connect(sink.getInputNode()); this.#connectedTo.push(sink); - document.dispatchEvent(new CustomEvent("audioSourcesUpdated")); + AudioSourcesChangedEvent.dispatch(getApp().getAudioManager().getSources()); } } @@ -33,7 +34,7 @@ export abstract class AudioSource { this.getOutputNode().disconnect(); } - document.dispatchEvent(new CustomEvent("audioSourcesUpdated")); + AudioSourcesChangedEvent.dispatch(getApp().getAudioManager().getSources()); } setName(name) { @@ -51,7 +52,7 @@ export abstract class AudioSource { setVolume(volume) { this.#volume = volume; this.#gainNode.gain.exponentialRampToValueAtTime(volume, getApp().getAudioManager().getAudioContext().currentTime + 0.02); - document.dispatchEvent(new CustomEvent("audioSourcesUpdated")); + AudioSourcesChangedEvent.dispatch(getApp().getAudioManager().getSources()); } getVolume() { diff --git a/frontend/react/src/audio/filesource.ts b/frontend/react/src/audio/filesource.ts index 647c134a..29647c6b 100644 --- a/frontend/react/src/audio/filesource.ts +++ b/frontend/react/src/audio/filesource.ts @@ -1,5 +1,6 @@ import { AudioSource } from "./audiosource"; import { getApp } from "../olympusapp"; +import { AudioSourcesChangedEvent } from "../events"; export class FileSource extends AudioSource { #file: File; @@ -50,7 +51,7 @@ export class FileSource extends AudioSource { const now = Date.now() / 1000; this.#lastUpdateTime = now; - document.dispatchEvent(new CustomEvent("audioSourcesUpdated")); + AudioSourcesChangedEvent.dispatch(getApp().getAudioManager().getSources()); this.#updateInterval = setInterval(() => { /* Update the current position value every second */ @@ -63,7 +64,7 @@ export class FileSource extends AudioSource { if (!this.#looping) this.pause(); } - document.dispatchEvent(new CustomEvent("audioSourcesUpdated")); + AudioSourcesChangedEvent.dispatch(getApp().getAudioManager().getSources()); }, 1000); } @@ -77,7 +78,7 @@ export class FileSource extends AudioSource { this.#currentPosition += now - this.#lastUpdateTime; clearInterval(this.#updateInterval); - document.dispatchEvent(new CustomEvent("audioSourcesUpdated")); + AudioSourcesChangedEvent.dispatch(getApp().getAudioManager().getSources()); } getPlaying() { @@ -110,7 +111,7 @@ export class FileSource extends AudioSource { setLooping(looping) { this.#looping = looping; if (this.#source) this.#source.loop = looping; - document.dispatchEvent(new CustomEvent("audioSourcesUpdated")); + AudioSourcesChangedEvent.dispatch(getApp().getAudioManager().getSources()); } getLooping() { diff --git a/frontend/react/src/audio/microphonesource.ts b/frontend/react/src/audio/microphonesource.ts index f744c804..17c584e2 100644 --- a/frontend/react/src/audio/microphonesource.ts +++ b/frontend/react/src/audio/microphonesource.ts @@ -1,3 +1,4 @@ +import { AudioSourcesChangedEvent } from "../events"; import { getApp } from "../olympusapp"; import { AudioSource } from "./audiosource"; @@ -20,6 +21,6 @@ export class MicrophoneSource extends AudioSource { } play() { - document.dispatchEvent(new CustomEvent("audioSourcesUpdated")); + AudioSourcesChangedEvent.dispatch(getApp().getAudioManager().getSources()); } } diff --git a/frontend/react/src/audio/radiosink.ts b/frontend/react/src/audio/radiosink.ts index f6767b4e..fa813e49 100644 --- a/frontend/react/src/audio/radiosink.ts +++ b/frontend/react/src/audio/radiosink.ts @@ -1,6 +1,7 @@ import { AudioSink } from "./audiosink"; import { AudioPacket } from "./audiopacket"; import { getApp } from "../olympusapp"; +import { AudioSinksChangedEvent } from "../events"; let packetID = 0; @@ -54,7 +55,7 @@ export class RadioSink extends AudioSink { setFrequency(frequency) { this.#frequency = frequency; - document.dispatchEvent(new CustomEvent("audioSinksUpdated")); + AudioSinksChangedEvent.dispatch(getApp().getAudioManager().getSinks()); } getFrequency() { @@ -63,7 +64,7 @@ export class RadioSink extends AudioSink { setModulation(modulation) { this.#modulation = modulation; - document.dispatchEvent(new CustomEvent("audioSinksUpdated")); + AudioSinksChangedEvent.dispatch(getApp().getAudioManager().getSinks()); } getModulation() { @@ -72,7 +73,7 @@ export class RadioSink extends AudioSink { setPtt(ptt) { this.#ptt = ptt; - document.dispatchEvent(new CustomEvent("audioSinksUpdated")); + AudioSinksChangedEvent.dispatch(getApp().getAudioManager().getSinks()); } getPtt() { @@ -81,7 +82,7 @@ export class RadioSink extends AudioSink { setTuned(tuned) { this.#tuned = tuned; - document.dispatchEvent(new CustomEvent("audioSinksUpdated")); + AudioSinksChangedEvent.dispatch(getApp().getAudioManager().getSinks()); } getTuned() { @@ -90,7 +91,7 @@ export class RadioSink extends AudioSink { setVolume(volume) { this.#volume = volume; - document.dispatchEvent(new CustomEvent("audioSinksUpdated")); + AudioSinksChangedEvent.dispatch(getApp().getAudioManager().getSinks()); } getVolume() { diff --git a/frontend/react/src/audio/unitsink.ts b/frontend/react/src/audio/unitsink.ts index 73861c70..087a56ae 100644 --- a/frontend/react/src/audio/unitsink.ts +++ b/frontend/react/src/audio/unitsink.ts @@ -2,6 +2,7 @@ import { AudioSink } from "./audiosink"; import { getApp } from "../olympusapp"; import { Unit } from "../unit/unit"; import { AudioUnitPipeline } from "./audiounitpipeline"; +import { AudioSinksChangedEvent, SRSClientsChangedEvent } from "../events"; /* Unit sink to implement a "loudspeaker" external sound. Useful for stuff like 5MC calls, air sirens, scramble calls and so on. Ideally, one may want to move this code to the backend*/ @@ -17,7 +18,7 @@ export class UnitSink extends AudioSink { this.#unit = unit; this.setName(`${unit.getUnitName()} - ${unit.getName()}`); - document.addEventListener("SRSClientsUpdated", () => { + SRSClientsChangedEvent.on(() => { this.#updatePipelines(); }); @@ -53,7 +54,7 @@ export class UnitSink extends AudioSink { Object.values(this.#unitPipelines).forEach((pipeline) => { pipeline.setPtt(ptt); }) - document.dispatchEvent(new CustomEvent("audioSinksUpdated")); + AudioSinksChangedEvent.dispatch(getApp().getAudioManager().getSinks()); } getPtt() { @@ -65,7 +66,7 @@ export class UnitSink extends AudioSink { Object.values(this.#unitPipelines).forEach((pipeline) => { pipeline.setMaxDistance(maxDistance); }) - document.dispatchEvent(new CustomEvent("audioSinksUpdated")); + AudioSinksChangedEvent.dispatch(getApp().getAudioManager().getSinks()); } getMaxDistance() { diff --git a/frontend/react/src/events.ts b/frontend/react/src/events.ts new file mode 100644 index 00000000..f5409271 --- /dev/null +++ b/frontend/react/src/events.ts @@ -0,0 +1,280 @@ +import { AudioSink } from "./audio/audiosink"; +import { AudioSource } from "./audio/audiosource"; +import { OlympusState, OlympusSubState } from "./constants/constants"; +import { ServerStatus } from "./interfaces"; +import { CoalitionCircle } from "./map/coalitionarea/coalitioncircle"; +import { CoalitionPolygon } from "./map/coalitionarea/coalitionpolygon"; +import { Airbase } from "./mission/airbase"; +import { MapHiddenTypes, MapOptions } from "./types/types"; +import { ContextAction } from "./unit/contextaction"; +import { ContextActionSet } from "./unit/contextactionset"; +import { Unit } from "./unit/unit"; + +export class BaseOlympusEvent { + static on(callback: () => void) { + document.addEventListener(this.name, (ev: CustomEventInit) => { + callback(); + }); + } + + static dispatch() { + document.dispatchEvent(new CustomEvent(this.name)); + console.log(`Event ${this.name} dispatched`); + } +} + +export class BaseUnitEvent { + static on(callback: (unit: Unit) => void) { + document.addEventListener(this.name, (ev: CustomEventInit) => { + callback(ev.detail.unit); + }); + } + + static dispatch(unit: Unit) { + document.dispatchEvent(new CustomEvent(this.name, { detail: { unit } })); + console.log(`Event ${this.name} dispatched`); + console.log(unit) + } +} + +/************** App events ***************/ +export class AppStateChangedEvent { + static on(callback: (state: OlympusState, subState: OlympusSubState) => void) { + document.addEventListener(this.name, (ev: CustomEventInit) => { + callback(ev.detail.state, ev.detail.subState); + }); + } + + static dispatch(state: OlympusState, subState: OlympusSubState) { + const detail = { state, subState }; + document.dispatchEvent(new CustomEvent(this.name, { detail })); + console.log(`Event ${this.name} dispatched with detail:`); + console.log(detail); + } +} + +export class ConfigLoadedEvent { + /* TODO add config */ + static on(callback: () => void) { + document.addEventListener(this.name, (ev: CustomEventInit) => { + callback(); + }); + } + + static dispatch() { + document.dispatchEvent(new CustomEvent(this.name)); + console.log(`Event ${this.name} dispatched`); + } +} + +export class ServerStatusUpdatedEvent { + static on(callback: (serverStatus: ServerStatus) => void) { + document.addEventListener(this.name, (ev: CustomEventInit) => { + callback(ev.detail.serverStatus); + }); + } + + static dispatch(serverStatus: ServerStatus) { + document.dispatchEvent(new CustomEvent(this.name, { detail: { serverStatus } })); + // Logging disabled since periodic + } +} + +/************** Map events ***************/ +export class HiddenTypesChangedEvent { + static on(callback: (hiddenTypes: MapHiddenTypes) => void) { + document.addEventListener(this.name, (ev: CustomEventInit) => { + callback(ev.detail.hiddenTypes); + }); + } + + static dispatch(hiddenTypes: MapHiddenTypes) { + document.dispatchEvent(new CustomEvent(this.name, {detail: {hiddenTypes}})); + console.log(`Event ${this.name} dispatched`); + } +} + +export class MapOptionsChangedEvent { + static on(callback: (mapOptions: MapOptions) => void) { + document.addEventListener(this.name, (ev: CustomEventInit) => { + callback(ev.detail.mapOptions); + }); + } + + static dispatch(mapOptions: MapOptions) { + document.dispatchEvent(new CustomEvent(this.name, {detail: {mapOptions}})); + console.log(`Event ${this.name} dispatched`); + } +} + +export class MapSourceChangedEvent { + static on(callback: (source: string) => void) { + document.addEventListener(this.name, (ev: CustomEventInit) => { + callback(ev.detail.source); + }); + } + + static dispatch(source: string) { + document.dispatchEvent(new CustomEvent(this.name, {detail: {source}})); + console.log(`Event ${this.name} dispatched`); + } +} + +export class CoalitionAreaSelectedEvent { + static on(callback: (coalitionArea: CoalitionCircle | CoalitionPolygon | null) => void) { + document.addEventListener(this.name, (ev: CustomEventInit) => { + callback(ev.detail.coalitionArea); + }); + } + + static dispatch(coalitionArea: CoalitionCircle | CoalitionPolygon | null) { + document.dispatchEvent(new CustomEvent(this.name, { detail: { coalitionArea } })); + console.log(`Event ${this.name} dispatched`); + } +} + +export class AirbaseSelectedEvent { + static on(callback: (airbase: Airbase) => void) { + document.addEventListener(this.name, (ev: CustomEventInit) => { + callback(ev.detail.airbase); + }); + } + + static dispatch(airbase: Airbase) { + document.dispatchEvent(new CustomEvent(this.name, { detail: { airbase } })); + console.log(`Event ${this.name} dispatched`); + } +} + +export class ContactsUpdatedEvent { + static on(callback: () => void) { + document.addEventListener(this.name, (ev: CustomEventInit) => { + callback(); + }); + } + + static dispatch() { + document.dispatchEvent(new CustomEvent(this.name)); + console.log(`Event ${this.name} dispatched`); + } +} + +export class ContextActionSetChangedEvent { + static on(callback: (contextActionSet: ContextActionSet) => void) { + document.addEventListener(this.name, (ev: CustomEventInit) => { + callback(ev.detail.contextActionSet); + }); + } + + static dispatch(contextActionSet: ContextActionSet) { + document.dispatchEvent(new CustomEvent(this.name, {detail: {contextActionSet}})); + console.log(`Event ${this.name} dispatched`); + } +} + +export class ContextActionChangedEvent { + static on(callback: (contextAction: ContextAction) => void) { + document.addEventListener(this.name, (ev: CustomEventInit) => { + callback(ev.detail.contextActionSet); + }); + } + + static dispatch(contextAction: ContextAction) { + document.dispatchEvent(new CustomEvent(this.name, {detail: {contextAction}})); + console.log(`Event ${this.name} dispatched`); + } +} + +export class UnitUpdatedEvent extends BaseUnitEvent {}; +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) { + document.addEventListener(this.name, (ev: CustomEventInit) => { + callback(ev.detail); + }); + } + + static dispatch(selectedUnits: Unit[]) { + document.dispatchEvent(new CustomEvent(this.name, {detail: selectedUnits})); + console.log(`Event ${this.name} dispatched`); + console.log(selectedUnits) + } +} + +/************** Command mode events ***************/ +export class CommandModeOptionsChangedEvent { + /* TODO: add command mode options */ + static on(callback: () => void) { + document.addEventListener(this.name, (ev: CustomEventInit) => { + callback(); + }); + } + + static dispatch() { + document.dispatchEvent(new CustomEvent(this.name)); + console.log(`Event ${this.name} dispatched`); + } +} + +/************** Audio backend events ***************/ +/* TODO: split into two events for signgle source changed */ +export class AudioSourcesChangedEvent { + /* TODO add audio sources */ + static on(callback: (audioSources: AudioSource[]) => void) { + document.addEventListener(this.name, (ev: CustomEventInit) => { + callback(ev.detail); + }); + } + + static dispatch(audioSources: AudioSource[]) { + document.dispatchEvent(new CustomEvent(this.name, {detail: {audioSources}})); + console.log(`Event ${this.name} dispatched`); + console.log(audioSources) + } +} + +/* TODO: split into two events for signgle sink changed */ +export class AudioSinksChangedEvent { + static on(callback: (audioSinks: AudioSink[]) => void) { + document.addEventListener(this.name, (ev: CustomEventInit) => { + callback(ev.detail); + }); + } + + static dispatch(audioSinks: AudioSink[]) { + document.dispatchEvent(new CustomEvent(this.name, {detail: {audioSinks}})); + console.log(`Event ${this.name} dispatched`); + console.log(audioSinks) + } +} + +export class SRSClientsChangedEvent { + /* TODO add clients */ + static on(callback: () => void) { + document.addEventListener(this.name, (ev: CustomEventInit) => { + callback(); + }); + } + + static dispatch() { + document.dispatchEvent(new CustomEvent(this.name)); + // Logging disabled since periodic + } +} + +export class AudioManagerStateChangedEvent { + static on(callback: (state: boolean) => void) { + document.addEventListener(this.name, (ev: CustomEventInit) => { + callback(ev.detail.state); + }); + } + + static dispatch(state: boolean) { + document.dispatchEvent(new CustomEvent(this.name, {detail: {state}})); + console.log(`Event ${this.name} dispatched`); + } +} diff --git a/frontend/react/src/map/coalitionarea/coalitioncircle.ts b/frontend/react/src/map/coalitionarea/coalitioncircle.ts index 73d888c0..217c928b 100644 --- a/frontend/react/src/map/coalitionarea/coalitioncircle.ts +++ b/frontend/react/src/map/coalitionarea/coalitioncircle.ts @@ -4,6 +4,7 @@ import { CoalitionAreaHandle } from "./coalitionareahandle"; import { BLUE_COMMANDER, RED_COMMANDER } from "../../constants/constants"; import { Coalition } from "../../types/types"; import * as turf from "@turf/turf"; +import { CoalitionAreaSelectedEvent } from "../../events"; let totalAreas = 0; @@ -59,13 +60,7 @@ export class CoalitionCircle extends Circle { this.#drawLabel(); this.setOpacity(selected ? 1 : 0.5); - if (selected) { - document.dispatchEvent( - new CustomEvent("coalitionAreaSelected", { - detail: this, - }) - ); - } + if (selected) CoalitionAreaSelectedEvent.dispatch(this); //@ts-ignore draggable option added by leaflet-path-drag selected ? this.dragging.enable() : this.dragging.disable(); diff --git a/frontend/react/src/map/coalitionarea/coalitionpolygon.ts b/frontend/react/src/map/coalitionarea/coalitionpolygon.ts index 9020bda6..32d64276 100644 --- a/frontend/react/src/map/coalitionarea/coalitionpolygon.ts +++ b/frontend/react/src/map/coalitionarea/coalitionpolygon.ts @@ -5,6 +5,7 @@ import { CoalitionAreaMiddleHandle } from "./coalitionareamiddlehandle"; import { BLUE_COMMANDER, RED_COMMANDER } from "../../constants/constants"; import { Coalition } from "../../types/types"; import { polyCenter } from "../../other/utils"; +import { CoalitionAreaSelectedEvent } from "../../events"; let totalAreas = 0; @@ -70,13 +71,7 @@ export class CoalitionPolygon extends Polygon { this.setEditing(false); } - if (selected) { - document.dispatchEvent( - new CustomEvent("coalitionAreaSelected", { - detail: this, - }) - ); - } + if (selected) CoalitionAreaSelectedEvent.dispatch(this); //@ts-ignore draggable option added by leaflet-path-drag selected ? this.dragging.enable() : this.dragging.disable(); diff --git a/frontend/react/src/map/map.ts b/frontend/react/src/map/map.ts index 8c10e18e..cd9d00ad 100644 --- a/frontend/react/src/map/map.ts +++ b/frontend/react/src/map/map.ts @@ -37,6 +37,8 @@ import { faDrawPolygon, faHandPointer, faJetFighter, faMap } from "@fortawesome/ import { ExplosionMarker } from "./markers/explosionmarker"; import { TextMarker } from "./markers/textmarker"; import { TargetMarker } from "./markers/targetmarker"; +import { AppStateChangedEvent, CoalitionAreaSelectedEvent, ConfigLoadedEvent, HiddenTypesChangedEvent, MapOptionsChangedEvent, MapSourceChangedEvent } from "../events"; +import { ContextActionSet } from "../unit/contextactionset"; /* Register the handler for the box selection */ L.Map.addInitHook("addHandler", "boxSelect", BoxSelect); @@ -105,8 +107,8 @@ export class Map extends L.Map { #coalitionAreas: (CoalitionPolygon | CoalitionCircle)[] = []; /* Unit context actions */ + #contextActionSet: null | ContextActionSet = null; #contextAction: null | ContextAction = null; - #defaultContextAction: null | ContextAction = null; /* Unit spawning */ #spawnRequestTable: SpawnRequestTable | null = null; @@ -181,11 +183,9 @@ export class Map extends L.Map { L.DomEvent.on(this.getContainer(), "touchend", this.#onMouseUp, this); /* Event listeners */ - document.addEventListener("appStateChanged", (ev: CustomEventInit) => { - this.#onStateChanged(ev.detail.state, ev.detail.subState); - }); + AppStateChangedEvent.on((state, subState) => this.#onStateChanged(state, subState)); - document.addEventListener("hiddenTypesChanged", (ev: CustomEventInit) => { + HiddenTypesChangedEvent.on((hiddenTypes) => { Object.values(getApp().getUnitsManager().getUnits()).forEach((unit: Unit) => unit.updateVisibility()); Object.values(getApp().getMissionManager().getAirbases()).forEach((airbase: Airbase) => { if (this.getHiddenTypes().airbase) airbase.removeFrom(this); @@ -198,10 +198,10 @@ export class Map extends L.Map { // this.#panToUnit(this.#centerUnit); //}); - document.addEventListener("mapOptionsChanged", () => { - this.getContainer().toggleAttribute("data-hide-labels", !this.getOptions().showUnitLabels); - //this.#cameraControlPort = this.getOptions()[DCS_LINK_PORT] as number; - //this.#cameraZoomRatio = 50 / (20 + (this.getOptions()[DCS_LINK_RATIO] as number)); + MapOptionsChangedEvent.on((options) => { + this.getContainer().toggleAttribute("data-hide-labels", !options.showUnitLabels); + //this.#cameraControlPort = options[DCS_LINK_PORT] as number; + //this.#cameraZoomRatio = 50 / (20 + (options[DCS_LINK_RATIO] as number)); if (this.#slaveDCSCamera) { this.#broadcastPosition(); @@ -213,7 +213,7 @@ export class Map extends L.Map { this.updateMinimap(); }); - document.addEventListener("configLoaded", () => { + ConfigLoadedEvent.on(() => { let config = getApp().getConfig(); let layerSet = false; @@ -248,73 +248,6 @@ export class Map extends L.Map { } }); - document.addEventListener("toggleCameraLinkStatus", () => { - this.setSlaveDCSCamera(!this.#slaveDCSCamera); - }); - - document.addEventListener("slewCameraToPosition", () => { - this.#broadcastPosition(); - }); - - document.addEventListener("selectJTACECHO", (ev: CustomEventInit) => { - if (!this.#ECHOPoint) { - this.#ECHOPoint = new TextMarker(ev.detail, "BP", "rgb(37 99 235)", { interactive: true, draggable: true }); - this.#ECHOPoint.addTo(this); - this.#ECHOPoint.on("dragstart", (event) => { - event.target.options["freeze"] = true; - }); - this.#ECHOPoint.on("dragend", (event) => { - document.dispatchEvent(new CustomEvent("selectJTACECHO", { detail: this.#ECHOPoint?.getLatLng() })); - event.target.options["freeze"] = false; - }); - this.#ECHOPoint.on("click", (event) => { - getApp().setState(OlympusState.JTAC) - }) - } else this.#ECHOPoint.setLatLng(ev.detail); - }); - - document.addEventListener("selectJTACIP", (ev: CustomEventInit) => { - if (!this.#IPPoint) { - this.#IPPoint = new TextMarker(ev.detail, "IP", "rgb(168 85 247)", { interactive: true, draggable: true }); - this.#IPPoint.addTo(this); - this.#IPPoint.on("dragstart", (event) => { - event.target.options["freeze"] = true; - }); - this.#IPPoint.on("dragend", (event) => { - document.dispatchEvent(new CustomEvent("selectJTACIP", { detail: this.#IPPoint?.getLatLng() })); - event.target.options["freeze"] = false; - }); - this.#IPPoint.on("click", (event) => { - getApp().setState(OlympusState.JTAC) - }) - } else this.#IPPoint.setLatLng(ev.detail); - - this.#drawIPToTargetLine(); - }); - - document.addEventListener("selectJTACTarget", (ev: CustomEventInit) => { - if (ev.detail.location) { - if (!this.#targetPoint) { - this.#targetPoint = new TargetMarker(ev.detail.location, { interactive: true, draggable: true }); - this.#targetPoint.addTo(this); - this.#targetPoint.on("dragstart", (event) => { - event.target.options["freeze"] = true; - }); - this.#targetPoint.on("dragend", (event) => { - document.dispatchEvent(new CustomEvent("selectJTACTarget", { detail: { location: this.#targetPoint?.getLatLng() } })); - event.target.options["freeze"] = false; - }); - this.#targetPoint.on("click", (event) => { - getApp().setState(OlympusState.JTAC) - }) - } else this.#targetPoint.setLatLng(ev.detail.location); - } else { - this.#targetPoint?.removeFrom(this); - this.#targetPoint = null; - } - this.#drawIPToTargetLine(); - }); - /* Pan interval */ this.#panInterval = window.setInterval(() => { if (this.#panUp || this.#panDown || this.#panRight || this.#panLeft) @@ -395,7 +328,7 @@ export class Map extends L.Map { } this.#layerName = layerName; - document.dispatchEvent(new CustomEvent("mapSourceChanged", { detail: layerName })); + MapSourceChangedEvent.dispatch(layerName); } getLayerName() { @@ -414,12 +347,12 @@ export class Map extends L.Map { this.#effectRequestTable = effectRequestTable; } - setContextAction(contextAction: ContextAction | null) { - this.#contextAction = contextAction; + setContextActionSet(contextActionSet: ContextActionSet | null) { + this.#contextActionSet = contextActionSet; } - setDefaultContextAction(defaultContextAction: ContextAction | null) { - this.#defaultContextAction = defaultContextAction; + setContextAction(contextAction: ContextAction | null) { + this.#contextAction = contextAction; } #onStateChanged(state: OlympusState, subState: OlympusSubState) { @@ -459,13 +392,11 @@ export class Map extends L.Map { } else if (state === OlympusState.UNIT_CONTROL) { console.log(`Context action:`); console.log(this.#contextAction); - console.log(`Default context action callback:`); - console.log(this.#defaultContextAction); } else if (state === OlympusState.DRAW) { if (subState == DrawSubState.DRAW_POLYGON) { this.#coalitionAreas.push(new CoalitionPolygon([])); this.#coalitionAreas[this.#coalitionAreas.length - 1].addTo(this); - this.#coalitionAreas[this.#coalitionAreas.length - 1].setSelected(true); + this.#coalitionAreas[this.#coalitionAreas.length - 1].setSelected(true); } else if (subState === DrawSubState.DRAW_CIRCLE) { this.#coalitionAreas.push(new CoalitionCircle(new L.LatLng(0, 0), { radius: 1000 })); this.#coalitionAreas[this.#coalitionAreas.length - 1].addTo(this); @@ -683,12 +614,7 @@ export class Map extends L.Map { } deselectAllCoalitionAreas() { - document.dispatchEvent( - new CustomEvent("coalitionAreaSelected", { - detail: null, - }) - ); - + CoalitionAreaSelectedEvent.dispatch(null); this.#coalitionAreas.forEach((coalitionArea: CoalitionPolygon | CoalitionCircle) => coalitionArea.setSelected(false)); } @@ -700,7 +626,7 @@ export class Map extends L.Map { setHiddenType(key: string, value: boolean) { this.#hiddenTypes[key] = value; - document.dispatchEvent(new CustomEvent("hiddenTypesChanged")); + HiddenTypesChangedEvent.dispatch(this.#hiddenTypes); } getHiddenTypes() { @@ -847,7 +773,7 @@ export class Map extends L.Map { setOption(key, value) { this.#options[key] = value; - document.dispatchEvent(new CustomEvent("mapOptionsChanged")); + MapOptionsChangedEvent.dispatch(this.#options); } getOptions() { @@ -906,16 +832,16 @@ export class Map extends L.Map { this.#contextAction?.executeCallback(targetUnit, targetPosition); } + getContextActionSet() { + return this.#contextActionSet; + } + getContextAction() { return this.#contextAction; } executeDefaultContextAction(targetUnit: Unit | null, targetPosition: L.LatLng | null) { - if (this.#defaultContextAction) this.#defaultContextAction.executeCallback(targetUnit, targetPosition); - } - - getDefaultContextAction() { - return this.#defaultContextAction; + this.#contextActionSet?.getDefaultContextAction()?.executeCallback(targetUnit, targetPosition); } preventClicks() { @@ -939,7 +865,6 @@ export class Map extends L.Map { #onSelectionEnd(e: any) { getApp().getUnitsManager().selectFromBounds(e.selectionBounds); - document.dispatchEvent(new CustomEvent("mapSelectionEnd")); this.#selecting = false; } @@ -1037,7 +962,7 @@ export class Map extends L.Map { for (let idx = 0; idx < this.#coalitionAreas.length; idx++) { if (areaContains(pressLocation, this.#coalitionAreas[idx])) { this.#coalitionAreas[idx].setSelected(true); - getApp().setState(OlympusState.DRAW, DrawSubState.EDIT) + getApp().setState(OlympusState.DRAW, DrawSubState.EDIT); break; } } @@ -1047,17 +972,57 @@ export class Map extends L.Map { if (this.#contextAction !== null) this.executeContextAction(null, pressLocation); else getApp().setState(OlympusState.IDLE); } else if (e.originalEvent.buttons === 2) { - if (this.#defaultContextAction !== null) this.executeDefaultContextAction(null, pressLocation); + this.executeDefaultContextAction(null, pressLocation); } } else if (getApp().getState() === OlympusState.JTAC) { if (getApp().getSubState() === JTACSubState.SELECT_TARGET) { - document.dispatchEvent(new CustomEvent("selectJTACTarget", { detail: { location: pressLocation } })); + if (!this.#targetPoint) { + this.#targetPoint = new TextMarker(pressLocation, "BP", "rgb(37 99 235)", { interactive: true, draggable: true }); + this.#targetPoint.addTo(this); + this.#targetPoint.on("dragstart", (event) => { + event.target.options["freeze"] = true; + }); + this.#targetPoint.on("dragend", (event) => { + getApp().setState(OlympusState.JTAC); + event.target.options["freeze"] = false; + }); + this.#targetPoint.on("click", (event) => { + getApp().setState(OlympusState.JTAC); + }); + } else this.#targetPoint.setLatLng(pressLocation); } else if (getApp().getSubState() === JTACSubState.SELECT_ECHO_POINT) { - document.dispatchEvent(new CustomEvent("selectJTACECHO", { detail: pressLocation })); + if (!this.#ECHOPoint) { + this.#ECHOPoint = new TextMarker(pressLocation, "BP", "rgb(37 99 235)", { interactive: true, draggable: true }); + this.#ECHOPoint.addTo(this); + this.#ECHOPoint.on("dragstart", (event) => { + event.target.options["freeze"] = true; + }); + this.#ECHOPoint.on("dragend", (event) => { + getApp().setState(OlympusState.JTAC); + event.target.options["freeze"] = false; + }); + this.#ECHOPoint.on("click", (event) => { + getApp().setState(OlympusState.JTAC); + }); + } else this.#ECHOPoint.setLatLng(pressLocation); } else if (getApp().getSubState() === JTACSubState.SELECT_IP) { - document.dispatchEvent(new CustomEvent("selectJTACIP", { detail: pressLocation })); + if (!this.#IPPoint) { + this.#IPPoint = new TextMarker(pressLocation, "BP", "rgb(37 99 235)", { interactive: true, draggable: true }); + this.#IPPoint.addTo(this); + this.#IPPoint.on("dragstart", (event) => { + event.target.options["freeze"] = true; + }); + this.#IPPoint.on("dragend", (event) => { + getApp().setState(OlympusState.JTAC); + event.target.options["freeze"] = false; + }); + this.#IPPoint.on("click", (event) => { + getApp().setState(OlympusState.JTAC); + }); + } else this.#IPPoint.setLatLng(pressLocation); } getApp().setState(OlympusState.JTAC); + this.#drawIPToTargetLine(); } else { } } diff --git a/frontend/react/src/mission/airbase.ts b/frontend/react/src/mission/airbase.ts index 7e3e0655..31e198fb 100644 --- a/frontend/react/src/mission/airbase.ts +++ b/frontend/react/src/mission/airbase.ts @@ -4,6 +4,7 @@ import { SVGInjector } from "@tanem/svg-injector"; import { AirbaseChartData, AirbaseOptions } from "../interfaces"; import { getApp } from "../olympusapp"; import { OlympusState } from "../constants/constants"; +import { AirbaseSelectedEvent } from "../events"; export class Airbase extends CustomMarker { #name: string = ""; @@ -27,7 +28,7 @@ export class Airbase extends CustomMarker { this.addEventListener("click", (ev) => { if (getApp().getState() === OlympusState.IDLE) { getApp().setState(OlympusState.AIRBASE) - // TODO: document.dispatchEvent(new CustomEvent("airbaseClick", { detail: ev.target })); + AirbaseSelectedEvent.dispatch(this) } }); } @@ -48,12 +49,6 @@ export class Airbase extends CustomMarker { 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 })); - }); - el.addEventListener("mouseout", (ev) => { - document.dispatchEvent(new CustomEvent("airbasemouseout", { detail: this })); - }); el.dataset.coalition = this.#coalition; } diff --git a/frontend/react/src/mission/carrier.ts b/frontend/react/src/mission/carrier.ts index 35b5412b..74d15084 100644 --- a/frontend/react/src/mission/carrier.ts +++ b/frontend/react/src/mission/carrier.ts @@ -18,12 +18,6 @@ export class Carrier extends Airbase { 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(); } diff --git a/frontend/react/src/mission/missionmanager.ts b/frontend/react/src/mission/missionmanager.ts index 1167a38b..67d49f78 100644 --- a/frontend/react/src/mission/missionmanager.ts +++ b/frontend/react/src/mission/missionmanager.ts @@ -14,6 +14,7 @@ import { AirbasesData, BullseyesData, CommandModeOptions, DateAndTime, MissionDa import { Coalition } from "../types/types"; import { Carrier } from "./carrier"; import { NavyUnit } from "../unit/unit"; +import { CommandModeOptionsChangedEvent } from "../events"; /** The MissionManager */ export class MissionManager { @@ -42,17 +43,7 @@ export class MissionManager { //#commandModeErasDropdown: Dropdown; #coalitions: { red: string[]; blue: string[] } = { red: [], blue: [] }; - constructor() { - document.addEventListener("applycommandModeOptions", () => this.#applycommandModeOptions()); - document.addEventListener("showCommandModeDialog", () => this.showCommandModeDialog()); - document.addEventListener("toggleSpawnRestrictions", (ev: CustomEventInit) => { - this.#toggleSpawnRestrictions(ev.detail._element.checked); - }); - - /* command-mode settings dialog */ - //this.#commandModeDialog = document.querySelector("#command-mode-settings-dialog") as HTMLElement; - //this.#commandModeErasDropdown = new Dropdown("command-mode-era-options", () => {}); - } + constructor() {} /** Update location of bullseyes * @@ -303,13 +294,7 @@ export class MissionManager { this.refreshSpawnPoints(); if (commandModeOptionsChanged) { - document.dispatchEvent(new CustomEvent("commandModeOptionsChanged", { detail: this })); - document.getElementById("command-mode-toolbar")?.classList.remove("hide"); - const el = document.getElementById("command-mode"); - if (el) { - el.dataset.mode = commandModeOptions.commandMode; - el.textContent = commandModeOptions.commandMode.toUpperCase(); - } + CommandModeOptionsChangedEvent.dispatch(); } document diff --git a/frontend/react/src/olympusapp.ts b/frontend/react/src/olympusapp.ts index 84ebaf47..319b2462 100644 --- a/frontend/react/src/olympusapp.ts +++ b/frontend/react/src/olympusapp.ts @@ -27,12 +27,12 @@ import { groundUnitDatabase } from "./unit/databases/groundunitdatabase"; import { navyUnitDatabase } from "./unit/databases/navyunitdatabase"; import { Coalition, Context } from "./types/types"; import { Unit } from "./unit/unit"; +import { AppStateChangedEvent, ConfigLoadedEvent, SelectedUnitsChangedEvent } from "./events"; export var VERSION = "{{OLYMPUS_VERSION_NUMBER}}"; export var IP = window.location.toString(); export var connectedToServer = true; // TODO Temporary - export class OlympusApp { /* Global data */ #latestVersion: string | undefined = undefined; @@ -42,7 +42,7 @@ export class OlympusApp { #events = { [OlympusEvent.STATE_CHANGED]: [] as ((state: OlympusState, subState: OlympusSubState) => void)[], - [OlympusEvent.UNITS_SELECTED]: [] as ((units: Unit[]) => void)[] + [OlympusEvent.UNITS_SELECTED]: [] as ((units: Unit[]) => void)[], }; /* Main leaflet map, extended by custom methods */ @@ -60,7 +60,12 @@ export class OlympusApp { /* Current context */ #context: Context = DEFAULT_CONTEXT; - constructor() {} + constructor() { + SelectedUnitsChangedEvent.on((selectedUnits) => { + if (selectedUnits.length > 0) this.setState(OlympusState.UNIT_CONTROL); + else this.getState() === OlympusState.UNIT_CONTROL && this.setState(OlympusState.IDLE) + }); + } getCurrentContext() { return this.#context; @@ -178,10 +183,9 @@ export class OlympusApp { }) .then((res) => { this.#config = res; - document.dispatchEvent(new CustomEvent("configLoaded")); + ConfigLoadedEvent.dispatch(); // TODO actually dispatch the config this.setState(OlympusState.LOGIN); }); - } getConfig() { @@ -192,8 +196,8 @@ export class OlympusApp { this.#state = state; this.#subState = subState; - console.log(`App state set to ${state}, substate ${subState}`) - this.dispatchEvent(OlympusEvent.STATE_CHANGED, state, subState) + console.log(`App state set to ${state}, substate ${subState}`); + AppStateChangedEvent.dispatch(state, subState); } getState() { @@ -203,15 +207,4 @@ export class OlympusApp { getSubState() { return this.#subState; } - - registerEventCallback(event: OlympusEvent, callback: any) { - this.#events[event].push(callback) - } - - dispatchEvent(event: OlympusEvent, ...args) { - console.log(`Dispatching event ${event}. Arguments: ${args}`) - this.#events[event].forEach((event) => { - event(args); - }) - } } diff --git a/frontend/react/src/server/servermanager.ts b/frontend/react/src/server/servermanager.ts index 1d4f8d02..2cd64ad3 100644 --- a/frontend/react/src/server/servermanager.ts +++ b/frontend/react/src/server/servermanager.ts @@ -14,6 +14,7 @@ import { reactionsToThreat, } from "../constants/constants"; import { AirbasesData, BullseyesData, GeneralSettings, MissionData, Radio, ServerRequestOptions, ServerStatus, TACAN } from "../interfaces"; +import { ServerStatusUpdatedEvent } from "../events"; export class ServerManager { #connected: boolean = false; @@ -142,7 +143,7 @@ export class ServerManager { setAddress(address: string) { this.#REST_ADDRESS = `${address.replace("vite/", "").replace("vite", "")}olympus`; - + console.log(`Setting REST address to ${this.#REST_ADDRESS}`); } @@ -610,18 +611,14 @@ export class ServerManager { this.#serverIsPaused = elapsedMissionTime === this.#previousMissionElapsedTime; this.#previousMissionElapsedTime = elapsedMissionTime; - document.dispatchEvent( - new CustomEvent("serverStatusUpdated", { - detail: { - frameRate: getApp().getMissionManager().getFrameRate(), - load: getApp().getMissionManager().getLoad(), - elapsedTime: getApp().getMissionManager().getDateAndTime().elapsedTime, - missionTime: getApp().getMissionManager().getDateAndTime().time, - connected: this.getConnected(), - paused: this.getPaused(), - } as ServerStatus, - }) - ); + ServerStatusUpdatedEvent.dispatch({ + frameRate: getApp().getMissionManager().getFrameRate(), + load: getApp().getMissionManager().getLoad(), + elapsedTime: getApp().getMissionManager().getDateAndTime().elapsedTime, + missionTime: getApp().getMissionManager().getDateAndTime().time, + connected: this.getConnected(), + paused: this.getPaused(), + } as ServerStatus); }, 1000) ); diff --git a/frontend/react/src/statecontext.tsx b/frontend/react/src/statecontext.tsx index a6f93ec9..1e7b1755 100644 --- a/frontend/react/src/statecontext.tsx +++ b/frontend/react/src/statecontext.tsx @@ -1,5 +1,11 @@ import { createContext } from "react"; import { MAP_HIDDEN_TYPES_DEFAULTS, MAP_OPTIONS_DEFAULTS, NO_SUBSTATE, OlympusState, OlympusSubState } from "./constants/constants"; +import { Unit } from "./unit/unit"; +import { AudioSource } from "./audio/audiosource"; +import { AudioSink } from "./audio/audiosink"; +import { ServerStatus } from "./interfaces"; +import { ContextActionSet } from "./unit/contextactionset"; +import { ContextAction } from "./unit/contextaction"; export const StateContext = createContext({ appState: OlympusState.NOT_INITIALIZED as OlympusState, @@ -7,7 +13,14 @@ export const StateContext = createContext({ mapHiddenTypes: MAP_HIDDEN_TYPES_DEFAULTS, mapOptions: MAP_OPTIONS_DEFAULTS, mapSources: [] as string[], - activeMapSource: "" + activeMapSource: "", + selectedUnits: [] as Unit[], + audioSources: [] as AudioSource[], + audioSinks: [] as AudioSink[], + audioManagerState: false, + serverStatus: {} as ServerStatus, + contextActionSet: null as ContextActionSet | null, + contextAction: null as ContextAction | null }); export const StateProvider = StateContext.Provider; diff --git a/frontend/react/src/ui/contextmenus/mapcontextmenu.tsx b/frontend/react/src/ui/contextmenus/mapcontextmenu.tsx index ec39e50a..351571a4 100644 --- a/frontend/react/src/ui/contextmenus/mapcontextmenu.tsx +++ b/frontend/react/src/ui/contextmenus/mapcontextmenu.tsx @@ -7,6 +7,7 @@ import { CONTEXT_ACTION_COLORS } from "../../constants/constants"; import { OlDropdownItem } from "../components/oldropdown"; import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; import { LatLng } from "leaflet"; +import { SelectionClearedEvent } from "../../events"; export function MapContextMenu(props: {}) { const [open, setOpen] = useState(false); @@ -71,7 +72,7 @@ export function MapContextMenu(props: {}) { setOpen(false); }); - document.addEventListener("clearSelection", () => { + SelectionClearedEvent.on(() => { setOpen(false); }); }, []); diff --git a/frontend/react/src/ui/panels/audiomenu.tsx b/frontend/react/src/ui/panels/audiomenu.tsx index 5b716e01..16ef6f10 100644 --- a/frontend/react/src/ui/panels/audiomenu.tsx +++ b/frontend/react/src/ui/panels/audiomenu.tsx @@ -11,6 +11,7 @@ import { UnitSinkPanel } from "./components/unitsinkpanel"; import { UnitSink } from "../../audio/unitsink"; import { FaMinus, FaVolumeHigh } from "react-icons/fa6"; import { getRandomColor } from "../../other/utils"; +import { AudioManagerStateChangedEvent, AudioSinksChangedEvent, AudioSourcesChangedEvent } from "../../events"; let shortcutKeys = ["Z", "X", "C", "V", "B", "N", "M", "K", "L"]; @@ -36,7 +37,7 @@ export function AudioMenu(props: { open: boolean; onClose: () => void; children? useEffect(() => { /* Force a rerender */ - document.addEventListener("audioSinksUpdated", () => { + AudioSinksChangedEvent.on(() => { setSinks( getApp() ?.getAudioManager() @@ -47,7 +48,7 @@ export function AudioMenu(props: { open: boolean; onClose: () => void; children? }); /* Force a rerender */ - document.addEventListener("audioSourcesUpdated", () => { + AudioSourcesChangedEvent.on(() => { setSources( getApp() ?.getAudioManager() @@ -56,7 +57,7 @@ export function AudioMenu(props: { open: boolean; onClose: () => void; children? ); }); - document.addEventListener("audioManagerStateChanged", () => { + AudioManagerStateChangedEvent.on(() => { setAudioManagerEnabled(getApp().getAudioManager().isRunning()); }); }, []); diff --git a/frontend/react/src/ui/panels/controlspanel.tsx b/frontend/react/src/ui/panels/controlspanel.tsx index 20f1119b..d121d4be 100644 --- a/frontend/react/src/ui/panels/controlspanel.tsx +++ b/frontend/react/src/ui/panels/controlspanel.tsx @@ -2,6 +2,7 @@ import React, { useEffect, useState } from "react"; import { getApp } from "../../olympusapp"; import { IconDefinition } from "@fortawesome/free-solid-svg-icons"; import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; +import { AppStateChangedEvent } from "../../events"; export function ControlsPanel(props: {}) { const [controls, setControls] = useState( @@ -19,7 +20,7 @@ export function ControlsPanel(props: {}) { }); useEffect(() => { - document.addEventListener("appStateChanged", (ev) => { + AppStateChangedEvent.on(() => { setControls(getApp().getMap().getCurrentControls()); }); }, []); diff --git a/frontend/react/src/ui/panels/drawingmenu.tsx b/frontend/react/src/ui/panels/drawingmenu.tsx index 3c6c69b2..1465c273 100644 --- a/frontend/react/src/ui/panels/drawingmenu.tsx +++ b/frontend/react/src/ui/panels/drawingmenu.tsx @@ -13,10 +13,10 @@ import { Coalition } from "../../types/types"; import { OlRangeSlider } from "../components/olrangeslider"; import { CoalitionCircle } from "../../map/coalitionarea/coalitioncircle"; import { DrawSubState, NO_SUBSTATE, OlympusState } from "../../constants/constants"; +import { StateConsumer } from "../../statecontext"; +import { CoalitionAreaSelectedEvent } from "../../events"; export function DrawingMenu(props: { open: boolean; onClose: () => void }) { - const [appState, setAppState] = useState(OlympusState.NOT_INITIALIZED); - const [appSubState, setAppSubState] = useState(NO_SUBSTATE); const [activeCoalitionArea, setActiveCoalitionArea] = useState(null as null | CoalitionPolygon | CoalitionCircle); const [areaCoalition, setAreaCoalition] = useState("blue" as Coalition); const [IADSDensity, setIADSDensity] = useState(50); @@ -32,276 +32,275 @@ export function DrawingMenu(props: { open: boolean; onClose: () => void }) { // TODO ///* If we are not in polygon drawing mode, force the draw polygon button off */ //if (drawingPolygon && getApp().getState() !== COALITIONAREA_DRAW_POLYGON) setDrawingPolygon(false); -// + // ///* If we are not in circle drawing mode, force the draw circle button off */ //if (drawingCircle && getApp().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); -// + // ///* Align the state of the coalition toggle to the coalition of the area */ //if (activeCoalitionArea && activeCoalitionArea?.getCoalition() !== areaCoalition) setAreaCoalition(activeCoalitionArea?.getCoalition()); } }); useEffect(() => { - document.addEventListener("coalitionAreaSelected", (event: any) => { - setActiveCoalitionArea(event.detail); - }); - - document.addEventListener("appStateChanged", (ev: CustomEventInit) => { - setAppState(ev.detail.state); - setAppSubState(ev.detail.subState); - }); + CoalitionAreaSelectedEvent.on((coalitionArea) => setActiveCoalitionArea(coalitionArea)); }, []); return ( - { - getApp().setState(OlympusState.DRAW, DrawSubState.NO_SUBSTATE) - }} - > - <> - {appState === OlympusState.DRAW && appSubState !== DrawSubState.EDIT && ( -
- { - if (appSubState === DrawSubState.DRAW_POLYGON) getApp().setState(OlympusState.DRAW, DrawSubState.EDIT); - else getApp().setState(OlympusState.DRAW, DrawSubState.DRAW_POLYGON); - }} - > -
Add polygon
-
- { - if (appSubState === DrawSubState.DRAW_CIRCLE) getApp().setState(OlympusState.DRAW, DrawSubState.EDIT); - else getApp().setState(OlympusState.DRAW, DrawSubState.DRAW_CIRCLE); - }} - > -
Add circle
-
-
- )} - -
- {activeCoalitionArea !== null && appSubState === DrawSubState.EDIT && ( -
-
-
- Area label -
+ {(appState) => ( + { + getApp().setState(OlympusState.DRAW, DrawSubState.NO_SUBSTATE); + }} + > + <> + {appState.appState === OlympusState.DRAW && appState.appSubState !== DrawSubState.EDIT && ( +
+ { - getApp().getMap().deleteCoalitionArea(activeCoalitionArea); - setActiveCoalitionArea(null); + if (appState.appSubState === DrawSubState.DRAW_POLYGON) getApp().setState(OlympusState.DRAW, DrawSubState.EDIT); + else getApp().setState(OlympusState.DRAW, DrawSubState.DRAW_POLYGON); }} > - -
+
Add polygon
+ + { + if (appState.appSubState === DrawSubState.DRAW_CIRCLE) getApp().setState(OlympusState.DRAW, DrawSubState.EDIT); + else getApp().setState(OlympusState.DRAW, DrawSubState.DRAW_CIRCLE); + }} + > +
Add circle
+
- activeCoalitionArea.setLabelText(ev.currentTarget.value)} - > -
-
-
Coalition:
- { - let newCoalition = ""; - if (areaCoalition === "blue") newCoalition = "neutral"; - else if (areaCoalition === "neutral") newCoalition = "red"; - else if (areaCoalition === "red") newCoalition = "blue"; - setAreaCoalition(newCoalition as Coalition); - activeCoalitionArea.setCoalition(newCoalition as Coalition); - }} - > -
-
-
Automatic IADS generation
- - {getApp() - .getGroundUnitDatabase() - .getTypes() - .map((type, idx) => { - if (!(type in typesSelection)) { - typesSelection[type] = true; - setTypesSelection(JSON.parse(JSON.stringify(typesSelection))); - } - - return ( - - { - typesSelection[type] = ev.currentTarget.checked; - setTypesSelection(JSON.parse(JSON.stringify(typesSelection))); - }} - /> -
{type}
-
- ); - })} -
- - {getApp() - .getGroundUnitDatabase() - .getEras() - .map((era) => { - if (!(era in erasSelection)) { - erasSelection[era] = true; - setErasSelection(JSON.parse(JSON.stringify(erasSelection))); - } - - return ( - - { - erasSelection[era] = ev.currentTarget.checked; - setErasSelection(JSON.parse(JSON.stringify(erasSelection))); - }} - /> -
{era}
-
- ); - })} -
- - {["Short range", "Medium range", "Long range"].map((range) => { - if (!(range in rangesSelection)) { - rangesSelection[range] = true; - setRangesSelection(JSON.parse(JSON.stringify(rangesSelection))); - } - - return ( - - { - rangesSelection[range] = ev.currentTarget.checked; - setErasSelection(JSON.parse(JSON.stringify(rangesSelection))); - }} - /> -
{range}
-
- ); - })} -
-
-
-
IADS Density
-
- {IADSDensity}% + )} + +
+ {activeCoalitionArea !== null && appState.appSubState === DrawSubState.EDIT && ( +
+
+
+ Area label +
{ + getApp().getMap().deleteCoalitionArea(activeCoalitionArea); + setActiveCoalitionArea(null); + }} + > + +
-
- { - setIADSDensity(Number(ev.currentTarget.value)); - }} - > -
-
-
-
IADS Distribution
-
- {IADSDistribution}% -
+ placeholder={activeCoalitionArea.getLabelText()} + onInput={(ev) => activeCoalitionArea.setLabelText(ev.currentTarget.value)} + > +
+
+
Coalition:
+ { + let newCoalition = ""; + if (areaCoalition === "blue") newCoalition = "neutral"; + else if (areaCoalition === "neutral") newCoalition = "red"; + else if (areaCoalition === "red") newCoalition = "blue"; + setAreaCoalition(newCoalition as Coalition); + activeCoalitionArea.setCoalition(newCoalition as Coalition); + }} + > +
+
+
Automatic IADS generation
+ + {getApp() + .getGroundUnitDatabase() + .getTypes() + .map((type, idx) => { + if (!(type in typesSelection)) { + typesSelection[type] = true; + setTypesSelection(JSON.parse(JSON.stringify(typesSelection))); + } + + return ( + + { + typesSelection[type] = ev.currentTarget.checked; + setTypesSelection(JSON.parse(JSON.stringify(typesSelection))); + }} + /> +
{type}
+
+ ); + })} +
+ + {getApp() + .getGroundUnitDatabase() + .getEras() + .map((era) => { + if (!(era in erasSelection)) { + erasSelection[era] = true; + setErasSelection(JSON.parse(JSON.stringify(erasSelection))); + } + + return ( + + { + erasSelection[era] = ev.currentTarget.checked; + setErasSelection(JSON.parse(JSON.stringify(erasSelection))); + }} + /> +
{era}
+
+ ); + })} +
+ + {["Short range", "Medium range", "Long range"].map((range) => { + if (!(range in rangesSelection)) { + rangesSelection[range] = true; + setRangesSelection(JSON.parse(JSON.stringify(rangesSelection))); + } + + return ( + + { + rangesSelection[range] = ev.currentTarget.checked; + setErasSelection(JSON.parse(JSON.stringify(rangesSelection))); + }} + /> +
{range}
+
+ ); + })} +
+
+
+
IADS Density
+
+ {IADSDensity}% +
+
+ { + setIADSDensity(Number(ev.currentTarget.value)); + }} + > +
+
+
+
IADS Distribution
+
+ {IADSDistribution}% +
+
+ { + setIADSDistribution(Number(ev.target.value)); + }} + > +
+
+ { + setForceCoalitionApproriateUnits(!forceCoalitionAppropriateUnits); + }} + /> + Force coalition appropriate units +
+
- { - setIADSDistribution(Number(ev.target.value)); - }} - >
-
- { - setForceCoalitionApproriateUnits(!forceCoalitionAppropriateUnits); - }} - /> - Force coalition appropriate units -
- -
+ )}
- )} -
-
+ + )} + ); } diff --git a/frontend/react/src/ui/panels/jtacmenu.tsx b/frontend/react/src/ui/panels/jtacmenu.tsx index 9dbfabf3..7329c7f9 100644 --- a/frontend/react/src/ui/panels/jtacmenu.tsx +++ b/frontend/react/src/ui/panels/jtacmenu.tsx @@ -10,6 +10,7 @@ import { FaMousePointer } from "react-icons/fa"; import { OlLocation } from "../components/ollocation"; import { FaBullseye } from "react-icons/fa6"; import { JTACSubState, OlympusState } from "../../constants/constants"; +import { AppStateChangedEvent } from "../../events"; export function JTACMenu(props: { open: boolean; onClose: () => void; children?: JSX.Element | JSX.Element[] }) { const [referenceSystem, setReferenceSystem] = useState("LatLngDec"); @@ -23,24 +24,8 @@ export function JTACMenu(props: { open: boolean; onClose: () => void; children?: const [type, setType] = useState("Type 1"); useEffect(() => { - document.addEventListener("selectJTACTarget", (ev: CustomEventInit) => { - setTargetLocation(null); - setTargetUnit(null); - - if (ev.detail.location) setTargetLocation(ev.detail.location); - if (ev.detail.unit) setTargetUnit(ev.detail.unit); - }); - - document.addEventListener("selectJTACECHO", (ev: CustomEventInit) => { - setECHO(ev.detail); - }); - - document.addEventListener("selectJTACIP", (ev: CustomEventInit) => { - setIP(ev.detail); - }); - - document.addEventListener("appStateChanged", (ev: CustomEventInit) => { - if (ev.detail.subState === JTACSubState.SELECT_TARGET) { + AppStateChangedEvent.on((state, subState) => { + if (subState === JTACSubState.SELECT_TARGET) { setTargetLocation(null); setTargetUnit(null); } diff --git a/frontend/react/src/ui/panels/minimappanel.tsx b/frontend/react/src/ui/panels/minimappanel.tsx index b13db808..b9ae7e26 100644 --- a/frontend/react/src/ui/panels/minimappanel.tsx +++ b/frontend/react/src/ui/panels/minimappanel.tsx @@ -1,34 +1,14 @@ -import React, { useState, useEffect } from "react"; +import React, { useState, useEffect, useContext } from "react"; import { zeroAppend } from "../../other/utils"; import { DateAndTime } from "../../interfaces"; import { getApp } from "../../olympusapp"; import { FaChevronDown, FaChevronUp } from "react-icons/fa6"; +import { StateContext } from "../../statecontext"; export function MiniMapPanel(props: {}) { - const [frameRate, setFrameRate] = useState(0); - const [load, setLoad] = useState(0); - const [elapsedTime, setElapsedTime] = useState(0); - const [missionTime, setMissionTime] = useState({ - h: 0, - m: 0, - s: 0, - } as DateAndTime["time"]); - const [connected, setConnected] = useState(false); - const [paused, setPaused] = useState(false); - const [showMissionTime, setShowMissionTime] = useState(false); - const [showMinimap, setShowMinimap] = useState(false); + const appState = useContext(StateContext) - useEffect(() => { - document.addEventListener("serverStatusUpdated", (ev) => { - const detail = (ev as CustomEvent).detail; - setFrameRate(detail.frameRate); - setLoad(detail.load); - setElapsedTime(detail.elapsedTime); - setMissionTime(detail.missionTime); - setConnected(detail.connected); - setPaused(detail.paused); - }); - }, []); + const [showMissionTime, setShowMissionTime] = useState(false); // A bit of a hack to set the rounded borders to the minimap useEffect(() => { @@ -38,55 +18,51 @@ export function MiniMapPanel(props: {}) { } }); - document.addEventListener("mapOptionsChanged", (event) => { - setShowMinimap(getApp().getMap().getOptions().showMinimap); - }); - // Compute the time string depending on mission or elapsed time let hours = 0; let minutes = 0; let seconds = 0; if (showMissionTime) { - hours = missionTime.h; - minutes = missionTime.m; - seconds = missionTime.s; + hours = appState.serverStatus.missionTime.h; + minutes = appState.serverStatus.missionTime.m; + seconds = appState.serverStatus.missionTime.s; } else { - hours = Math.floor(elapsedTime / 3600); - minutes = Math.floor(elapsedTime / 60) % 60; - seconds = Math.round(elapsedTime) % 60; + hours = Math.floor(appState.serverStatus.elapsedTime / 3600); + minutes = Math.floor(appState.serverStatus.elapsedTime / 60) % 60; + seconds = Math.round(appState.serverStatus.elapsedTime) % 60; } let timeString = `${zeroAppend(hours, 2)}:${zeroAppend(minutes, 2)}:${zeroAppend(seconds, 2)}`; // Choose frame rate string color let frameRateColor = "#8BFF63"; - if (frameRate < 30) frameRateColor = "#F05252"; - else if (frameRate >= 30 && frameRate < 60) frameRateColor = "#FF9900"; + if (appState.serverStatus.frameRate < 30) frameRateColor = "#F05252"; + else if (appState.serverStatus.frameRate >= 30 && appState.serverStatus.frameRate < 60) frameRateColor = "#FF9900"; // Choose load string color let loadColor = "#8BFF63"; - if (load > 1000) loadColor = "#F05252"; - else if (load >= 100 && load < 1000) loadColor = "#FF9900"; + if (appState.serverStatus.load > 1000) loadColor = "#F05252"; + else if (appState.serverStatus.load >= 100 && appState.serverStatus.load < 1000) loadColor = "#FF9900"; return (
setShowMissionTime(!showMissionTime)} className={` absolute right-[10px] - ${showMinimap ? `top-[232px]` : `top-[70px]`} + ${appState.mapOptions.showMinimap ? `top-[232px]` : `top-[70px]`} flex w-[288px] items-center justify-between - ${showMinimap ? `rounded-b-lg` : `rounded-lg`} + ${appState.mapOptions.showMinimap ? `rounded-b-lg` : `rounded-lg`} bg-gray-200 p-3 text-sm backdrop-blur-lg backdrop-grayscale dark:bg-olympus-800/90 dark:text-gray-200 `} > - {!connected ? ( + {!appState.serverStatus.connected ? (
Server disconnected
- ) : paused ? ( + ) : appState.serverStatus.paused ? (
Server paused @@ -96,13 +72,13 @@ export function MiniMapPanel(props: {}) {
FPS: - {frameRate} + {appState.serverStatus.frameRate}
Load: - {load} + {appState.serverStatus.load}
@@ -111,7 +87,7 @@ export function MiniMapPanel(props: {}) {
)} - {showMinimap ? ( + {appState.mapOptions.showMinimap ? ( { getApp().getMap().setOption("showMinimap", false); diff --git a/frontend/react/src/ui/panels/spawnmenu.tsx b/frontend/react/src/ui/panels/spawnmenu.tsx index f7f98ecb..96b70108 100644 --- a/frontend/react/src/ui/panels/spawnmenu.tsx +++ b/frontend/react/src/ui/panels/spawnmenu.tsx @@ -22,6 +22,7 @@ import { navyUnitDatabase } from "../../unit/databases/navyunitdatabase"; import { filterBlueprintsByLabel } from "../../other/utils"; import { helicopterDatabase } from "../../unit/databases/helicopterdatabase"; import { groundUnitDatabase } from "../../unit/databases/groundunitdatabase"; +import { AppStateChangedEvent } from "../../events"; enum Accordion { NONE, @@ -67,8 +68,8 @@ export function SpawnMenu(props: { open: boolean; onClose: () => void; children? }); useEffect(() => { - document.addEventListener("appStateChanged", (ev: CustomEventInit) => { - if (ev.detail.subState === NO_SUBSTATE) { + AppStateChangedEvent.on((state, subState) => { + if (subState === NO_SUBSTATE) { setBlueprint(null); setEffect(null); } diff --git a/frontend/react/src/ui/panels/unitcontrolmenu.tsx b/frontend/react/src/ui/panels/unitcontrolmenu.tsx index 31223439..0a549d08 100644 --- a/frontend/react/src/ui/panels/unitcontrolmenu.tsx +++ b/frontend/react/src/ui/panels/unitcontrolmenu.tsx @@ -1,4 +1,4 @@ -import React, { MutableRefObject, useEffect, useRef, useState } from "react"; +import React, { MutableRefObject, useContext, useEffect, useRef, useState } from "react"; import { Menu } from "./components/menu"; import { Unit } from "../../unit/unit"; import { OlLabelToggle } from "../components/ollabeltoggle"; @@ -50,9 +50,11 @@ import { Radio, TACAN } from "../../interfaces"; import { OlStringInput } from "../components/olstringinput"; import { OlFrequencyInput } from "../components/olfrequencyinput"; import { UnitSink } from "../../audio/unitsink"; +import { StateContext } from "../../statecontext"; export function UnitControlMenu(props: { open: boolean; onClose: () => void }) { - const [selectedUnits, setSelectedUnits] = useState([] as Unit[]); + const appState = useContext(StateContext); + const [selectedUnitsData, setSelectedUnitsData] = useState({ desiredAltitude: undefined as undefined | number, desiredAltitudeType: undefined as undefined | string, @@ -105,7 +107,6 @@ export function UnitControlMenu(props: { open: boolean; onClose: () => void }) { const [filterString, setFilterString] = useState(""); const [showAdvancedSettings, setShowAdvancedSettings] = useState(false); const [activeAdvancedSettings, setActiveAdvancedSettings] = useState(null as null | { radio: Radio; TACAN: TACAN }); - const [audioManagerEnabled, setAudioManagerEnabled] = useState(false); var searchBarRef = useRef(null); @@ -115,83 +116,26 @@ export function UnitControlMenu(props: { open: boolean; onClose: () => void }) { if (!props.open && filterString !== "") setFilterString(""); }); - useEffect(() => { - /* When a unit is selected, update the data */ - document.addEventListener("unitsSelection", (ev: CustomEventInit) => { - setSelectedUnits(ev.detail as Unit[]); - updateData(); - }); - - /* When a unit is deselected, refresh the view */ - document.addEventListener("unitDeselection", (ev: CustomEventInit) => { - window.setTimeout(() => updateData(), 200); - }); - - /* When all units are deselected clean the view */ - document.addEventListener("clearSelection", () => { - setSelectedUnits([]); - }); - - document.addEventListener("audioManagerStateChanged", () => { - setAudioManagerEnabled(getApp().getAudioManager().isRunning()); - }); - }, []); - useEffect(() => { setShowAdvancedSettings(false); - }, [selectedUnits]) - /* Update the current values of the shown data */ - function updateData() { const getters = { - desiredAltitude: (unit: Unit) => { - return Math.round(mToFt(unit.getDesiredAltitude())); - }, - desiredAltitudeType: (unit: Unit) => { - return unit.getDesiredAltitudeType(); - }, - desiredSpeed: (unit: Unit) => { - return Math.round(msToKnots(unit.getDesiredSpeed())); - }, - desiredSpeedType: (unit: Unit) => { - return unit.getDesiredSpeedType(); - }, - ROE: (unit: Unit) => { - return unit.getROE(); - }, - reactionToThreat: (unit: Unit) => { - return unit.getReactionToThreat(); - }, - emissionsCountermeasures: (unit: Unit) => { - return unit.getEmissionsCountermeasures(); - }, - scenicAAA: (unit: Unit) => { - return unit.getState() === "scenic-aaa"; - }, - missOnPurpose: (unit: Unit) => { - return unit.getState() === "miss-on-purpose"; - }, - shotsScatter: (unit: Unit) => { - return unit.getShotsScatter(); - }, - shotsIntensity: (unit: Unit) => { - return unit.getShotsIntensity(); - }, - operateAs: (unit: Unit) => { - return unit.getOperateAs(); - }, - followRoads: (unit: Unit) => { - return unit.getFollowRoads(); - }, - isActiveAWACS: (unit: Unit) => { - return unit.getIsActiveAWACS(); - }, - isActiveTanker: (unit: Unit) => { - return unit.getIsActiveTanker(); - }, - onOff: (unit: Unit) => { - return unit.getOnOff(); - }, + desiredAltitude: (unit: Unit) => Math.round(mToFt(unit.getDesiredAltitude())), + desiredAltitudeType: (unit: Unit) => unit.getDesiredAltitudeType(), + desiredSpeed: (unit: Unit) => Math.round(msToKnots(unit.getDesiredSpeed())), + desiredSpeedType: (unit: Unit) => unit.getDesiredSpeedType(), + ROE: (unit: Unit) => unit.getROE(), + reactionToThreat: (unit: Unit) => unit.getReactionToThreat(), + emissionsCountermeasures: (unit: Unit) => unit.getEmissionsCountermeasures(), + scenicAAA: (unit: Unit) => unit.getState() === "scenic-aaa", + missOnPurpose: (unit: Unit) => unit.getState() === "miss-on-purpose", + shotsScatter: (unit: Unit) => unit.getShotsScatter(), + shotsIntensity: (unit: Unit) => unit.getShotsIntensity(), + operateAs: (unit: Unit) => unit.getOperateAs(), + followRoads: (unit: Unit) => unit.getFollowRoads(), + isActiveAWACS: (unit: Unit) => unit.getIsActiveAWACS(), + isActiveTanker: (unit: Unit) => unit.getIsActiveTanker(), + onOff: (unit: Unit) => unit.getOnOff(), isAudioSink: (unit: Unit) => { return ( getApp() @@ -215,7 +159,7 @@ export function UnitControlMenu(props: { open: boolean; onClose: () => void }) { updatedData[key] = getApp()?.getUnitsManager()?.getSelectedUnitsVariable(getter); }); setSelectedUnitsData(updatedData); - } + }, [appState.selectedUnits]); /* Count how many units are selected of each type, divided by coalition */ var unitOccurences: { @@ -228,7 +172,7 @@ export function UnitControlMenu(props: { open: boolean; onClose: () => void }) { neutral: {}, }; - selectedUnits.forEach((unit) => { + appState.selectedUnits.forEach((unit) => { if (!(unit.getName() in unitOccurences[unit.getCoalition()])) unitOccurences[unit.getCoalition()][unit.getName()] = { occurences: 1, label: unit.getBlueprint()?.label }; else unitOccurences[unit.getCoalition()][unit.getName()].occurences++; @@ -236,7 +180,7 @@ export function UnitControlMenu(props: { open: boolean; onClose: () => void }) { const selectedCategories = getApp()?.getUnitsManager()?.getSelectedUnitsCategories() ?? []; - const [filteredAircraft, filteredHelicopters, filteredAirDefense, filteredGroundUnits, filteredNavyUnits] = [{}, {}, {}, {}, {}] // TODOgetUnitsByLabel(filterString); + const [filteredAircraft, filteredHelicopters, filteredAirDefense, filteredGroundUnits, filteredNavyUnits] = [{}, {}, {}, {}, {}]; // TODOgetUnitsByLabel(filterString); const mergedFilteredUnits = Object.assign({}, filteredAircraft, filteredHelicopters, filteredAirDefense, filteredGroundUnits, filteredNavyUnits) as { [key: string]: UnitBlueprint; @@ -276,13 +220,13 @@ export function UnitControlMenu(props: { open: boolean; onClose: () => void }) { return ( 0 ? `Units selected (x${selectedUnits.length})` : `No units selected`} + title={appState.selectedUnits.length > 0 ? `Units selected (x${appState.selectedUnits.length})` : `No units selected`} onClose={props.onClose} canBeHidden={true} > <> {/* ============== Selection tool START ============== */} - {selectedUnits.length == 0 && ( + {appState.selectedUnits.length == 0 && (
Selection tool
@@ -485,7 +429,7 @@ export function UnitControlMenu(props: { open: boolean; onClose: () => void }) { {/* */} <> {/* ============== Unit control menu START ============== */} - {selectedUnits.length > 0 && ( + {appState.selectedUnits.length > 0 && ( <> {/* ============== Units list START ============== */}
void }) { leftLabel={"AGL"} rightLabel={"ASL"} onClick={() => { - selectedUnits.forEach((unit) => { + appState.selectedUnits.forEach((unit) => { unit.setAltitudeType(selectedUnitsData.desiredAltitudeType === "ASL" ? "AGL" : "ASL"); setSelectedUnitsData({ ...selectedUnitsData, @@ -590,7 +534,7 @@ export function UnitControlMenu(props: { open: boolean; onClose: () => void }) {
{ - selectedUnits.forEach((unit) => { + appState.selectedUnits.forEach((unit) => { unit.setAltitude(ftToM(Number(ev.target.value))); setSelectedUnitsData({ ...selectedUnitsData, @@ -640,7 +584,7 @@ export function UnitControlMenu(props: { open: boolean; onClose: () => void }) { leftLabel={"GS"} rightLabel={"CAS"} onClick={() => { - selectedUnits.forEach((unit) => { + appState.selectedUnits.forEach((unit) => { unit.setSpeedType(selectedUnitsData.desiredSpeedType === "CAS" ? "GS" : "CAS"); setSelectedUnitsData({ ...selectedUnitsData, @@ -653,7 +597,7 @@ export function UnitControlMenu(props: { open: boolean; onClose: () => void }) {
{ - selectedUnits.forEach((unit) => { + appState.selectedUnits.forEach((unit) => { unit.setSpeed(knotsToMs(Number(ev.target.value))); setSelectedUnitsData({ ...selectedUnitsData, @@ -669,38 +613,39 @@ export function UnitControlMenu(props: { open: boolean; onClose: () => void }) {
{/* ============== Airspeed selector END ============== */} {/* ============== Rules of Engagement START ============== */} - {!(selectedUnits.length === 1 && selectedUnits[0].isTanker()) && !(selectedUnits.length === 1 && selectedUnits[0].isAWACS()) && ( -
- - Rules of engagement - - - {[olButtonsRoeHold, olButtonsRoeReturn, olButtonsRoeDesignated, olButtonsRoeFree].map((icon, idx) => { - return ( - { - selectedUnits.forEach((unit) => { - unit.setROE(ROEs[idx]); - setSelectedUnitsData({ - ...selectedUnitsData, - ROE: ROEs[idx], + {!(appState.selectedUnits.length === 1 && appState.selectedUnits[0].isTanker()) && + !(appState.selectedUnits.length === 1 && appState.selectedUnits[0].isAWACS()) && ( +
+ + Rules of engagement + + + {[olButtonsRoeHold, olButtonsRoeReturn, olButtonsRoeDesignated, olButtonsRoeFree].map((icon, idx) => { + return ( + { + appState.selectedUnits.forEach((unit) => { + unit.setROE(ROEs[idx]); + setSelectedUnitsData({ + ...selectedUnitsData, + ROE: ROEs[idx], + }); }); - }); - }} - active={selectedUnitsData.ROE === ROEs[idx]} - icon={icon} - /> - ); - })} - -
- )} + }} + active={selectedUnitsData.ROE === ROEs[idx]} + icon={icon} + /> + ); + })} +
+
+ )} {/* ============== Rules of Engagement END ============== */} {selectedCategories.every((category) => { @@ -723,7 +668,7 @@ export function UnitControlMenu(props: { open: boolean; onClose: () => void }) { { - selectedUnits.forEach((unit) => { + appState.selectedUnits.forEach((unit) => { unit.setReactionToThreat(reactionsToThreat[idx]); setSelectedUnitsData({ ...selectedUnitsData, @@ -755,7 +700,7 @@ export function UnitControlMenu(props: { open: boolean; onClose: () => void }) { { - selectedUnits.forEach((unit) => { + appState.selectedUnits.forEach((unit) => { unit.setEmissionsCountermeasures(emissionsCountermeasures[idx]); setSelectedUnitsData({ ...selectedUnitsData, @@ -791,7 +736,7 @@ export function UnitControlMenu(props: { open: boolean; onClose: () => void }) { { - selectedUnits.forEach((unit) => { + appState.selectedUnits.forEach((unit) => { unit.setAdvancedOptions( !selectedUnitsData.isActiveTanker, unit.getIsActiveAWACS(), @@ -825,7 +770,7 @@ export function UnitControlMenu(props: { open: boolean; onClose: () => void }) { { - selectedUnits.forEach((unit) => { + appState.selectedUnits.forEach((unit) => { unit.setAdvancedOptions( unit.getIsActiveTanker(), !selectedUnitsData.isActiveAWACS, @@ -844,7 +789,7 @@ export function UnitControlMenu(props: { open: boolean; onClose: () => void }) { )} {/* ============== Tanker and AWACS available button END ============== */} {/* ============== Advanced settings buttons START ============== */} - {selectedUnits.length === 1 && (selectedUnits[0].isTanker() || selectedUnits[0].isAWACS()) && ( + {appState.selectedUnits.length === 1 && (appState.selectedUnits[0].isTanker() || appState.selectedUnits[0].isAWACS()) && (
)} @@ -889,7 +834,7 @@ export function UnitControlMenu(props: { open: boolean; onClose: () => void }) { { - selectedUnits.forEach((unit) => { + appState.selectedUnits.forEach((unit) => { selectedUnitsData.scenicAAA ? unit.changeSpeed("stop") : unit.scenicAAA(); setSelectedUnitsData({ ...selectedUnitsData, @@ -914,7 +859,7 @@ export function UnitControlMenu(props: { open: boolean; onClose: () => void }) { { - selectedUnits.forEach((unit) => { + appState.selectedUnits.forEach((unit) => { selectedUnitsData.missOnPurpose ? unit.changeSpeed("stop") : unit.missOnPurpose(); setSelectedUnitsData({ ...selectedUnitsData, @@ -943,7 +888,7 @@ export function UnitControlMenu(props: { open: boolean; onClose: () => void }) { { - selectedUnits.forEach((unit) => { + appState.selectedUnits.forEach((unit) => { unit.setShotsScatter(idx + 1); setSelectedUnitsData({ ...selectedUnitsData, @@ -975,7 +920,7 @@ export function UnitControlMenu(props: { open: boolean; onClose: () => void }) { { - selectedUnits.forEach((unit) => { + appState.selectedUnits.forEach((unit) => { unit.setShotsIntensity(idx + 1); setSelectedUnitsData({ ...selectedUnitsData, @@ -1005,7 +950,7 @@ export function UnitControlMenu(props: { open: boolean; onClose: () => void }) { { - selectedUnits.forEach((unit) => { + appState.selectedUnits.forEach((unit) => { unit.setOperateAs(selectedUnitsData.operateAs === "blue" ? "red" : "blue"); setSelectedUnitsData({ ...selectedUnitsData, @@ -1030,7 +975,7 @@ export function UnitControlMenu(props: { open: boolean; onClose: () => void }) { { - selectedUnits.forEach((unit) => { + appState.selectedUnits.forEach((unit) => { unit.setFollowRoads(!selectedUnitsData.followRoads); setSelectedUnitsData({ ...selectedUnitsData, @@ -1054,7 +999,7 @@ export function UnitControlMenu(props: { open: boolean; onClose: () => void }) { { - selectedUnits.forEach((unit) => { + appState.selectedUnits.forEach((unit) => { unit.setOnOff(!selectedUnitsData.onOff); setSelectedUnitsData({ ...selectedUnitsData, @@ -1077,11 +1022,11 @@ export function UnitControlMenu(props: { open: boolean; onClose: () => void }) { > Loudspeakers - {audioManagerEnabled ? ( + {appState.audioManagerState ? ( { - selectedUnits.forEach((unit) => { + appState.selectedUnits.forEach((unit) => { if (!selectedUnitsData.isAudioSink) { getApp()?.getAudioManager().addUnitSink(unit); setSelectedUnitsData({ @@ -1115,7 +1060,8 @@ export function UnitControlMenu(props: { open: boolean; onClose: () => void }) { `} > - {" "}first + {" "} + first
)}
@@ -1131,14 +1077,14 @@ export function UnitControlMenu(props: { open: boolean; onClose: () => void }) {
<> - {selectedUnits[0].isAWACS() && ( + {appState.selectedUnits[0].isAWACS() && ( <> {["Overlord", "Magic", "Wizard", "Focus", "Darkstar"].map((name, idx) => { return ( @@ -1157,7 +1103,7 @@ export function UnitControlMenu(props: { open: boolean; onClose: () => void }) { )} <> - {selectedUnits[0].isTanker() && ( + {appState.selectedUnits[0].isTanker() && ( <> {["Texaco", "Arco", "Shell"].map((name, idx) => { return ( @@ -1220,9 +1166,10 @@ export function UnitControlMenu(props: { open: boolean; onClose: () => void }) { value={activeAdvancedSettings ? activeAdvancedSettings.TACAN.channel : 1} > - + { @@ -1291,12 +1238,12 @@ export function UnitControlMenu(props: { open: boolean; onClose: () => void }) { `} onClick={() => { if (activeAdvancedSettings) - selectedUnits[0].setAdvancedOptions( - selectedUnits[0].getIsActiveTanker(), - selectedUnits[0].getIsActiveAWACS(), + appState.selectedUnits[0].setAdvancedOptions( + appState.selectedUnits[0].getIsActiveTanker(), + appState.selectedUnits[0].getIsActiveAWACS(), activeAdvancedSettings.TACAN, activeAdvancedSettings.radio, - selectedUnits[0].getGeneralSettings() + appState.selectedUnits[0].getGeneralSettings() ); setActiveAdvancedSettings(null); setShowAdvancedSettings(false); @@ -1329,7 +1276,7 @@ export function UnitControlMenu(props: { open: boolean; onClose: () => void }) { {/* ============== Unit basic options END ============== */} <> {/* ============== Fuel/payload/radio section START ============== */} - {selectedUnits.length === 1 && ( + {appState.selectedUnits.length === 1 && (
void }) {
40 && `bg-green-700`} - ${selectedUnits[0].getFuel() > 10 && selectedUnits[0].getFuel() <= 40 && ` - bg-yellow-700 + ${appState.selectedUnits[0].getFuel() > 40 && ` + bg-green-700 + `} + ${ + appState.selectedUnits[0].getFuel() > 10 && + appState.selectedUnits[0].getFuel() <= 40 && + `bg-yellow-700` + } + ${appState.selectedUnits[0].getFuel() <= 10 && ` + bg-red-700 `} - ${selectedUnits[0].getFuel() <= 10 && `bg-red-700`} px-2 py-1 text-sm font-bold text-white `} > - {selectedUnits[0].getFuel()}% + {appState.selectedUnits[0].getFuel()}%
- {selectedUnits[0].isControlledByOlympus() && (selectedUnits[0].isTanker() || selectedUnits[0].isAWACS()) && ( + {appState.selectedUnits[0].isControlledByOlympus() && (appState.selectedUnits[0].isTanker() || appState.selectedUnits[0].isAWACS()) && ( <> {/* ============== Radio section START ============== */}
@@ -1376,7 +1329,7 @@ export function UnitControlMenu(props: { open: boolean; onClose: () => void }) { dark:text-gray-300 `} > - {`${selectedUnits[0].isTanker() ? ["Texaco", "Arco", "Shell"][selectedUnits[0].getRadio().callsign - 1] : ["Overlord", "Magic", "Wizard", "Focus", "Darkstar"][selectedUnits[0].getRadio().callsign - 1]}-${selectedUnits[0].getRadio().callsignNumber}`} + {`${appState.selectedUnits[0].isTanker() ? ["Texaco", "Arco", "Shell"][appState.selectedUnits[0].getRadio().callsign - 1] : ["Overlord", "Magic", "Wizard", "Focus", "Darkstar"][appState.selectedUnits[0].getRadio().callsign - 1]}-${appState.selectedUnits[0].getRadio().callsignNumber}`}
@@ -1399,7 +1352,7 @@ export function UnitControlMenu(props: { open: boolean; onClose: () => void }) { dark:text-gray-300 `} > - {`${(selectedUnits[0].getRadio().frequency / 1000000).toFixed(3)} MHz`} + {`${(appState.selectedUnits[0].getRadio().frequency / 1000000).toFixed(3)} MHz`}
@@ -1423,8 +1376,8 @@ export function UnitControlMenu(props: { open: boolean; onClose: () => void }) { dark:text-gray-300 `} > - {selectedUnits[0].getTACAN().isOn - ? `${selectedUnits[0].getTACAN().channel}${selectedUnits[0].getTACAN().XY} ${selectedUnits[0].getTACAN().callsign}` + {appState.selectedUnits[0].getTACAN().isOn + ? `${appState.selectedUnits[0].getTACAN().channel}${appState.selectedUnits[0].getTACAN().XY} ${appState.selectedUnits[0].getTACAN().callsign}` : "TACAN OFF"} @@ -1433,9 +1386,9 @@ export function UnitControlMenu(props: { open: boolean; onClose: () => void }) { )} {/* ============== Payload section START ============== */} - {!selectedUnits[0].isTanker() && - !selectedUnits[0].isAWACS() && - selectedUnits[0].getAmmo().map((ammo, idx) => { + {!appState.selectedUnits[0].isTanker() && + !appState.selectedUnits[0].isAWACS() && + appState.selectedUnits[0].getAmmo().map((ammo, idx) => { return (
{ - /* When a unit is selected, open the menu */ - document.addEventListener("unitsSelection", (ev: CustomEventInit) => { - setOpen(true); - updateData(); - setActiveContextAction(null); - }); - - /* When a unit is deselected, refresh the view */ - document.addEventListener("unitDeselection", (ev: CustomEventInit) => { - window.setTimeout(() => updateData(), 200); - }); - - /* When all units are deselected clean the view */ - document.addEventListener("clearSelection", () => { - setOpen(false); - updateData(); - }); - - /* Deselect the context action when exiting state */ - document.addEventListener("appStateChanged", (ev) => { - setOpen((ev as CustomEvent).detail.state === OlympusState.UNIT_CONTROL); + AppStateChangedEvent.on((state, subState) => { + setOpen(state === OlympusState.UNIT_CONTROL); }); }, []); - /* Update the current values of the shown data */ - function updateData() { - var newContextActionSet = new ContextActionSet(); - - getApp() - .getUnitsManager() - .getSelectedUnits() - .forEach((unit: Unit) => { - unit.appendContextActions(newContextActionSet); - }); - - setContextActionsSet(newContextActionSet); - return newContextActionSet; - } - function onScroll(el) { const sl = el.scrollLeft; const sr = el.scrollWidth - el.scrollLeft - el.clientWidth; @@ -75,15 +43,17 @@ export function UnitMouseControlBar(props: {}) { let reorderedActions: ContextAction[] = []; CONTEXT_ACTION_COLORS.forEach((color) => { - Object.values(contextActionsSet.getContextActions()).forEach((contextAction: ContextAction) => { - if (color === null && contextAction.getOptions().buttonColor === undefined) reorderedActions.push(contextAction); - else if (color === contextAction.getOptions().buttonColor) reorderedActions.push(contextAction); - }); + if (appState.contextActionSet) { + Object.values(appState.contextActionSet.getContextActions()).forEach((contextAction: ContextAction) => { + if (color === null && contextAction.getOptions().buttonColor === undefined) reorderedActions.push(contextAction); + else if (color === contextAction.getOptions().buttonColor) reorderedActions.push(contextAction); + }); + } }); return ( <> - {open && Object.keys(contextActionsSet.getContextActions()).length > 0 && ( + {open && appState.contextActionSet && Object.keys(appState.contextActionSet.getContextActions()).length > 0 && ( <>
{ if (contextAction.getOptions().executeImmediately) { - setActiveContextAction(null); contextAction.executeCallback(null, null); } else { - if (activeContextAction !== contextAction) { - setActiveContextAction(contextAction); - getApp().getMap().setContextAction(contextAction); - getApp().getMap().setDefaultContextAction(contextActionsSet.getDefaultContextAction()); - } else { - setActiveContextAction(null); - getApp().getMap().setContextAction(null); - getApp().getMap().setDefaultContextAction(null); - } + appState.contextAction !== contextAction ? getApp().getMap().setContextAction(contextAction) : getApp().getMap().setContextAction(null); } }} /> @@ -147,7 +108,7 @@ export function UnitMouseControlBar(props: {}) { /> )}
- {activeContextAction && ( + {appState.contextAction && (
- {activeContextAction.getDescription()} + {appState.contextAction.getDescription()}
)} diff --git a/frontend/react/src/ui/ui.tsx b/frontend/react/src/ui/ui.tsx index d2e798ff..501a53ce 100644 --- a/frontend/react/src/ui/ui.tsx +++ b/frontend/react/src/ui/ui.tsx @@ -10,7 +10,18 @@ import { MainMenu } from "./panels/mainmenu"; import { SideBar } from "./panels/sidebar"; import { OptionsMenu } from "./panels/optionsmenu"; import { MapHiddenTypes, MapOptions } from "../types/types"; -import { BLUE_COMMANDER, GAME_MASTER, MAP_HIDDEN_TYPES_DEFAULTS, MAP_OPTIONS_DEFAULTS, NO_SUBSTATE, OlympusEvent, OlympusState, OlympusSubState, RED_COMMANDER, UnitControlSubState } from "../constants/constants"; +import { + BLUE_COMMANDER, + GAME_MASTER, + MAP_HIDDEN_TYPES_DEFAULTS, + MAP_OPTIONS_DEFAULTS, + NO_SUBSTATE, + OlympusEvent, + OlympusState, + OlympusSubState, + RED_COMMANDER, + UnitControlSubState, +} from "../constants/constants"; import { getApp, setupApp } from "../olympusapp"; import { LoginModal } from "./modals/login"; import { sha256 } from "js-sha256"; @@ -27,6 +38,26 @@ import { Unit } from "../unit/unit"; import { ProtectionPrompt } from "./modals/protectionprompt"; import { UnitExplosionMenu } from "./panels/unitexplosionmenu"; import { JTACMenu } from "./panels/jtacmenu"; +import { + AppStateChangedEvent, + AudioManagerStateChangedEvent, + AudioSinksChangedEvent, + AudioSourcesChangedEvent, + ConfigLoadedEvent, + ContextActionChangedEvent, + ContextActionSetChangedEvent, + HiddenTypesChangedEvent, + MapOptionsChangedEvent, + MapSourceChangedEvent, + SelectedUnitsChangedEvent, + ServerStatusUpdatedEvent, + UnitSelectedEvent, +} from "../events"; +import { ServerStatus } from "../interfaces"; +import { AudioSource } from "../audio/audiosource"; +import { AudioSink } from "../audio/audiosink"; +import { ContextAction } from "../unit/contextaction"; +import { ContextActionSet } from "../unit/contextactionset"; export type OlympusUIState = { mainMenuVisible: boolean; @@ -43,16 +74,22 @@ export type OlympusUIState = { export function UI() { const [appState, setAppState] = useState(OlympusState.NOT_INITIALIZED); const [appSubState, setAppSubState] = useState(NO_SUBSTATE as OlympusSubState); - const [mapHiddenTypes, setMapHiddenTypes] = useState(MAP_HIDDEN_TYPES_DEFAULTS); const [mapOptions, setMapOptions] = useState(MAP_OPTIONS_DEFAULTS); const [mapSources, setMapSources] = useState([] as string[]); const [activeMapSource, setActiveMapSource] = useState(""); + const [selectedUnits, setSelectedUnits] = useState([] as Unit[]); + const [audioSources, setAudioSources] = useState([] as AudioSource[]); + const [audioSinks, setAudioSinks] = useState([] as AudioSink[]); + const [audioManagerState, setAudioManagerState] = useState(false); + const [serverStatus, setServerStatus] = useState({} as ServerStatus); + const [contextActionSet, setContextActionsSet] = useState(null as ContextActionSet | null); + const [contextAction, setContextActions] = useState(null as ContextAction | null); const [checkingPassword, setCheckingPassword] = useState(false); const [loginError, setLoginError] = useState(false); const [commandMode, setCommandMode] = useState(null as null | string); - + const [airbase, setAirbase] = useState(null as null | Airbase); const [formationLeader, setFormationLeader] = useState(null as null | Unit); @@ -64,31 +101,27 @@ export function UI() { const [unitExplosionUnits, setUnitExplosionUnits] = useState([] as Unit[]); useEffect(() => { - getApp()?.registerEventCallback(OlympusEvent.STATE_CHANGED, (state, subState) => { + AppStateChangedEvent.on((state, subState) => { setAppState(state); setAppSubState(subState); - }) - - document.addEventListener("hiddenTypesChanged", (ev) => { - setMapHiddenTypes({ ...getApp().getMap().getHiddenTypes() }); }); - - document.addEventListener("mapOptionsChanged", (ev) => { - setMapOptions({ ...getApp().getMap().getOptions() }); - }); - - document.addEventListener("mapSourceChanged", (ev) => { - var source = (ev as CustomEvent).detail; - setActiveMapSource(source); - }); - - document.addEventListener("configLoaded", (ev) => { + ConfigLoadedEvent.on(() => { let config = getApp().getConfig(); var sources = Object.keys(config.mapMirrors).concat(Object.keys(config.mapLayers)); setMapSources(sources); setActiveMapSource(sources[0]); }); - + HiddenTypesChangedEvent.on((hiddenTypes) => setMapHiddenTypes({ ...hiddenTypes })); + MapOptionsChangedEvent.on((mapOptions) => setMapOptions({ ...mapOptions })); + MapSourceChangedEvent.on((source) => setActiveMapSource(source)); + SelectedUnitsChangedEvent.on((units) => setSelectedUnits(units)); + AudioSourcesChangedEvent.on((sources) => setAudioSources(sources)); + AudioSinksChangedEvent.on((sinks) => setAudioSinks(sinks)); + AudioManagerStateChangedEvent.on((state) => setAudioManagerState(state)); + ServerStatusUpdatedEvent.on((status) => setServerStatus(status)); + ContextActionSetChangedEvent.on((contextActionSet) => setContextActionsSet(contextActionSet)); + ContextActionChangedEvent.on((contextAction) => setContextActions(contextAction)); + document.addEventListener("showProtectionPrompt", (ev: CustomEventInit) => { setProtectionPromptVisible(true); setProtectionCallback(() => { @@ -143,74 +176,88 @@ export function UI() { mapHiddenTypes, mapSources, activeMapSource, + selectedUnits, + audioSources, + audioSinks, + audioManagerState, + serverStatus, + contextActionSet, + contextAction, }} > - -
-
- {appState === OlympusState.LOGIN && ( - <> -
- { - checkPassword(password); - }} - onContinue={(username) => { - connect(username); - }} - onBack={() => { - setCommandMode(null); - }} - checkingPassword={checkingPassword} - loginError={loginError} - commandMode={commandMode} - /> - - )} - {protectionPromptVisible && ( - <> -
- { - protectionCallback(units); - setProtectionPromptVisible(false); - }} - onBack={() => { - setProtectionPromptVisible(false); - }} - units={protectionUnits} - /> - - )} -
- getApp().setState(OlympusState.IDLE)} /> - getApp().setState(OlympusState.IDLE)} /> - getApp().setState(OlympusState.IDLE)} options={mapOptions} /> +
+
+ {appState === OlympusState.LOGIN && ( + <> +
+ { + checkPassword(password); + }} + onContinue={(username) => { + connect(username); + }} + onBack={() => { + setCommandMode(null); + }} + checkingPassword={checkingPassword} + loginError={loginError} + commandMode={commandMode} + /> + + )} + {protectionPromptVisible && ( + <> +
+ { + protectionCallback(units); + setProtectionPromptVisible(false); + }} + onBack={() => { + setProtectionPromptVisible(false); + }} + units={protectionUnits} + /> + + )} +
+ getApp().setState(OlympusState.IDLE)} /> + getApp().setState(OlympusState.IDLE)} /> + getApp().setState(OlympusState.IDLE)} options={mapOptions} /> - getApp().setState(OlympusState.IDLE)} /> - getApp().setState(OlympusState.IDLE)} /> - - getApp().setState(OlympusState.IDLE)} /> - getApp().setState(OlympusState.IDLE)} airbase={airbase} /> - getApp().setState(OlympusState.IDLE)} /> - - {/* TODO} getApp().setState(OlympusState.IDLE)} /> {*/} - getApp().setState(OlympusState.IDLE)} /> + getApp().setState(OlympusState.IDLE)} + /> + getApp().setState(OlympusState.IDLE)} + /> - - - - - -
+ getApp().setState(OlympusState.IDLE)} /> + getApp().setState(OlympusState.IDLE)} airbase={airbase} /> + getApp().setState(OlympusState.IDLE)} /> + + {/* TODO} getApp().setState(OlympusState.IDLE)} /> {*/} + getApp().setState(OlympusState.IDLE)} /> + + + + + + +
); diff --git a/frontend/react/src/unit/group.ts b/frontend/react/src/unit/group.ts index 73480683..6aa694fd 100644 --- a/frontend/react/src/unit/group.ts +++ b/frontend/react/src/unit/group.ts @@ -1,3 +1,4 @@ +import { UnitDeadEvent } from "../events"; import { Unit } from "./unit"; export class Group { @@ -7,8 +8,8 @@ export class Group { constructor(name: string) { this.#name = name; - document.addEventListener("unitDeath", (e: any) => { - if (this.#members.includes(e.detail)) this.getLeader()?.onGroupChanged(e.detail); + UnitDeadEvent.on((unit) => { + if (this.#members.includes(unit)) this.getLeader()?.onGroupChanged(unit); }); } diff --git a/frontend/react/src/unit/unit.ts b/frontend/react/src/unit/unit.ts index 447abff9..99a229c8 100644 --- a/frontend/react/src/unit/unit.ts +++ b/frontend/react/src/unit/unit.ts @@ -71,6 +71,7 @@ import { faXmarksLines, } from "@fortawesome/free-solid-svg-icons"; import { Carrier } from "../mission/carrier"; +import { ContactsUpdatedEvent, HiddenTypesChangedEvent, MapOptionsChangedEvent, UnitDeadEvent, UnitDeselectedEvent, UnitSelectedEvent } from "../events"; var pathIcon = new Icon({ iconUrl: "/vite/images/markers/marker-icon.png", @@ -356,29 +357,20 @@ 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()) { + if (this.belongsToCommandedCoalition()) this.setHighlighted(true); - document.dispatchEvent(new CustomEvent("unitMouseover", { detail: this })); - } }); - this.on("mouseout", () => { - this.setHighlighted(false); - document.dispatchEvent(new CustomEvent("unitMouseout", { detail: this })); - }); - getApp() - .getMap() - .on("zoomend", (e: any) => { - this.#onZoom(e); - }); + this.on("mouseout", () => this.setHighlighted(false)); + getApp().getMap().on("zoomend", (e: any) => this.#onZoom(e)); /* Deselect units if they are hidden */ - document.addEventListener("hiddenTypesChanged", (ev: CustomEventInit) => { + HiddenTypesChangedEvent.on((hiddenTypes) => { this.#updateMarker(); this.setSelected(this.getSelected() && !this.getHidden()); }); /* Update the marker when the options change */ - document.addEventListener("mapOptionChanged", (ev: CustomEventInit) => { + MapOptionsChangedEvent.on(() => { this.#updateMarker(); /* Circles don't like to be updated when the map is zooming */ @@ -559,7 +551,7 @@ export abstract class Unit extends CustomMarker { break; case DataIndexes.contacts: this.#contacts = dataExtractor.extractContacts(); - document.dispatchEvent(new CustomEvent("contactsUpdated", { detail: this })); + ContactsUpdatedEvent.dispatch(); break; case DataIndexes.activePath: this.#activePath = dataExtractor.extractActivePath(); @@ -599,9 +591,6 @@ export abstract class Unit extends CustomMarker { this.setSelected(true); } } - - /* If the unit is selected or if the view is centered on this unit, sent the update signal so that other elements like the UnitControlPanel can be updated. */ - if (this.getSelected() || getApp().getMap().getCenteredOnUnit() === this) document.dispatchEvent(new CustomEvent("unitUpdated", { detail: this })); } /** Get unit data collated into an object @@ -672,7 +661,7 @@ export abstract class Unit extends CustomMarker { * @param newAlive (boolean) true = alive, false = dead */ setAlive(newAlive: boolean) { - if (newAlive != this.#alive) document.dispatchEvent(new CustomEvent("unitDeath", { detail: this })); + if (newAlive != this.#alive) UnitDeadEvent.dispatch(this) this.#alive = newAlive; } @@ -709,11 +698,7 @@ export abstract class Unit extends CustomMarker { this.getElement()?.querySelector(`.unit`)?.toggleAttribute("data-is-selected", selected); /* Trigger events after all (de-)selecting has been done */ - if (selected) { - document.dispatchEvent(new CustomEvent("unitSelection", { detail: this })); - } else { - document.dispatchEvent(new CustomEvent("unitDeselection", { detail: this })); - } + selected? UnitSelectedEvent.dispatch(this): UnitDeselectedEvent.dispatch(this); } } diff --git a/frontend/react/src/unit/unitsmanager.ts b/frontend/react/src/unit/unitsmanager.ts index 95c300fd..36946ffa 100644 --- a/frontend/react/src/unit/unitsmanager.ts +++ b/frontend/react/src/unit/unitsmanager.ts @@ -35,6 +35,14 @@ import { UnitDataFileExport } from "./importexport/unitdatafileexport"; import { UnitDataFileImport } from "./importexport/unitdatafileimport"; import { CoalitionCircle } from "../map/coalitionarea/coalitioncircle"; import { ContextActionSet } from "./contextactionset"; +import { + CommandModeOptionsChangedEvent, + ContactsUpdatedEvent, + SelectedUnitsChangedEvent, + SelectionClearedEvent, + UnitDeselectedEvent, + UnitSelectedEvent, +} from "../events"; /** The UnitsManager handles the creation, update, and control of units. Data is strictly updated by the server ONLY. This means that any interaction from the user will always and only * result in a command to the server, executed by means of a REST PUT request. Any subsequent change in data will be reflected only when the new data is sent back by the server. This strategy allows @@ -55,17 +63,18 @@ export class UnitsManager { this.#copiedUnits = []; this.#units = {}; - document.addEventListener("commandModeOptionsChanged", () => { + CommandModeOptionsChangedEvent.on(() => { Object.values(this.#units).forEach((unit: Unit) => unit.updateVisibility()); }); - document.addEventListener("contactsUpdated", (e) => { + ContactsUpdatedEvent.on(() => { this.#requestDetectionUpdate = true; }); + UnitSelectedEvent.on((unit) => this.#onUnitDeselection(unit)); + UnitDeselectedEvent.on((unit) => this.#onUnitSelection(unit)); + document.addEventListener("copy", () => this.copy()); document.addEventListener("keyup", (event) => this.#onKeyUp(event)); document.addEventListener("paste", () => this.paste()); - document.addEventListener("unitDeselection", (e) => this.#onUnitDeselection((e as CustomEvent).detail)); - document.addEventListener("unitSelection", (e) => this.#onUnitSelection((e as CustomEvent).detail)); //this.#slowDeleteDialog = new Dialog("slow-delete-dialog"); } @@ -1416,18 +1425,6 @@ export class UnitsManager { if (spawnPoints <= getApp().getMissionManager().getAvailableSpawnPoints()) { getApp().getMissionManager().setSpentSpawnPoints(spawnPoints); spawnFunction(); - document.dispatchEvent( - new CustomEvent("unitSpawned", { - detail: { - airbase: airbase, - category: category, - coalition: coalition, - country: country, - immediate: immediate, - unitSpawnTable: units, - }, - }) - ); return true; } else { //(getApp().getPopupsManager().get("infoPopup") as Popup).setText("Not enough spawn points available!"); @@ -1453,16 +1450,13 @@ export class UnitsManager { /* Disable the firing of the selection event for a certain amount of time. This avoids firing many events if many units are selected */ if (!this.#selectionEventDisabled) { window.setTimeout(() => { - document.dispatchEvent( - new CustomEvent("unitsSelection", { - detail: this.getSelectedUnits(), - }) - ); + SelectedUnitsChangedEvent.dispatch(this.getSelectedUnits()); let newContextActionSet = new ContextActionSet(); this.getSelectedUnits().forEach((unit) => unit.appendContextActions(newContextActionSet)); + getApp().getMap().setContextAction(null); - getApp().getMap().setDefaultContextAction(newContextActionSet.getDefaultContextAction()); + getApp().getMap().setContextActionSet(newContextActionSet); getApp().setState(OlympusState.UNIT_CONTROL); this.#selectionEventDisabled = false; @@ -1472,24 +1466,19 @@ export class UnitsManager { } } else { getApp().setState(OlympusState.IDLE); - document.dispatchEvent(new CustomEvent("clearSelection")); + SelectionClearedEvent.dispatch(); } } #onUnitDeselection(unit: Unit) { if (this.getSelectedUnits().length == 0) { - if (getApp().getState() === OlympusState.UNIT_CONTROL) - getApp().setState(OlympusState.IDLE); - document.dispatchEvent(new CustomEvent("clearSelection")); + if (getApp().getState() === OlympusState.UNIT_CONTROL) getApp().setState(OlympusState.IDLE); + SelectionClearedEvent.dispatch(); } else { /* Disable the firing of the selection event for a certain amount of time. This avoids firing many events if many units are selected */ if (!this.#deselectionEventDisabled) { window.setTimeout(() => { - document.dispatchEvent( - new CustomEvent("unitsDeselection", { - detail: this.getSelectedUnits(), - }) - ); + SelectedUnitsChangedEvent.dispatch(this.getSelectedUnits()); this.#deselectionEventDisabled = false; }, 100); this.#deselectionEventDisabled = true; diff --git a/frontend/react/src/weapon/weapon.ts b/frontend/react/src/weapon/weapon.ts index 6d87bc0d..9ffbb683 100644 --- a/frontend/react/src/weapon/weapon.ts +++ b/frontend/react/src/weapon/weapon.ts @@ -6,6 +6,7 @@ import { SVGInjector } from "@tanem/svg-injector"; import { DLINK, DataIndexes, GAME_MASTER, IRST, OPTIC, RADAR, VISUAL } from "../constants/constants"; import { DataExtractor } from "../server/dataextractor"; import { ObjectIconOptions } from "../interfaces"; +import { MapOptionsChangedEvent } from "../events"; export class Weapon extends CustomMarker { ID: number; @@ -50,9 +51,7 @@ export class Weapon extends CustomMarker { this.ID = ID; /* Update the marker when the options change */ - document.addEventListener("mapOptionsChanged", (ev: CustomEventInit) => { - this.#updateMarker(); - }); + MapOptionsChangedEvent.on(() => this.#updateMarker()); } getCategory() { diff --git a/frontend/react/src/weapon/weaponsmanager.ts b/frontend/react/src/weapon/weaponsmanager.ts index 7e6d21d4..3809c89e 100644 --- a/frontend/react/src/weapon/weaponsmanager.ts +++ b/frontend/react/src/weapon/weaponsmanager.ts @@ -3,6 +3,7 @@ import { Weapon } from "./weapon"; import { DataIndexes } from "../constants/constants"; import { DataExtractor } from "../server/dataextractor"; import { Contact } from "../interfaces"; +import { CommandModeOptionsChangedEvent } from "../events"; /** The WeaponsManager handles the creation and update of weapons. Data is strictly updated by the server ONLY. */ export class WeaponsManager { @@ -11,7 +12,7 @@ export class WeaponsManager { constructor() { this.#weapons = {}; - document.addEventListener("commandModeOptionsChanged", () => { + CommandModeOptionsChangedEvent.on(() => { Object.values(this.#weapons).forEach((weapon: Weapon) => weapon.updateVisibility()); }); }