diff --git a/backend/core/include/datatypes.h b/backend/core/include/datatypes.h index 0ce20be1..485d1e3c 100644 --- a/backend/core/include/datatypes.h +++ b/backend/core/include/datatypes.h @@ -185,6 +185,7 @@ struct SpawnOptions { string skill; string liveryID; double heading; + string payload; }; struct CloneOptions { diff --git a/backend/core/src/commands.cpp b/backend/core/src/commands.cpp index 75f1cf65..5ea705ac 100644 --- a/backend/core/src/commands.cpp +++ b/backend/core/src/commands.cpp @@ -102,6 +102,7 @@ string SpawnAircrafts::getString() << "alt = " << spawnOptions[i].location.alt << ", " << "heading = " << spawnOptions[i].heading << ", " << "loadout = \"" << spawnOptions[i].loadout << "\"" << ", " + << "payload = " << spawnOptions[i].payload << ", " << "liveryID = " << "\"" << spawnOptions[i].liveryID << "\"" << ", " << "skill = \"" << spawnOptions[i].skill << "\"" << "}, "; } @@ -132,6 +133,7 @@ string SpawnHelicopters::getString() << "alt = " << spawnOptions[i].location.alt << ", " << "heading = " << spawnOptions[i].heading << ", " << "loadout = \"" << spawnOptions[i].loadout << "\"" << ", " + << "payload = " << spawnOptions[i].payload << ", " << "liveryID = " << "\"" << spawnOptions[i].liveryID << "\"" << ", " << "skill = \"" << spawnOptions[i].skill << "\"" << "}, "; } diff --git a/backend/core/src/scheduler.cpp b/backend/core/src/scheduler.cpp index 67393614..015b4744 100644 --- a/backend/core/src/scheduler.cpp +++ b/backend/core/src/scheduler.cpp @@ -223,7 +223,11 @@ void Scheduler::handleRequest(string key, json::value value, string username, js string liveryID = to_string(unit[L"liveryID"]); string skill = to_string(unit[L"skill"]); - spawnOptions.push_back({ unitType, location, loadout, skill, liveryID, heading }); + string payload = "nil"; + if (unit.has_string_field(L"payload")) + payload = to_string(unit[L"payload"]); + + spawnOptions.push_back({ unitType, location, loadout, skill, liveryID, heading, payload }); log(username + " spawned a " + coalition + " " + unitType, true); } diff --git a/frontend/react/src/constants/constants.ts b/frontend/react/src/constants/constants.ts index 71f1896b..f1470eba 100644 --- a/frontend/react/src/constants/constants.ts +++ b/frontend/react/src/constants/constants.ts @@ -399,6 +399,7 @@ export enum SpawnSubState { NO_SUBSTATE = "No substate", SPAWN_UNIT = "Unit", SPAWN_EFFECT = "Effect", + LOADOUT_WIZARD = "Loadout wizard" } export enum OptionsSubstate { diff --git a/frontend/react/src/events.ts b/frontend/react/src/events.ts index 6dc49454..d785cce0 100644 --- a/frontend/react/src/events.ts +++ b/frontend/react/src/events.ts @@ -1,7 +1,7 @@ import { AudioSink } from "./audio/audiosink"; import { AudioSource } from "./audio/audiosource"; import { OlympusState, OlympusSubState } from "./constants/constants"; -import { CommandModeOptions, MissionData, OlympusConfig, ServerStatus, SessionData, SpawnRequestTable, UnitData } from "./interfaces"; +import { CommandModeOptions, LoadoutBlueprint, MissionData, OlympusConfig, ServerStatus, SessionData, SpawnRequestTable, UnitBlueprint, UnitData } from "./interfaces"; import { CoalitionCircle } from "./map/coalitionarea/coalitioncircle"; import { CoalitionPolygon } from "./map/coalitionarea/coalitionpolygon"; import { Airbase } from "./mission/airbase"; @@ -17,907 +17,943 @@ import { Weapon } from "./weapon/weapon"; const DEBUG = false; export class BaseOlympusEvent { - static on(callback: () => void, singleShot = false) { - document.addEventListener( - this.name, - (ev: CustomEventInit) => { - callback(); - }, - { once: singleShot } - ); - } + static on(callback: () => void, singleShot = false) { + document.addEventListener( + this.name, + (ev: CustomEventInit) => { + callback(); + }, + { once: singleShot } + ); + } - static dispatch() { - document.dispatchEvent(new CustomEvent(this.name)); - if (DEBUG) console.log(`Event ${this.name} dispatched`); - } + static dispatch() { + document.dispatchEvent(new CustomEvent(this.name)); + if (DEBUG) console.log(`Event ${this.name} dispatched`); + } } export class BaseUnitEvent { - static on(callback: (unit: Unit) => void, singleShot = false) { - document.addEventListener( - this.name, - (ev: CustomEventInit) => { - callback(ev.detail.unit); - }, - { once: singleShot } - ); - } + static on(callback: (unit: Unit) => void, singleShot = false) { + document.addEventListener( + this.name, + (ev: CustomEventInit) => { + callback(ev.detail.unit); + }, + { once: singleShot } + ); + } - static dispatch(unit: Unit) { - document.dispatchEvent(new CustomEvent(this.name, { detail: { unit } })); - if (DEBUG) console.log(`Event ${this.name} dispatched`); - if (DEBUG) console.log(unit); - } + static dispatch(unit: Unit) { + document.dispatchEvent(new CustomEvent(this.name, { detail: { unit } })); + if (DEBUG) console.log(`Event ${this.name} dispatched`); + if (DEBUG) console.log(unit); + } } export class BaseUnitsEvent { - static on(callback: (selectedUnits: Unit[]) => void, singleShot = false) { - document.addEventListener( - this.name, - (ev: CustomEventInit) => { - callback(ev.detail); - }, - { once: singleShot } - ); - } + static on(callback: (selectedUnits: Unit[]) => void, singleShot = false) { + document.addEventListener( + this.name, + (ev: CustomEventInit) => { + callback(ev.detail); + }, + { once: singleShot } + ); + } - static dispatch(units: Unit[]) { - document.dispatchEvent(new CustomEvent(this.name, { detail: units })); - if (DEBUG) console.log(`Event ${this.name} dispatched`); - } + static dispatch(units: Unit[]) { + document.dispatchEvent(new CustomEvent(this.name, { detail: units })); + if (DEBUG) console.log(`Event ${this.name} dispatched`); + } } /************** App events ***************/ export class AppStateChangedEvent { - static on(callback: (state: OlympusState, subState: OlympusSubState) => void, singleShot = false) { - document.addEventListener( - this.name, - (ev: CustomEventInit) => { - callback(ev.detail.state, ev.detail.subState); - }, - { once: singleShot } - ); - } + static on( + callback: (state: OlympusState, subState: OlympusSubState, previousState: OlympusState, previousSubState: OlympusSubState) => void, + singleShot = false + ) { + document.addEventListener( + this.name, + (ev: CustomEventInit) => { + callback(ev.detail.state, ev.detail.subState, ev.detail.previousState, ev.detail.previousSubState); + }, + { once: singleShot } + ); + } - static dispatch(state: OlympusState, subState: OlympusSubState) { - const detail = { state, subState }; - document.dispatchEvent(new CustomEvent(this.name, { detail })); - if (DEBUG) console.log(`Event ${this.name} dispatched`); - if (DEBUG) console.log(`State: ${state} Substate: ${subState}`); - } + static dispatch(state: OlympusState, subState: OlympusSubState, previousState: OlympusState, previousSubState: OlympusSubState) { + const detail = { state, subState, previousState, previousSubState }; + document.dispatchEvent(new CustomEvent(this.name, { detail })); + if (DEBUG) console.log(`Event ${this.name} dispatched`); + if (DEBUG) console.log(`State: ${state} Substate: ${subState}`); + } } export class ConfigLoadedEvent { - static on(callback: (config: OlympusConfig) => void, singleShot = false) { - document.addEventListener( - this.name, - (ev: CustomEventInit) => { - callback(ev.detail); - }, - { once: singleShot } - ); - } + static on(callback: (config: OlympusConfig) => void, singleShot = false) { + document.addEventListener( + this.name, + (ev: CustomEventInit) => { + callback(ev.detail); + }, + { once: singleShot } + ); + } - static dispatch(config: OlympusConfig) { - document.dispatchEvent(new CustomEvent(this.name, { detail: config })); - if (DEBUG) console.log(`Event ${this.name} dispatched`); - if (DEBUG) console.log(config); - } + static dispatch(config: OlympusConfig) { + document.dispatchEvent(new CustomEvent(this.name, { detail: config })); + if (DEBUG) console.log(`Event ${this.name} dispatched`); + if (DEBUG) console.log(config); + } } export class ServerStatusUpdatedEvent { - static on(callback: (serverStatus: ServerStatus) => void, singleShot = false) { - document.addEventListener( - this.name, - (ev: CustomEventInit) => { - callback(ev.detail.serverStatus); - }, - { once: singleShot } - ); - } + static on(callback: (serverStatus: ServerStatus) => void, singleShot = false) { + document.addEventListener( + this.name, + (ev: CustomEventInit) => { + callback(ev.detail.serverStatus); + }, + { once: singleShot } + ); + } - static dispatch(serverStatus: ServerStatus) { - document.dispatchEvent(new CustomEvent(this.name, { detail: { serverStatus } })); - // Logging disabled since periodic - } + static dispatch(serverStatus: ServerStatus) { + document.dispatchEvent(new CustomEvent(this.name, { detail: { serverStatus } })); + // Logging disabled since periodic + } } export class UnitDatabaseLoadedEvent extends BaseOlympusEvent {} export class InfoPopupEvent { - static on(callback: (messages: string[]) => void, singleShot = false) { - document.addEventListener( - this.name, - (ev: CustomEventInit) => { - callback(ev.detail.messages); - }, - { once: singleShot } - ); - } + static on(callback: (messages: string[]) => void, singleShot = false) { + document.addEventListener( + this.name, + (ev: CustomEventInit) => { + callback(ev.detail.messages); + }, + { once: singleShot } + ); + } - static dispatch(messages: string[]) { - document.dispatchEvent(new CustomEvent(this.name, { detail: { messages } })); - if (DEBUG) console.log(`Event ${this.name} dispatched`); - } + static dispatch(messages: string[]) { + document.dispatchEvent(new CustomEvent(this.name, { detail: { messages } })); + if (DEBUG) console.log(`Event ${this.name} dispatched`); + } } export class WrongCredentialsEvent extends BaseOlympusEvent {} export class ShortcutsChangedEvent { - static on(callback: (shortcuts: { [key: string]: Shortcut }) => void, singleShot = false) { - document.addEventListener( - this.name, - (ev: CustomEventInit) => { - callback(ev.detail.shortcuts); - }, - { once: singleShot } - ); - } + static on(callback: (shortcuts: { [key: string]: Shortcut }) => void, singleShot = false) { + document.addEventListener( + this.name, + (ev: CustomEventInit) => { + callback(ev.detail.shortcuts); + }, + { once: singleShot } + ); + } - static dispatch(shortcuts: { [key: string]: Shortcut }) { - document.dispatchEvent(new CustomEvent(this.name, { detail: { shortcuts } })); - if (DEBUG) console.log(`Event ${this.name} dispatched`); - } + static dispatch(shortcuts: { [key: string]: Shortcut }) { + document.dispatchEvent(new CustomEvent(this.name, { detail: { shortcuts } })); + if (DEBUG) console.log(`Event ${this.name} dispatched`); + } } export class ShortcutChangedEvent { - static on(callback: (shortcut: Shortcut) => void, singleShot = false) { - document.addEventListener( - this.name, - (ev: CustomEventInit) => { - callback(ev.detail.shortcut); - }, - { once: singleShot } - ); - } + static on(callback: (shortcut: Shortcut) => void, singleShot = false) { + document.addEventListener( + this.name, + (ev: CustomEventInit) => { + callback(ev.detail.shortcut); + }, + { once: singleShot } + ); + } - static dispatch(shortcut: Shortcut) { - document.dispatchEvent(new CustomEvent(this.name, { detail: { shortcut } })); - if (DEBUG) console.log(`Event ${this.name} dispatched`); - } + static dispatch(shortcut: Shortcut) { + document.dispatchEvent(new CustomEvent(this.name, { detail: { shortcut } })); + if (DEBUG) console.log(`Event ${this.name} dispatched`); + } } export class BindShortcutRequestEvent { - static on(callback: (shortcut: Shortcut) => void, singleShot = false) { - document.addEventListener( - this.name, - (ev: CustomEventInit) => { - callback(ev.detail.shortcut); - }, - { once: singleShot } - ); - } + static on(callback: (shortcut: Shortcut) => void, singleShot = false) { + document.addEventListener( + this.name, + (ev: CustomEventInit) => { + callback(ev.detail.shortcut); + }, + { once: singleShot } + ); + } - static dispatch(shortcut: Shortcut) { - document.dispatchEvent(new CustomEvent(this.name, { detail: { shortcut } })); - if (DEBUG) console.log(`Event ${this.name} dispatched`); - } + static dispatch(shortcut: Shortcut) { + document.dispatchEvent(new CustomEvent(this.name, { detail: { shortcut } })); + if (DEBUG) console.log(`Event ${this.name} dispatched`); + } } export class AudioOptionsChangedEvent { - static on(callback: (audioOptions: AudioOptions) => void, singleShot = false) { - document.addEventListener( - this.name, - (ev: CustomEventInit) => { - callback(ev.detail.audioOptions); - }, - { once: singleShot } - ); - } - static dispatch(audioOptions: AudioOptions) { - document.dispatchEvent(new CustomEvent(this.name, { detail: { audioOptions } })); - if (DEBUG) console.log(`Event ${this.name} dispatched`); - } + static on(callback: (audioOptions: AudioOptions) => void, singleShot = false) { + document.addEventListener( + this.name, + (ev: CustomEventInit) => { + callback(ev.detail.audioOptions); + }, + { once: singleShot } + ); + } + static dispatch(audioOptions: AudioOptions) { + document.dispatchEvent(new CustomEvent(this.name, { detail: { audioOptions } })); + if (DEBUG) console.log(`Event ${this.name} dispatched`); + } } export class ModalEvent { - static on(callback: (modal: boolean) => void, singleShot = false) { - document.addEventListener( - this.name, - (ev: CustomEventInit) => { - callback(ev.detail.modal); - }, - { once: singleShot } - ); - } + static on(callback: (modal: boolean) => void, singleShot = false) { + document.addEventListener( + this.name, + (ev: CustomEventInit) => { + callback(ev.detail.modal); + }, + { once: singleShot } + ); + } - static dispatch(modal: boolean) { - document.dispatchEvent(new CustomEvent(this.name, { detail: { modal } })); - if (DEBUG) console.log(`Event ${this.name} dispatched`); - } + static dispatch(modal: boolean) { + document.dispatchEvent(new CustomEvent(this.name, { detail: { modal } })); + if (DEBUG) console.log(`Event ${this.name} dispatched`); + } } export class SessionDataChangedEvent { - static on(callback: (sessionData: SessionData) => void, singleShot = false) { - document.addEventListener( - this.name, - (ev: CustomEventInit) => { - callback(ev.detail.sessionData); - }, - { once: singleShot } - ); - } + static on(callback: (sessionData: SessionData) => void, singleShot = false) { + document.addEventListener( + this.name, + (ev: CustomEventInit) => { + callback(ev.detail.sessionData); + }, + { once: singleShot } + ); + } - static dispatch(sessionData: SessionData) { - document.dispatchEvent(new CustomEvent(this.name, { detail: { sessionData } })); - if (DEBUG) console.log(`Event ${this.name} dispatched`); - } + static dispatch(sessionData: SessionData) { + document.dispatchEvent(new CustomEvent(this.name, { detail: { sessionData } })); + if (DEBUG) console.log(`Event ${this.name} dispatched`); + } } export class SessionDataSavedEvent extends SessionDataChangedEvent {} export class SessionDataLoadedEvent extends SessionDataChangedEvent {} export class AdminPasswordChangedEvent { - static on(callback: (password: string) => void, singleShot = false) { - document.addEventListener( - this.name, - (ev: CustomEventInit) => { - callback(ev.detail.password); - }, - { once: singleShot } - ); - } + static on(callback: (password: string) => void, singleShot = false) { + document.addEventListener( + this.name, + (ev: CustomEventInit) => { + callback(ev.detail.password); + }, + { once: singleShot } + ); + } - static dispatch(password: string) { - document.dispatchEvent(new CustomEvent(this.name, { detail: { password } })); - if (DEBUG) console.log(`Event ${this.name} dispatched`); - } + static dispatch(password: string) { + document.dispatchEvent(new CustomEvent(this.name, { detail: { password } })); + if (DEBUG) console.log(`Event ${this.name} dispatched`); + } } /************** Map events ***************/ export class MouseMovedEvent { - static on(callback: (latlng: LatLng, elevation: number) => void, singleShot = false) { - document.addEventListener( - this.name, - (ev: CustomEventInit) => { - callback(ev.detail.latlng, ev.detail.elevation); - }, - { once: singleShot } - ); - } + static on(callback: (latlng: LatLng, elevation: number) => void, singleShot = false) { + document.addEventListener( + this.name, + (ev: CustomEventInit) => { + callback(ev.detail.latlng, ev.detail.elevation); + }, + { once: singleShot } + ); + } - static dispatch(latlng: LatLng, elevation?: number) { - document.dispatchEvent(new CustomEvent(this.name, { detail: { latlng, elevation } })); - // Logging disabled since periodic - } + static dispatch(latlng: LatLng, elevation?: number) { + document.dispatchEvent(new CustomEvent(this.name, { detail: { latlng, elevation } })); + // Logging disabled since periodic + } } export class HiddenTypesChangedEvent { - static on(callback: (hiddenTypes: MapHiddenTypes) => void, singleShot = false) { - document.addEventListener( - this.name, - (ev: CustomEventInit) => { - callback(ev.detail.hiddenTypes); - }, - { once: singleShot } - ); - } + static on(callback: (hiddenTypes: MapHiddenTypes) => void, singleShot = false) { + document.addEventListener( + this.name, + (ev: CustomEventInit) => { + callback(ev.detail.hiddenTypes); + }, + { once: singleShot } + ); + } - static dispatch(hiddenTypes: MapHiddenTypes) { - document.dispatchEvent(new CustomEvent(this.name, { detail: { hiddenTypes } })); - if (DEBUG) console.log(`Event ${this.name} dispatched`); - } + static dispatch(hiddenTypes: MapHiddenTypes) { + document.dispatchEvent(new CustomEvent(this.name, { detail: { hiddenTypes } })); + if (DEBUG) console.log(`Event ${this.name} dispatched`); + } } export class MapOptionsChangedEvent { - static on(callback: (mapOptions: MapOptions, key: keyof MapOptions | undefined) => void, singleShot = false) { - document.addEventListener( - this.name, - (ev: CustomEventInit) => { - callback(ev.detail.mapOptions, ev.detail.key); - }, - { once: singleShot } - ); - } + static on(callback: (mapOptions: MapOptions, key: keyof MapOptions | undefined) => void, singleShot = false) { + document.addEventListener( + this.name, + (ev: CustomEventInit) => { + callback(ev.detail.mapOptions, ev.detail.key); + }, + { once: singleShot } + ); + } - static dispatch(mapOptions: MapOptions, key?: keyof MapOptions | undefined) { - document.dispatchEvent(new CustomEvent(this.name, { detail: { mapOptions, key: key } })); - if (DEBUG) console.log(`Event ${this.name} dispatched`); - } + static dispatch(mapOptions: MapOptions, key?: keyof MapOptions | undefined) { + document.dispatchEvent(new CustomEvent(this.name, { detail: { mapOptions, key: key } })); + if (DEBUG) console.log(`Event ${this.name} dispatched`); + } } export class MapSourceChangedEvent { - static on(callback: (source: string) => void, singleShot = false) { - document.addEventListener( - this.name, - (ev: CustomEventInit) => { - callback(ev.detail.source); - }, - { once: singleShot } - ); - } + static on(callback: (source: string) => void, singleShot = false) { + document.addEventListener( + this.name, + (ev: CustomEventInit) => { + callback(ev.detail.source); + }, + { once: singleShot } + ); + } - static dispatch(source: string) { - document.dispatchEvent(new CustomEvent(this.name, { detail: { source } })); - if (DEBUG) console.log(`Event ${this.name} dispatched`); - } + static dispatch(source: string) { + document.dispatchEvent(new CustomEvent(this.name, { detail: { source } })); + if (DEBUG) console.log(`Event ${this.name} dispatched`); + } } export class CoalitionAreaSelectedEvent { - static on(callback: (coalitionArea: CoalitionCircle | CoalitionPolygon | null) => void, singleShot = false) { - document.addEventListener( - this.name, - (ev: CustomEventInit) => { - callback(ev.detail.coalitionArea); - }, - { once: singleShot } - ); - } + static on(callback: (coalitionArea: CoalitionCircle | CoalitionPolygon | null) => void, singleShot = false) { + document.addEventListener( + this.name, + (ev: CustomEventInit) => { + callback(ev.detail.coalitionArea); + }, + { once: singleShot } + ); + } - static dispatch(coalitionArea: CoalitionCircle | CoalitionPolygon | null) { - document.dispatchEvent(new CustomEvent(this.name, { detail: { coalitionArea } })); - if (DEBUG) console.log(`Event ${this.name} dispatched`); - } + static dispatch(coalitionArea: CoalitionCircle | CoalitionPolygon | null) { + document.dispatchEvent(new CustomEvent(this.name, { detail: { coalitionArea } })); + if (DEBUG) console.log(`Event ${this.name} dispatched`); + } } export class CoalitionAreaChangedEvent extends CoalitionAreaSelectedEvent {} export class CoalitionAreasChangedEvent { - static on(callback: (coalitionAreas: (CoalitionCircle | CoalitionPolygon)[]) => void, singleShot = false) { - document.addEventListener( - this.name, - (ev: CustomEventInit) => { - callback(ev.detail.coalitionAreas); - }, - { once: singleShot } - ); - } + static on(callback: (coalitionAreas: (CoalitionCircle | CoalitionPolygon)[]) => void, singleShot = false) { + document.addEventListener( + this.name, + (ev: CustomEventInit) => { + callback(ev.detail.coalitionAreas); + }, + { once: singleShot } + ); + } - static dispatch(coalitionAreas: (CoalitionCircle | CoalitionPolygon)[]) { - document.dispatchEvent(new CustomEvent(this.name, { detail: { coalitionAreas } })); - if (DEBUG) console.log(`Event ${this.name} dispatched`); - } + static dispatch(coalitionAreas: (CoalitionCircle | CoalitionPolygon)[]) { + document.dispatchEvent(new CustomEvent(this.name, { detail: { coalitionAreas } })); + if (DEBUG) console.log(`Event ${this.name} dispatched`); + } } export class AirbaseSelectedEvent { - static on(callback: (airbase: Airbase | null) => void, singleShot = false) { - document.addEventListener( - this.name, - (ev: CustomEventInit) => { - callback(ev.detail.airbase); - }, - { once: singleShot } - ); - } + static on(callback: (airbase: Airbase | null) => void, singleShot = false) { + document.addEventListener( + this.name, + (ev: CustomEventInit) => { + callback(ev.detail.airbase); + }, + { once: singleShot } + ); + } - static dispatch(airbase: Airbase | null) { - document.dispatchEvent(new CustomEvent(this.name, { detail: { airbase } })); - if (DEBUG) console.log(`Event ${this.name} dispatched`); - } + static dispatch(airbase: Airbase | null) { + document.dispatchEvent(new CustomEvent(this.name, { detail: { airbase } })); + if (DEBUG) console.log(`Event ${this.name} dispatched`); + } } export class SelectionEnabledChangedEvent { - static on(callback: (enabled: boolean) => void, singleShot = false) { - document.addEventListener( - this.name, - (ev: CustomEventInit) => { - callback(ev.detail.enabled); - }, - { once: singleShot } - ); - } + static on(callback: (enabled: boolean) => void, singleShot = false) { + document.addEventListener( + this.name, + (ev: CustomEventInit) => { + callback(ev.detail.enabled); + }, + { once: singleShot } + ); + } - static dispatch(enabled: boolean) { - document.dispatchEvent(new CustomEvent(this.name, { detail: { enabled } })); - if (DEBUG) console.log(`Event ${this.name} dispatched`); - } + static dispatch(enabled: boolean) { + document.dispatchEvent(new CustomEvent(this.name, { detail: { enabled } })); + if (DEBUG) console.log(`Event ${this.name} dispatched`); + } } export class PasteEnabledChangedEvent { - static on(callback: (enabled: boolean) => void, singleShot = false) { - document.addEventListener( - this.name, - (ev: CustomEventInit) => { - callback(ev.detail.enabled); - }, - { once: singleShot } - ); - } + static on(callback: (enabled: boolean) => void, singleShot = false) { + document.addEventListener( + this.name, + (ev: CustomEventInit) => { + callback(ev.detail.enabled); + }, + { once: singleShot } + ); + } - static dispatch(enabled: boolean) { - document.dispatchEvent(new CustomEvent(this.name, { detail: { enabled } })); - if (DEBUG) console.log(`Event ${this.name} dispatched`); - } + static dispatch(enabled: boolean) { + document.dispatchEvent(new CustomEvent(this.name, { detail: { enabled } })); + if (DEBUG) console.log(`Event ${this.name} dispatched`); + } } export class ContactsUpdatedEvent { - static on(callback: () => void, singleShot = false) { - document.addEventListener( - this.name, - (ev: CustomEventInit) => { - callback(); - }, - { once: singleShot } - ); - } + static on(callback: () => void, singleShot = false) { + document.addEventListener( + this.name, + (ev: CustomEventInit) => { + callback(); + }, + { once: singleShot } + ); + } - static dispatch() { - document.dispatchEvent(new CustomEvent(this.name)); - // Logging disabled since periodic - } + static dispatch() { + document.dispatchEvent(new CustomEvent(this.name)); + // Logging disabled since periodic + } } export class ContextActionSetChangedEvent { - static on(callback: (contextActionSet: ContextActionSet | null) => void, singleShot = false) { - document.addEventListener( - this.name, - (ev: CustomEventInit) => { - callback(ev.detail.contextActionSet); - }, - { once: singleShot } - ); - } + static on(callback: (contextActionSet: ContextActionSet | null) => void, singleShot = false) { + document.addEventListener( + this.name, + (ev: CustomEventInit) => { + callback(ev.detail.contextActionSet); + }, + { once: singleShot } + ); + } - static dispatch(contextActionSet: ContextActionSet | null) { - document.dispatchEvent(new CustomEvent(this.name, { detail: { contextActionSet } })); - if (DEBUG) console.log(`Event ${this.name} dispatched`); - } + static dispatch(contextActionSet: ContextActionSet | null) { + document.dispatchEvent(new CustomEvent(this.name, { detail: { contextActionSet } })); + if (DEBUG) console.log(`Event ${this.name} dispatched`); + } } export class ContextActionChangedEvent { - static on(callback: (contextAction: ContextAction | null) => void, singleShot = false) { - document.addEventListener( - this.name, - (ev: CustomEventInit) => { - callback(ev.detail.contextAction); - }, - { once: singleShot } - ); - } + static on(callback: (contextAction: ContextAction | null) => void, singleShot = false) { + document.addEventListener( + this.name, + (ev: CustomEventInit) => { + callback(ev.detail.contextAction); + }, + { once: singleShot } + ); + } - static dispatch(contextAction: ContextAction | null) { - document.dispatchEvent(new CustomEvent(this.name, { detail: { contextAction } })); - if (DEBUG) console.log(`Event ${this.name} dispatched`); - } + static dispatch(contextAction: ContextAction | null) { + document.dispatchEvent(new CustomEvent(this.name, { detail: { contextAction } })); + if (DEBUG) console.log(`Event ${this.name} dispatched`); + } } export class CopiedUnitsEvents { - static on(callback: (unitsData: UnitData[]) => void, singleShot = false) { - document.addEventListener( - this.name, - (ev: CustomEventInit) => { - callback(ev.detail.unitsData); - }, - { once: singleShot } - ); - } + static on(callback: (unitsData: UnitData[]) => void, singleShot = false) { + document.addEventListener( + this.name, + (ev: CustomEventInit) => { + callback(ev.detail.unitsData); + }, + { once: singleShot } + ); + } - static dispatch(unitsData: UnitData[]) { - document.dispatchEvent(new CustomEvent(this.name, { detail: { unitsData } })); - if (DEBUG) console.log(`Event ${this.name} dispatched`); - } + static dispatch(unitsData: UnitData[]) { + document.dispatchEvent(new CustomEvent(this.name, { detail: { unitsData } })); + if (DEBUG) console.log(`Event ${this.name} dispatched`); + } } export class SelectionClearedEvent extends BaseOlympusEvent {} export class UnitUpdatedEvent extends BaseUnitEvent { - static dispatch(unit: Unit) { - document.dispatchEvent(new CustomEvent(this.name, { detail: { unit } })); - // Logging disabled since periodic - } + static dispatch(unit: Unit) { + document.dispatchEvent(new CustomEvent(this.name, { detail: { unit } })); + // Logging disabled since periodic + } } export class UnitSelectedEvent extends BaseUnitEvent {} export class UnitDeselectedEvent extends BaseUnitEvent {} export class UnitDeadEvent extends BaseUnitEvent {} export class UnitsUpdatedEvent extends BaseUnitsEvent { - static dispatch(units: Unit[]) { - document.dispatchEvent(new CustomEvent(this.name, { detail: units })); - // Logging disabled since periodic - } + static dispatch(units: Unit[]) { + document.dispatchEvent(new CustomEvent(this.name, { detail: units })); + // Logging disabled since periodic + } } export class UnitsRefreshedEvent extends BaseUnitsEvent {} export class SelectedUnitsChangedEvent extends BaseUnitsEvent {} export class UnitExplosionRequestEvent { - static on(callback: (units: Unit[]) => void, singleShot = false) { - document.addEventListener( - this.name, - (ev: CustomEventInit) => { - callback(ev.detail.units); - }, - { once: singleShot } - ); - } + static on(callback: (units: Unit[]) => void, singleShot = false) { + document.addEventListener( + this.name, + (ev: CustomEventInit) => { + callback(ev.detail.units); + }, + { once: singleShot } + ); + } - static dispatch(units: Unit[]) { - document.dispatchEvent(new CustomEvent(this.name, { detail: { units } })); - if (DEBUG) console.log(`Event ${this.name} dispatched`); - } + static dispatch(units: Unit[]) { + document.dispatchEvent(new CustomEvent(this.name, { detail: { units } })); + if (DEBUG) console.log(`Event ${this.name} dispatched`); + } } export class FormationCreationRequestEvent { - static on(callback: (leader: Unit, wingmen: Unit[]) => void, singleShot = false) { - document.addEventListener( - this.name, - (ev: CustomEventInit) => { - callback(ev.detail.leader, ev.detail.wingmen); - }, - { once: singleShot } - ); - } + static on(callback: (leader: Unit, wingmen: Unit[]) => void, singleShot = false) { + document.addEventListener( + this.name, + (ev: CustomEventInit) => { + callback(ev.detail.leader, ev.detail.wingmen); + }, + { once: singleShot } + ); + } - static dispatch(leader: Unit, wingmen: Unit[]) { - document.dispatchEvent(new CustomEvent(this.name, { detail: { leader, wingmen } })); - if (DEBUG) console.log(`Event ${this.name} dispatched`); - } + static dispatch(leader: Unit, wingmen: Unit[]) { + document.dispatchEvent(new CustomEvent(this.name, { detail: { leader, wingmen } })); + if (DEBUG) console.log(`Event ${this.name} dispatched`); + } } export class MapContextMenuRequestEvent { - static on(callback: (latlng: L.LatLng) => void, singleShot = false) { - document.addEventListener( - this.name, - (ev: CustomEventInit) => { - callback(ev.detail.latlng); - }, - { once: singleShot } - ); - } + static on(callback: (latlng: L.LatLng) => void, singleShot = false) { + document.addEventListener( + this.name, + (ev: CustomEventInit) => { + callback(ev.detail.latlng); + }, + { once: singleShot } + ); + } - static dispatch(latlng: L.LatLng) { - document.dispatchEvent(new CustomEvent(this.name, { detail: { latlng } })); - if (DEBUG) console.log(`Event ${this.name} dispatched`); - } + static dispatch(latlng: L.LatLng) { + document.dispatchEvent(new CustomEvent(this.name, { detail: { latlng } })); + if (DEBUG) console.log(`Event ${this.name} dispatched`); + } } export class UnitContextMenuRequestEvent { - static on(callback: (unit: Unit) => void, singleShot = false) { - document.addEventListener( - this.name, - (ev: CustomEventInit) => { - callback(ev.detail.unit); - }, - { once: singleShot } - ); - } + static on(callback: (unit: Unit) => void, singleShot = false) { + document.addEventListener( + this.name, + (ev: CustomEventInit) => { + callback(ev.detail.unit); + }, + { once: singleShot } + ); + } - static dispatch(unit: Unit) { - document.dispatchEvent(new CustomEvent(this.name, { detail: { unit } })); - if (DEBUG) console.log(`Event ${this.name} dispatched`); - } + static dispatch(unit: Unit) { + document.dispatchEvent(new CustomEvent(this.name, { detail: { unit } })); + if (DEBUG) console.log(`Event ${this.name} dispatched`); + } } export class SpawnContextMenuRequestEvent { - static on(callback: (latlng: L.LatLng) => void, singleShot = false) { - document.addEventListener( - this.name, - (ev: CustomEventInit) => { - callback(ev.detail.latlng); - }, - { once: singleShot } - ); - } + static on(callback: (latlng: L.LatLng) => void, singleShot = false) { + document.addEventListener( + this.name, + (ev: CustomEventInit) => { + callback(ev.detail.latlng); + }, + { once: singleShot } + ); + } - static dispatch(latlng: L.LatLng) { - document.dispatchEvent(new CustomEvent(this.name, { detail: { latlng } })); - if (DEBUG) console.log(`Event ${this.name} dispatched`); - } + static dispatch(latlng: L.LatLng) { + document.dispatchEvent(new CustomEvent(this.name, { detail: { latlng } })); + if (DEBUG) console.log(`Event ${this.name} dispatched`); + } } export class SpawnHeadingChangedEvent { - static on(callback: (heading: number) => void, singleShot = false) { - document.addEventListener( - this.name, - (ev: CustomEventInit) => { - callback(ev.detail.heading); - }, - { once: singleShot } - ); - } + static on(callback: (heading: number) => void, singleShot = false) { + document.addEventListener( + this.name, + (ev: CustomEventInit) => { + callback(ev.detail.heading); + }, + { once: singleShot } + ); + } - static dispatch(heading: number) { - document.dispatchEvent(new CustomEvent(this.name, { detail: { heading } })); - if (DEBUG) console.log(`Event ${this.name} dispatched`); - } + static dispatch(heading: number) { + document.dispatchEvent(new CustomEvent(this.name, { detail: { heading } })); + if (DEBUG) console.log(`Event ${this.name} dispatched`); + } } export class HotgroupsChangedEvent { - static on(callback: (hotgroups: { [key: number]: Unit[] }) => void, singleShot = false) { - document.addEventListener( - this.name, - (ev: CustomEventInit) => { - callback(ev.detail.hotgroups); - }, - { once: singleShot } - ); - } + static on(callback: (hotgroups: { [key: number]: Unit[] }) => void, singleShot = false) { + document.addEventListener( + this.name, + (ev: CustomEventInit) => { + callback(ev.detail.hotgroups); + }, + { once: singleShot } + ); + } - static dispatch(hotgroups: { [key: number]: Unit[] }) { - document.dispatchEvent(new CustomEvent(this.name, { detail: { hotgroups } })); - if (DEBUG) console.log(`Event ${this.name} dispatched`); - } + static dispatch(hotgroups: { [key: number]: Unit[] }) { + document.dispatchEvent(new CustomEvent(this.name, { detail: { hotgroups } })); + if (DEBUG) console.log(`Event ${this.name} dispatched`); + } } export class StarredSpawnsChangedEvent { - static on(callback: (starredSpawns: { [key: number]: SpawnRequestTable }) => void, singleShot = false) { - document.addEventListener( - this.name, - (ev: CustomEventInit) => { - callback(ev.detail.starredSpawns); - }, - { once: singleShot } - ); - } + static on(callback: (starredSpawns: { [key: number]: SpawnRequestTable }) => void, singleShot = false) { + document.addEventListener( + this.name, + (ev: CustomEventInit) => { + callback(ev.detail.starredSpawns); + }, + { once: singleShot } + ); + } - static dispatch(starredSpawns: { [key: number]: SpawnRequestTable }) { - document.dispatchEvent(new CustomEvent(this.name, { detail: { starredSpawns } })); - if (DEBUG) console.log(`Event ${this.name} dispatched`); - } + static dispatch(starredSpawns: { [key: number]: SpawnRequestTable }) { + document.dispatchEvent(new CustomEvent(this.name, { detail: { starredSpawns } })); + if (DEBUG) console.log(`Event ${this.name} dispatched`); + } } export class AWACSReferenceChangedEvent { - static on(callback: (unit: Unit | null) => void, singleShot = false) { - document.addEventListener( - this.name, - (ev: CustomEventInit) => { - callback(ev.detail); - }, - { once: singleShot } - ); - } + static on(callback: (unit: Unit | null) => void, singleShot = false) { + document.addEventListener( + this.name, + (ev: CustomEventInit) => { + callback(ev.detail); + }, + { once: singleShot } + ); + } - static dispatch(unit: Unit | null) { - document.dispatchEvent(new CustomEvent(this.name, { detail: unit })); - // Logging disabled since periodic - } + static dispatch(unit: Unit | null) { + document.dispatchEvent(new CustomEvent(this.name, { detail: unit })); + // Logging disabled since periodic + } } export class DrawingsInitEvent { - static on(callback: (drawingsData: any, navpointData: any /*TODO*/) => void, singleShot = false) { - document.addEventListener( - this.name, - (ev: CustomEventInit) => { - callback(ev.detail.drawingsData, ev.detail.navpointData); - }, - { once: singleShot } - ); - } + static on(callback: (drawingsData: any, navpointData: any /*TODO*/) => void, singleShot = false) { + document.addEventListener( + this.name, + (ev: CustomEventInit) => { + callback(ev.detail.drawingsData, ev.detail.navpointData); + }, + { once: singleShot } + ); + } - static dispatch(drawingsData: any, navpointData?: any /*TODO*/) { - document.dispatchEvent(new CustomEvent(this.name, { detail: drawingsData })); - if (DEBUG) console.log(`Event ${this.name} dispatched`); - } + static dispatch(drawingsData: any, navpointData?: any /*TODO*/) { + document.dispatchEvent(new CustomEvent(this.name, { detail: drawingsData })); + if (DEBUG) console.log(`Event ${this.name} dispatched`); + } } export class DrawingsUpdatedEvent extends BaseOlympusEvent {} /************** Command mode events ***************/ export class CommandModeOptionsChangedEvent { - static on(callback: (options: CommandModeOptions) => void, singleShot = false) { - document.addEventListener( - this.name, - (ev: CustomEventInit) => { - callback(ev.detail); - }, - { once: singleShot } - ); - } + static on(callback: (options: CommandModeOptions) => void, singleShot = false) { + document.addEventListener( + this.name, + (ev: CustomEventInit) => { + callback(ev.detail); + }, + { once: singleShot } + ); + } - static dispatch(options: CommandModeOptions) { - document.dispatchEvent(new CustomEvent(this.name, { detail: options })); - if (DEBUG) console.log(`Event ${this.name} dispatched`); - } + static dispatch(options: CommandModeOptions) { + document.dispatchEvent(new CustomEvent(this.name, { detail: options })); + if (DEBUG) console.log(`Event ${this.name} dispatched`); + } } /************** Audio backend events ***************/ export class AudioSourcesChangedEvent { - static on(callback: (audioSources: AudioSource[]) => void, singleShot = false) { - document.addEventListener( - this.name, - (ev: CustomEventInit) => { - callback(ev.detail.audioSources); - }, - { once: singleShot } - ); - } + static on(callback: (audioSources: AudioSource[]) => void, singleShot = false) { + document.addEventListener( + this.name, + (ev: CustomEventInit) => { + callback(ev.detail.audioSources); + }, + { once: singleShot } + ); + } - static dispatch(audioSources: AudioSource[]) { - document.dispatchEvent(new CustomEvent(this.name, { detail: { audioSources } })); - if (DEBUG) console.log(`Event ${this.name} dispatched`); - if (DEBUG) console.log(audioSources); - } + static dispatch(audioSources: AudioSource[]) { + document.dispatchEvent(new CustomEvent(this.name, { detail: { audioSources } })); + if (DEBUG) console.log(`Event ${this.name} dispatched`); + if (DEBUG) console.log(audioSources); + } } export class AudioSinksChangedEvent { - static on(callback: (audioSinks: AudioSink[]) => void, singleShot = false) { - document.addEventListener( - this.name, - (ev: CustomEventInit) => { - callback(ev.detail.audioSinks); - }, - { once: singleShot } - ); - } + static on(callback: (audioSinks: AudioSink[]) => void, singleShot = false) { + document.addEventListener( + this.name, + (ev: CustomEventInit) => { + callback(ev.detail.audioSinks); + }, + { once: singleShot } + ); + } - static dispatch(audioSinks: AudioSink[]) { - document.dispatchEvent(new CustomEvent(this.name, { detail: { audioSinks } })); - if (DEBUG) console.log(`Event ${this.name} dispatched`); - if (DEBUG) console.log(audioSinks); - } + static dispatch(audioSinks: AudioSink[]) { + document.dispatchEvent(new CustomEvent(this.name, { detail: { audioSinks } })); + if (DEBUG) console.log(`Event ${this.name} dispatched`); + if (DEBUG) console.log(audioSinks); + } } export class SRSClientsChangedEvent { - static on(callback: (clientsData: SRSClientData[]) => void, singleShot = false) { - document.addEventListener( - this.name, - (ev: CustomEventInit) => { - callback(ev.detail.clientsData); - }, - { once: singleShot } - ); - } + static on(callback: (clientsData: SRSClientData[]) => void, singleShot = false) { + document.addEventListener( + this.name, + (ev: CustomEventInit) => { + callback(ev.detail.clientsData); + }, + { once: singleShot } + ); + } - static dispatch(clientsData: SRSClientData[]) { - document.dispatchEvent(new CustomEvent(this.name, { detail: { clientsData } })); - // Logging disabled since periodic - } + static dispatch(clientsData: SRSClientData[]) { + document.dispatchEvent(new CustomEvent(this.name, { detail: { clientsData } })); + // Logging disabled since periodic + } } export class AudioManagerStateChangedEvent { - static on(callback: (state: string) => void, singleShot = false) { - document.addEventListener( - this.name, - (ev: CustomEventInit) => { - callback(ev.detail.state); - }, - { once: singleShot } - ); - } + static on(callback: (state: string) => void, singleShot = false) { + document.addEventListener( + this.name, + (ev: CustomEventInit) => { + callback(ev.detail.state); + }, + { once: singleShot } + ); + } - static dispatch(state: string) { - document.dispatchEvent(new CustomEvent(this.name, { detail: { state } })); - if (DEBUG) console.log(`Event ${this.name} dispatched`); - } + static dispatch(state: string) { + document.dispatchEvent(new CustomEvent(this.name, { detail: { state } })); + if (DEBUG) console.log(`Event ${this.name} dispatched`); + } } export class AudioManagerDevicesChangedEvent { - static on(callback: (devices: MediaDeviceInfo[]) => void, singleShot = false) { - document.addEventListener( - this.name, - (ev: CustomEventInit) => { - callback(ev.detail.devices); - }, - { once: singleShot } - ); - } + static on(callback: (devices: MediaDeviceInfo[]) => void, singleShot = false) { + document.addEventListener( + this.name, + (ev: CustomEventInit) => { + callback(ev.detail.devices); + }, + { once: singleShot } + ); + } - static dispatch(devices: MediaDeviceInfo[]) { - document.dispatchEvent(new CustomEvent(this.name, { detail: { devices } })); - if (DEBUG) console.log(`Event ${this.name} dispatched`); - } + static dispatch(devices: MediaDeviceInfo[]) { + document.dispatchEvent(new CustomEvent(this.name, { detail: { devices } })); + if (DEBUG) console.log(`Event ${this.name} dispatched`); + } } export class AudioManagerInputChangedEvent { - static on(callback: (input: MediaDeviceInfo) => void, singleShot = false) { - document.addEventListener( - this.name, - (ev: CustomEventInit) => { - callback(ev.detail.input); - }, - { once: singleShot } - ); - } + static on(callback: (input: MediaDeviceInfo) => void, singleShot = false) { + document.addEventListener( + this.name, + (ev: CustomEventInit) => { + callback(ev.detail.input); + }, + { once: singleShot } + ); + } - static dispatch(input: MediaDeviceInfo) { - document.dispatchEvent(new CustomEvent(this.name, { detail: { input } })); - if (DEBUG) console.log(`Event ${this.name} dispatched`); - } + static dispatch(input: MediaDeviceInfo) { + document.dispatchEvent(new CustomEvent(this.name, { detail: { input } })); + if (DEBUG) console.log(`Event ${this.name} dispatched`); + } } export class AudioManagerOutputChangedEvent { - static on(callback: (output: MediaDeviceInfo) => void, singleShot = false) { - document.addEventListener( - this.name, - (ev: CustomEventInit) => { - callback(ev.detail.output); - }, - { once: singleShot } - ); - } + static on(callback: (output: MediaDeviceInfo) => void, singleShot = false) { + document.addEventListener( + this.name, + (ev: CustomEventInit) => { + callback(ev.detail.output); + }, + { once: singleShot } + ); + } - static dispatch(output: MediaDeviceInfo) { - document.dispatchEvent(new CustomEvent(this.name, { detail: { output } })); - if (DEBUG) console.log(`Event ${this.name} dispatched`); - } + static dispatch(output: MediaDeviceInfo) { + document.dispatchEvent(new CustomEvent(this.name, { detail: { output } })); + if (DEBUG) console.log(`Event ${this.name} dispatched`); + } } export class AudioManagerCoalitionChangedEvent { - static on(callback: (coalition: Coalition) => void, singleShot = false) { - document.addEventListener( - this.name, - (ev: CustomEventInit) => { - callback(ev.detail.coalition); - }, - { once: singleShot } - ); - } + static on(callback: (coalition: Coalition) => void, singleShot = false) { + document.addEventListener( + this.name, + (ev: CustomEventInit) => { + callback(ev.detail.coalition); + }, + { once: singleShot } + ); + } - static dispatch(coalition: Coalition) { - document.dispatchEvent(new CustomEvent(this.name, { detail: { coalition } })); - if (DEBUG) console.log(`Event ${this.name} dispatched`); - } + static dispatch(coalition: Coalition) { + document.dispatchEvent(new CustomEvent(this.name, { detail: { coalition } })); + if (DEBUG) console.log(`Event ${this.name} dispatched`); + } } /************** Mission data events ***************/ export class BullseyesDataChangedEvent { - static on(callback: (bullseyes: { [name: string]: Bullseye }) => void, singleShot = false) { - document.addEventListener( - this.name, - (ev: CustomEventInit) => { - callback(ev.detail.bullseyes); - }, - { once: singleShot } - ); - } + static on(callback: (bullseyes: { [name: string]: Bullseye }) => void, singleShot = false) { + document.addEventListener( + this.name, + (ev: CustomEventInit) => { + callback(ev.detail.bullseyes); + }, + { once: singleShot } + ); + } - static dispatch(bullseyes: { [name: string]: Bullseye }) { - document.dispatchEvent(new CustomEvent(this.name, { detail: { bullseyes } })); - // Logging disabled since periodic - } + static dispatch(bullseyes: { [name: string]: Bullseye }) { + document.dispatchEvent(new CustomEvent(this.name, { detail: { bullseyes } })); + // Logging disabled since periodic + } } export class MissionDataChangedEvent { - static on(callback: (missionData: MissionData) => void, singleShot = false) { - document.addEventListener( - this.name, - (ev: CustomEventInit) => { - callback(ev.detail.missionData); - }, - { once: singleShot } - ); - } + static on(callback: (missionData: MissionData) => void, singleShot = false) { + document.addEventListener( + this.name, + (ev: CustomEventInit) => { + callback(ev.detail.missionData); + }, + { once: singleShot } + ); + } - static dispatch(missionData: MissionData) { - document.dispatchEvent(new CustomEvent(this.name, { detail: { missionData } })); - // Logging disabled since periodic - } + static dispatch(missionData: MissionData) { + document.dispatchEvent(new CustomEvent(this.name, { detail: { missionData } })); + // Logging disabled since periodic + } } export class EnabledCommandModesChangedEvent { - static on(callback: (enabledCommandModes: string[]) => void, singleShot = false) { - document.addEventListener( - this.name, - (ev: CustomEventInit) => { - callback(ev.detail.enabledCommandModes); - }, - { once: singleShot } - ); - } + static on(callback: (enabledCommandModes: string[]) => void, singleShot = false) { + document.addEventListener( + this.name, + (ev: CustomEventInit) => { + callback(ev.detail.enabledCommandModes); + }, + { once: singleShot } + ); + } - static dispatch(enabledCommandModes: string[]) { - document.dispatchEvent(new CustomEvent(this.name, { detail: { enabledCommandModes } })); - // Logging disabled since periodic - } + static dispatch(enabledCommandModes: string[]) { + document.dispatchEvent(new CustomEvent(this.name, { detail: { enabledCommandModes } })); + // Logging disabled since periodic + } } /************** Other events ***************/ export class WeaponsRefreshedEvent { - static on(callback: (weapons: Weapon[]) => void, singleShot = false) { - document.addEventListener( - this.name, - (ev: CustomEventInit) => { - callback(ev.detail); - }, - { once: singleShot } - ); - } + static on(callback: (weapons: Weapon[]) => void, singleShot = false) { + document.addEventListener( + this.name, + (ev: CustomEventInit) => { + callback(ev.detail); + }, + { once: singleShot } + ); + } - static dispatch(weapons: Weapon[]) { - document.dispatchEvent(new CustomEvent(this.name, { detail: weapons })); - if (DEBUG) console.log(`Event ${this.name} dispatched`); - } + static dispatch(weapons: Weapon[]) { + document.dispatchEvent(new CustomEvent(this.name, { detail: weapons })); + if (DEBUG) console.log(`Event ${this.name} dispatched`); + } } export class CoordinatesFreezeEvent { - static on(callback: () => void) { - document.addEventListener(this.name, (ev: CustomEventInit) => { - callback(); - }); - } + static on(callback: () => void) { + document.addEventListener(this.name, (ev: CustomEventInit) => { + callback(); + }); + } - static dispatch() { - document.dispatchEvent(new CustomEvent(this.name)); - if (DEBUG) console.log(`Event ${this.name} dispatched`); - } + static dispatch() { + document.dispatchEvent(new CustomEvent(this.name)); + if (DEBUG) console.log(`Event ${this.name} dispatched`); + } +} + +export class SetLoadoutWizardBlueprintEvent { + static on(callback: (blueprint: UnitBlueprint) => void, singleShot = false) { + document.addEventListener( + this.name, + (ev: CustomEventInit) => { + callback(ev.detail.blueprint); + }, + { once: singleShot } + ); + } + + static dispatch(blueprint: UnitBlueprint) { + document.dispatchEvent(new CustomEvent(this.name, { detail: { blueprint } })); + if (DEBUG) console.log(`Event ${this.name} dispatched`); + } +} + +export class CustomLoadoutsUpdatedEvent { + static on(callback: (unitName: string, loadout: LoadoutBlueprint) => void, singleShot = false) { + document.addEventListener( + this.name, + (ev: CustomEventInit) => { + callback(ev.detail.unitName, ev.detail.loadout); + }, + { once: singleShot } + ); + } + static dispatch(unitName: string, loadout: LoadoutBlueprint) { + document.dispatchEvent(new CustomEvent(this.name, { detail: { unitName, loadout } })); + if (DEBUG) console.log(`Event ${this.name} dispatched`); + } } diff --git a/frontend/react/src/interfaces.ts b/frontend/react/src/interfaces.ts index 7fbdef20..170c60e2 100644 --- a/frontend/react/src/interfaces.ts +++ b/frontend/react/src/interfaces.ts @@ -2,403 +2,405 @@ import { LatLng } from "leaflet"; import { AudioOptions, Coalition, MapOptions } from "./types/types"; export interface OlympusConfig { - /* Set by user */ - frontend: { - port: number; - elevationProvider: { - provider: string; - username: string | null; - password: string | null; - }; - mapLayers: { - [key: string]: { - urlTemplate: string; - minZoom: number; - maxZoom: number; - attribution?: string; - }; - }; - mapMirrors: { - [key: string]: string; + /* Set by user */ + frontend: { + port: number; + elevationProvider: { + provider: string; + username: string | null; + password: string | null; + }; + mapLayers: { + [key: string]: { + urlTemplate: string; + minZoom: number; + maxZoom: number; + attribution?: string; + }; + }; + mapMirrors: { + [key: string]: string; + }; + /* New with v2.0.0 */ + customAuthHeaders?: { + enabled: boolean; + username: string; + group: string; + }; + autoconnectWhenLocal?: boolean; }; /* New with v2.0.0 */ - customAuthHeaders?: { - enabled: boolean; - username: string; - group: string; + audio?: { + SRSPort: number; + WSPort?: number; + WSEndpoint?: string; }; - autoconnectWhenLocal?: boolean; - }; - /* New with v2.0.0 */ - audio?: { - SRSPort: number; - WSPort?: number; - WSEndpoint?: string; - }; - controllers?: [{ type: string; coalition: Coalition; frequency: number; modulation: number; callsign: string }]; - profiles?: { [key: string]: ProfileOptions }; + controllers?: [{ type: string; coalition: Coalition; frequency: number; modulation: number; callsign: string }]; + profiles?: { [key: string]: ProfileOptions }; - /* Set by server */ - local?: boolean; - authentication?: { - // Only sent when in localhost mode for autologin - gameMasterPassword: string; - blueCommanderPassword: string; - redCommanderPassword: string; - }; + /* Set by server */ + local?: boolean; + authentication?: { + // Only sent when in localhost mode for autologin + gameMasterPassword: string; + blueCommanderPassword: string; + redCommanderPassword: string; + }; } export interface SessionData { - radios?: { frequency: number; modulation: number; pan: number }[]; - fileSources?: { filename: string; volume: number }[]; - unitSinks?: { ID: number }[]; - connections?: any[]; - coalitionAreas?: ( - | { type: "circle"; label: string; latlng: { lat: number; lng: number }; radius: number; coalition: Coalition } - | { type: "polygon"; label: string; latlngs: { lat: number; lng: number }[]; coalition: Coalition } - )[]; - hotgroups?: { [key: string]: number[] }; - starredSpawns?: { [key: number]: SpawnRequestTable }; - drawings?: { [key: string]: { visibility: boolean; opacity: number; name: string; guid: string; containers: any; drawings: any } }; - navpoints?: { [key: string]: { visibility: boolean; opacity: number; name: string; guid: string; containers: any; drawings: any } }; - mapSource?: { id: string }; + radios?: { frequency: number; modulation: number; pan: number }[]; + fileSources?: { filename: string; volume: number }[]; + unitSinks?: { ID: number }[]; + connections?: any[]; + coalitionAreas?: ( + | { type: "circle"; label: string; latlng: { lat: number; lng: number }; radius: number; coalition: Coalition } + | { type: "polygon"; label: string; latlngs: { lat: number; lng: number }[]; coalition: Coalition } + )[]; + hotgroups?: { [key: string]: number[] }; + starredSpawns?: { [key: number]: SpawnRequestTable }; + drawings?: { [key: string]: { visibility: boolean; opacity: number; name: string; guid: string; containers: any; drawings: any } }; + navpoints?: { [key: string]: { visibility: boolean; opacity: number; name: string; guid: string; containers: any; drawings: any } }; + mapSource?: { id: string }; + customLoadouts?: { [key: string]: LoadoutBlueprint[] }; } export interface ProfileOptions { - mapOptions?: MapOptions; - shortcuts?: { [key: string]: ShortcutOptions }; - audioOptions?: AudioOptions; + mapOptions?: MapOptions; + shortcuts?: { [key: string]: ShortcutOptions }; + audioOptions?: AudioOptions; } export interface ContextMenuOption { - tooltip: string; - src: string; - callback: CallableFunction; + tooltip: string; + src: string; + callback: CallableFunction; } export interface AirbasesData { - airbases: { [key: string]: any }; - sessionHash: string; - time: number; + airbases: { [key: string]: any }; + sessionHash: string; + time: number; } export interface BullseyesData { - bullseyes: { - [key: string]: { latitude: number; longitude: number; coalition: string }; - }; - sessionHash: string; - time: number; + bullseyes: { + [key: string]: { latitude: number; longitude: number; coalition: string }; + }; + sessionHash: string; + time: number; } export interface SpotsData { - spots: { - [key: string]: { active: boolean; type: string; targetPosition: { lat: number; lng: number }; sourceUnitID: number; code?: number }; - }; - sessionHash: string; - time: number; + spots: { + [key: string]: { active: boolean; type: string; targetPosition: { lat: number; lng: number }; sourceUnitID: number; code?: number }; + }; + sessionHash: string; + time: number; } export interface MissionData { - mission: { - theatre: string; - dateAndTime: DateAndTime; - commandModeOptions: CommandModeOptions; - coalitions: { red: string[]; blue: string[] }; - }; - time: number; - sessionHash: string; + mission: { + theatre: string; + dateAndTime: DateAndTime; + commandModeOptions: CommandModeOptions; + coalitions: { red: string[]; blue: string[] }; + }; + time: number; + sessionHash: string; } export interface CommandModeOptions { - commandMode: string; - restrictSpawns: boolean; - restrictToCoalition: boolean; - setupTime: number; - spawnPoints: { - red: number; - blue: number; - }; - eras: string[]; + commandMode: string; + restrictSpawns: boolean; + restrictToCoalition: boolean; + setupTime: number; + spawnPoints: { + red: number; + blue: number; + }; + eras: string[]; } export interface DateAndTime { - date: { Year: number; Month: number; Day: number }; - time: { h: number; m: number; s: number }; - elapsedTime: number; - startTime: number; + date: { Year: number; Month: number; Day: number }; + time: { h: number; m: number; s: number }; + elapsedTime: number; + startTime: number; } export interface LogData { - logs: { [key: string]: string }; - sessionHash: string; - time: number; + logs: { [key: string]: string }; + sessionHash: string; + time: number; } export interface ServerRequestOptions { - time?: number; - commandHash?: string; + time?: number; + commandHash?: string; } export interface SpawnRequestTable { - category: string; - coalition: string; - unit: UnitSpawnTable; - amount: number; - quickAccessName?: string; + category: string; + coalition: string; + unit: UnitSpawnTable; + amount: number; + quickAccessName?: string; } export interface EffectRequestTable { - type: string; - explosionType?: string; - smokeColor?: string; + type: string; + explosionType?: string; + smokeColor?: string; } export interface UnitSpawnTable { - unitType: string; - location: LatLng; - skill: string; - liveryID: string; - altitude?: number; - loadout?: string; - heading?: number; + unitType: string; + location: LatLng; + skill: string; + liveryID: string; + altitude?: number; + loadout?: string; + heading?: number; + payload?: string; } export interface ObjectIconOptions { - showState: boolean; - showVvi: boolean; - showHealth: boolean; - showHotgroup: boolean; - showUnitIcon: boolean; - showShortLabel: boolean; - showFuel: boolean; - showAmmo: boolean; - showSummary: boolean; - showCallsign: boolean; - rotateToHeading: boolean; - showCluster: boolean; - showAlarmState: boolean; + showState: boolean; + showVvi: boolean; + showHealth: boolean; + showHotgroup: boolean; + showUnitIcon: boolean; + showShortLabel: boolean; + showFuel: boolean; + showAmmo: boolean; + showSummary: boolean; + showCallsign: boolean; + rotateToHeading: boolean; + showCluster: boolean; + showAlarmState: boolean; } export interface GeneralSettings { - prohibitJettison: boolean; - prohibitAA: boolean; - prohibitAG: boolean; - prohibitAfterburner: boolean; - prohibitAirWpn: boolean; + prohibitJettison: boolean; + prohibitAA: boolean; + prohibitAG: boolean; + prohibitAfterburner: boolean; + prohibitAirWpn: boolean; } export interface TACAN { - isOn: boolean; - channel: number; - XY: string; - callsign: string; + isOn: boolean; + channel: number; + XY: string; + callsign: string; } export interface Radio { - frequency: number; - callsign: number; - callsignNumber: number; + frequency: number; + callsign: number; + callsignNumber: number; } export interface Ammo { - quantity: number; - name: string; - guidance: number; - category: number; - missileCategory: number; + quantity: number; + name: string; + guidance: number; + category: number; + missileCategory: number; } export interface Contact { - ID: number; - detectionMethod: number; + ID: number; + detectionMethod: number; } export interface Offset { - x: number; - y: number; - z: number; + x: number; + y: number; + z: number; } export interface DrawingArgument { - argument: number; - value: number; + argument: number; + value: number; } export interface UnitData { - category: string; - markerCategory: string; - ID: number; - alive: boolean; - alarmState: AlarmState; - human: boolean; - controlled: boolean; - coalition: string; - country: number; - name: string; - unitName: string; - callsign: string; - unitID: number; - groupID: number; - groupName: string; - state: string; - task: string; - hasTask: boolean; - position: LatLng; - speed: number; - horizontalVelocity: number; - verticalVelocity: number; - heading: number; - track: number; - isActiveTanker: boolean; - isActiveAWACS: boolean; - onOff: boolean; - followRoads: boolean; - fuel: number; - desiredSpeed: number; - desiredSpeedType: string; - desiredAltitude: number; - desiredAltitudeType: string; - leaderID: number; - formationOffset: Offset; - targetID: number; - targetPosition: LatLng; - ROE: string; - reactionToThreat: string; - emissionsCountermeasures: string; - TACAN: TACAN; - radio: Radio; - generalSettings: GeneralSettings; - ammo: Ammo[]; - contacts: Contact[]; - activePath: LatLng[]; - isLeader: boolean; - operateAs: string; - shotsScatter: number; - shotsIntensity: number; - health: number; - racetrackLength: number; - racetrackAnchor: LatLng; - racetrackBearing: number; - timeToNextTasking: number; - barrelHeight: number; - muzzleVelocity: number; - aimTime: number; - shotsToFire: number; - shotsBaseInterval: number; - shotsBaseScatter: number; - engagementRange: number; - targetingRange: number; - aimMethodRange: number; - acquisitionRange: number; - airborne: boolean; - cargoWeight: number; - drawingArguments: DrawingArgument[]; - customString: string; - customInteger: number; + category: string; + markerCategory: string; + ID: number; + alive: boolean; + alarmState: AlarmState; + human: boolean; + controlled: boolean; + coalition: string; + country: number; + name: string; + unitName: string; + callsign: string; + unitID: number; + groupID: number; + groupName: string; + state: string; + task: string; + hasTask: boolean; + position: LatLng; + speed: number; + horizontalVelocity: number; + verticalVelocity: number; + heading: number; + track: number; + isActiveTanker: boolean; + isActiveAWACS: boolean; + onOff: boolean; + followRoads: boolean; + fuel: number; + desiredSpeed: number; + desiredSpeedType: string; + desiredAltitude: number; + desiredAltitudeType: string; + leaderID: number; + formationOffset: Offset; + targetID: number; + targetPosition: LatLng; + ROE: string; + reactionToThreat: string; + emissionsCountermeasures: string; + TACAN: TACAN; + radio: Radio; + generalSettings: GeneralSettings; + ammo: Ammo[]; + contacts: Contact[]; + activePath: LatLng[]; + isLeader: boolean; + operateAs: string; + shotsScatter: number; + shotsIntensity: number; + health: number; + racetrackLength: number; + racetrackAnchor: LatLng; + racetrackBearing: number; + timeToNextTasking: number; + barrelHeight: number; + muzzleVelocity: number; + aimTime: number; + shotsToFire: number; + shotsBaseInterval: number; + shotsBaseScatter: number; + engagementRange: number; + targetingRange: number; + aimMethodRange: number; + acquisitionRange: number; + airborne: boolean; + cargoWeight: number; + drawingArguments: DrawingArgument[]; + customString: string; + customInteger: number; } export interface LoadoutItemBlueprint { - name: string; - quantity: number; - type: string; - effectiveAgainst?: string; + name: string; + quantity: number; } export interface LoadoutBlueprint { - fuel: number; - items: LoadoutItemBlueprint[]; - roles: string[]; - code: string; - name: string; - enabled: boolean; + items: LoadoutItemBlueprint[]; + roles: string[]; + code: string; + name: string; + enabled: boolean; + isCustom?: boolean; + persistent?: boolean; + payload?: string; } export interface UnitBlueprint { - name: string; - category: string; - enabled: boolean; - coalition: string; - era: string; - label: string; - shortLabel: string; - roles?: string[]; - type?: string; - loadouts?: LoadoutBlueprint[]; - acceptedPayloads?: { [key: string]: { clsids: string; name: string, weight: number }[] }; - filename?: string; - liveries?: { [key: string]: { name: string; countries: string[] } }; - cost?: number; - barrelHeight?: number; - muzzleVelocity?: number; - aimTime?: number; - shotsToFire?: number; - shotsBaseInterval?: number; - shotsBaseScatter?: number; - description?: string; - abilities?: string; - tags?: string; - acquisitionRange?: number; - engagementRange?: number; - targetingRange?: number; - aimMethodRange?: number; - alertnessTimeConstant?: number; - canTargetPoint?: boolean; - canRearm?: boolean; - canAAA?: boolean; - indirectFire?: boolean; - markerFile?: string; - unitWhenGrouped?: string; - mainRole?: string; - length?: number; - carrierFilename?: string; + name: string; + category: string; + enabled: boolean; + coalition: string; + era: string; + label: string; + shortLabel: string; + roles?: string[]; + type?: string; + loadouts?: LoadoutBlueprint[]; + acceptedPayloads?: { [key: string]: { clsid: string; name: string; weight: number }[] }; + filename?: string; + liveries?: { [key: string]: { name: string; countries: string[] } }; + cost?: number; + barrelHeight?: number; + muzzleVelocity?: number; + aimTime?: number; + shotsToFire?: number; + shotsBaseInterval?: number; + shotsBaseScatter?: number; + description?: string; + abilities?: string; + tags?: string; + acquisitionRange?: number; + engagementRange?: number; + targetingRange?: number; + aimMethodRange?: number; + alertnessTimeConstant?: number; + canTargetPoint?: boolean; + canRearm?: boolean; + canAAA?: boolean; + indirectFire?: boolean; + markerFile?: string; + unitWhenGrouped?: string; + mainRole?: string; + length?: number; + carrierFilename?: string; } export interface AirbaseOptions { - name: string; - position: L.LatLng; + name: string; + position: L.LatLng; } export interface AirbaseChartData { - elevation: string; - ICAO: string; - TACAN: string; - runways: AirbaseChartRunwayData[]; + elevation: string; + ICAO: string; + TACAN: string; + runways: AirbaseChartRunwayData[]; } export interface AirbaseChartRunwayHeadingData { - [index: string]: { - magHeading: string; - ILS: string; - }; + [index: string]: { + magHeading: string; + ILS: string; + }; } export interface AirbaseChartRunwayData { - headings: AirbaseChartRunwayHeadingData[]; - length: string; + headings: AirbaseChartRunwayHeadingData[]; + length: string; } export interface ShortcutOptions { - label: string; - keyUpCallback: (e: KeyboardEvent) => void; - keyDownCallback?: (e: KeyboardEvent) => void; - code: string; - altKey?: boolean; - ctrlKey?: boolean; - shiftKey?: boolean; + label: string; + keyUpCallback: (e: KeyboardEvent) => void; + keyDownCallback?: (e: KeyboardEvent) => void; + code: string; + altKey?: boolean; + ctrlKey?: boolean; + shiftKey?: boolean; } export interface ServerStatus { - frameRate: number; - load: number; - elapsedTime: number; - missionTime: DateAndTime["time"]; - connected: boolean; - paused: boolean; + frameRate: number; + load: number; + elapsedTime: number; + missionTime: DateAndTime["time"]; + connected: boolean; + paused: boolean; } export type DrawingPoint = { - x: number; - y: number; + x: number; + y: number; }; export type PolygonPoints = DrawingPoint[] | DrawingPoint; @@ -406,36 +408,36 @@ export type PolygonPoints = DrawingPoint[] | DrawingPoint; export type DrawingPrimitiveType = "TextBox" | "Polygon" | "Line" | "Icon"; export interface Drawing { - name: string; - visible: boolean; - mapX: number; - mapY: number; - layerName: string; - layer: string; - primitiveType: DrawingPrimitiveType; - colorString: string; - fillColorString?: string; - borderThickness?: number; - fontSize?: number; - font?: string; - text?: string; - angle?: number; - radius?: number; - points?: PolygonPoints; - style?: string; - polygonMode?: string; - thickness?: number; - width?: number; - height?: number; - closed?: boolean; - lineMode?: string; - hiddenOnPlanner?: boolean; - file?: string; - scale?: number; + name: string; + visible: boolean; + mapX: number; + mapY: number; + layerName: string; + layer: string; + primitiveType: DrawingPrimitiveType; + colorString: string; + fillColorString?: string; + borderThickness?: number; + fontSize?: number; + font?: string; + text?: string; + angle?: number; + radius?: number; + points?: PolygonPoints; + style?: string; + polygonMode?: string; + thickness?: number; + width?: number; + height?: number; + closed?: boolean; + lineMode?: string; + hiddenOnPlanner?: boolean; + file?: string; + scale?: number; } export enum AlarmState { - RED = 'red', - GREEN = 'green', - AUTO = 'auto' + RED = "red", + GREEN = "green", + AUTO = "auto", } diff --git a/frontend/react/src/olympusapp.ts b/frontend/react/src/olympusapp.ts index 16f4a637..20f24a46 100644 --- a/frontend/react/src/olympusapp.ts +++ b/frontend/react/src/olympusapp.ts @@ -310,11 +310,13 @@ export class OlympusApp { } setState(state: OlympusState, subState: OlympusSubState = NO_SUBSTATE) { + const previousState = this.#state; + const previousSubState = this.#subState; this.#state = state; this.#subState = subState; console.log(`App state set to ${state}, substate ${subState}`); - AppStateChangedEvent.dispatch(state, subState); + AppStateChangedEvent.dispatch(state, subState, previousState, previousSubState); } getState() { diff --git a/frontend/react/src/sessiondata.ts b/frontend/react/src/sessiondata.ts index 763df95e..4164b73b 100644 --- a/frontend/react/src/sessiondata.ts +++ b/frontend/react/src/sessiondata.ts @@ -8,6 +8,7 @@ import { AudioSinksChangedEvent, AudioSourcesChangedEvent, CoalitionAreasChangedEvent, + CustomLoadoutsUpdatedEvent, DrawingsUpdatedEvent, HotgroupsChangedEvent, MapSourceChangedEvent, @@ -16,7 +17,7 @@ import { SessionDataSavedEvent, StarredSpawnsChangedEvent, } from "./events"; -import { SessionData } from "./interfaces"; +import { LoadoutBlueprint, SessionData } from "./interfaces"; import { CoalitionCircle } from "./map/coalitionarea/coalitioncircle"; import { CoalitionPolygon } from "./map/coalitionarea/coalitionpolygon"; import { getApp } from "./olympusapp"; @@ -124,7 +125,7 @@ export class SessionDataManager { HotgroupsChangedEvent.on((hotgroups) => { this.#sessionData.hotgroups = {}; Object.keys(hotgroups).forEach((hotgroup) => { - (this.#sessionData.hotgroups as { [key: string]: number[] })[hotgroup] = hotgroups[hotgroup].map((unit) => unit.ID); + (this.#sessionData.hotgroups as { [key: string]: number[] })[hotgroup] = hotgroups[parseInt(hotgroup)].map((unit) => unit.ID); }); this.#saveSessionData(); }); @@ -146,6 +147,16 @@ export class SessionDataManager { this.#sessionData.mapSource = { id: source }; this.#saveSessionData(); }); + + CustomLoadoutsUpdatedEvent.on((unitName, loadout) => { + // If the loadout is of type isPersistent, update the session data + if (loadout.persistent) { + if (!this.#sessionData.customLoadouts) this.#sessionData.customLoadouts = {}; + if (!this.#sessionData.customLoadouts[unitName]) this.#sessionData.customLoadouts[unitName] = []; + this.#sessionData.customLoadouts[unitName].push({...loadout}); + } + this.#saveSessionData(); + }); }, 200); }); } diff --git a/frontend/react/src/ui/modals/components/modal.tsx b/frontend/react/src/ui/modals/components/modal.tsx index 07ba7f8f..e937cc21 100644 --- a/frontend/react/src/ui/modals/components/modal.tsx +++ b/frontend/react/src/ui/modals/components/modal.tsx @@ -8,8 +8,9 @@ export function Modal(props: { open: boolean; children?: JSX.Element | JSX.Element[]; className?: string; - size?: "sm" | "md" | "lg" | "full"; + size?: "sm" | "md" | "lg" | "full" | "tall"; disableClose?: boolean; + onClose?: () => void; }) { const [splash, setSplash] = useState(Math.ceil(Math.random() * 7)); @@ -54,6 +55,14 @@ export function Modal(props: { ` : "" } + ${ + props.size === "tall" + ? ` + h-[80%] w-[800px] + max-md:h-full max-md:w-full + ` + : "" + } ${props.size === "full" ? "h-full w-full" : ""} `} > @@ -90,7 +99,7 @@ export function Modal(props: { > { - getApp().setState(OlympusState.IDLE); + props.onClose ? props.onClose() : getApp().setState(OlympusState.IDLE); }} />{" "} diff --git a/frontend/react/src/ui/modals/loadoutwizardmodal.tsx b/frontend/react/src/ui/modals/loadoutwizardmodal.tsx new file mode 100644 index 00000000..87214be8 --- /dev/null +++ b/frontend/react/src/ui/modals/loadoutwizardmodal.tsx @@ -0,0 +1,160 @@ +import React, { useEffect, useState } from "react"; +import { Modal } from "./components/modal"; +import { FaMagic, FaStar } from "react-icons/fa"; +import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; +import { faArrowRight } from "@fortawesome/free-solid-svg-icons"; +import { getApp } from "../../olympusapp"; +import { NO_SUBSTATE, OlympusState } from "../../constants/constants"; +import { AppStateChangedEvent, CustomLoadoutsUpdatedEvent, SetLoadoutWizardBlueprintEvent } from "../../events"; +import { WeaponsWizard } from "../panels/components/weaponswizard"; +import { LoadoutBlueprint, LoadoutItemBlueprint, UnitBlueprint } from "../../interfaces"; +import { OlToggle } from "../components/oltoggle"; + +export function LoadoutWizardModal(props: { open: boolean }) { + const [appState, setAppState] = useState(OlympusState.NOT_INITIALIZED); + const [appSubState, setAppSubState] = useState(NO_SUBSTATE); + const [previousState, setPreviousState] = useState(OlympusState.NOT_INITIALIZED); + const [previousSubState, setPreviousSubState] = useState(NO_SUBSTATE); + const [blueprint, setBlueprint] = useState(null as UnitBlueprint | null); + const [isPersistent, setIsPersistent] = useState(false); + + useEffect(() => { + AppStateChangedEvent.on((appState, appSubState, previousState, previousSubState) => { + setAppState(appState); + setAppSubState(appSubState); + setPreviousState(previousState); + setPreviousSubState(previousSubState); + }); + SetLoadoutWizardBlueprintEvent.on((blueprint) => { + setBlueprint(blueprint); + }); + }, []); + + useEffect(() => { + // Clear blueprint when modal is closed + if (!props.open) { + setBlueprint(null); + } + }, [props.open]); + + const [selectedWeapons, setSelectedWeapons] = useState({} as { [key: string]: { clsid: string; name: string; weight: number } }); + const [loadoutName, setLoadoutName] = useState("New loadout"); + const [loadoutRole, setLoadoutRole] = useState("Custom"); + + useEffect(() => { + setSelectedWeapons({}); + }, [props.open]); + + // If "New Loadout" already exists in the blueprint loadouts, append a number to make it unique + useEffect(() => { + if (!blueprint) return; + let name = "New loadout"; + let counter = 1; + const existingLoadoutNames = blueprint.loadouts?.map((loadout) => loadout.name) || []; + + while (existingLoadoutNames.includes(name)) { + name = `New loadout ${counter}`; + counter++; + } + + setLoadoutName(name); + }, [blueprint]); + + return ( + getApp().setState(previousState, previousSubState)}> +
+ +
Loadout wizard
+
+ +
+
+ +
Keep for the rest of the session
+ setIsPersistent(!isPersistent)} /> +
+ +
+
+ ); +} diff --git a/frontend/react/src/ui/panels/components/weaponswizard.tsx b/frontend/react/src/ui/panels/components/weaponswizard.tsx index 6aede9fe..ad6af834 100644 --- a/frontend/react/src/ui/panels/components/weaponswizard.tsx +++ b/frontend/react/src/ui/panels/components/weaponswizard.tsx @@ -1,20 +1,38 @@ -import React, { useState } from "react"; -import { OlDropdown, OlDropdownItem } from "../../components/oldropdown"; +import React, { useEffect, useState } from "react"; import { FaArrowsRotate, FaTrash, FaXmark } from "react-icons/fa6"; import { OlSearchBar } from "../../components/olsearchbar"; -import { OlCheckbox } from "../../components/olcheckbox"; import { OlToggle } from "../../components/oltoggle"; export function WeaponsWizard(props: { - selectedWeapons: { [key: string]: { clsids: string; name: string; weight: number } }; - setSelectedWeapons: (weapons: { [key: string]: { clsids: string; name: string; weight: number } }) => void; - weaponsByPylon: { [key: string]: { clsids: string; name: string; weight: number }[] }; + selectedWeapons: { [key: string]: { clsid: string; name: string; weight: number } }; + setSelectedWeapons: (weapons: { [key: string]: { clsid: string; name: string; weight: number } }) => void; + weaponsByPylon: { [key: string]: { clsid: string; name: string; weight: number }[] }; + loadoutName: string; + setLoadoutName: (name: string) => void; + loadoutRole: string; + setLoadoutRole: (role: string) => void; }) { const [searchText, setSearchText] = useState(""); const [selectedPylons, setSelectedPylons] = useState([]); const [autofillPylons, setAutofillPylons] = useState(false); + const [fillEmptyOnly, setFillEmptyOnly] = useState(true); + const [weaponLetters, setWeaponLetters] = useState<{ [key: string]: string }>({}); // Letter to weapon name mapping + const [hoveredWeapon, setHoveredWeapon] = useState(""); + + useEffect(() => { + // If autofill is enabled, clear selected pylons + if (autofillPylons) { + setSelectedPylons([]); + } + }, [autofillPylons]); + + useEffect(() => { + // Clear search text when weaponsByPylon changes + setSearchText(""); + setSelectedPylons([]); + }, [props.weaponsByPylon]); // Find the weapons that are availabile in all the selected pylons, meaning the intersection of the weapons in each pylon - let availableWeapons: { clsids: string; name: string; weight: number }[] = []; + let availableWeapons: { clsid: string; name: string; weight: number }[] = []; if (autofillPylons) { // If autofill is enabled, show all weapons availableWeapons = Object.values(props.weaponsByPylon).flat(); @@ -39,91 +57,266 @@ export function WeaponsWizard(props: { availableWeapons = availableWeapons.filter((weapon) => weapon.name.toLowerCase().includes(searchText.toLowerCase())); } + // If autofill is enabled and fillEmptyOnly is enabled, remove weapons that have no compatible empty pylons + if (autofillPylons && fillEmptyOnly) { + availableWeapons = availableWeapons.filter((weapon) => { + // Check if there is at least one pylon that is compatible with this weapon and is empty + return Object.keys(props.weaponsByPylon).some((pylon) => { + const weaponsInPylon = props.weaponsByPylon[pylon]; + return weaponsInPylon.some((w) => w.name === weapon.name) && !props.selectedWeapons[pylon]; + }); + }); + } + + // Assign a letter to each indiviual type of weapon selected in selectedWeapons for display in the pylon selection + // Find the first unused letter + Object.values(props.selectedWeapons).forEach((weapon) => { + if (Object.entries(weaponLetters).findIndex(([letter, name]) => name === weapon.name) === -1) { + // Find the first unused letter starting from A + let currentLetter = "A"; + while (weaponLetters[currentLetter]) { + currentLetter = String.fromCharCode(currentLetter.charCodeAt(0) + 1); + } + weaponLetters[currentLetter] = weapon.name; + currentLetter = String.fromCharCode(currentLetter.charCodeAt(0) + 1); + } + }); + + // Remove letters for weapons that are no longer selected + Object.entries(weaponLetters).forEach(([letter, name]) => { + if (Object.values(props.selectedWeapons).findIndex((weapon) => weapon.name === name) === -1) { + delete weaponLetters[letter]; + } + }); + + if (JSON.stringify(weaponLetters) !== JSON.stringify(weaponLetters)) setWeaponLetters({ ...weaponLetters }); + + // List of very bright and distinct colors + const colors = { + A: "#FF5733", + B: "#33FF57", + C: "#3357FF", + D: "#F333FF", + E: "#33FFF5", + F: "#F5FF33", + G: "#FF33A8", + H: "#A833FF", + I: "#33FFA8", + J: "#FFA833", + K: "#33A8FF", + }; + return (
-
-
- {Object.keys(props.weaponsByPylon).map((pylon) => ( -
+
setHoveredWeapon("")}> +
+
+
Loadout Name
+ props.setLoadoutName(e.target.value)} + className={` + rounded-md border border-gray-300 bg-gray-800 p-2 + text-sm text-white + `} + /> +
+
+
Loadout Role
+ props.setLoadoutRole(e.target.value)} + className={` + rounded-md border border-gray-300 bg-gray-800 p-2 + text-sm text-white + `} + /> +
+
+ Select weapons for each pylon +
+
+ {/* Draw an airplane seen from the front using only gray lines */} +
{ - if (autofillPylons) return; - if (selectedPylons.includes(pylon)) { - setSelectedPylons(selectedPylons.filter((p) => p !== pylon)); - } else { - setSelectedPylons([...selectedPylons, pylon]); - } - }} - > -
{pylon}
-
- {props.selectedWeapons[pylon] ? ( -
-
-
- ) : ( -
- )} -
+ >
+
+
- ))} -
- {/* Buttons to select/deselect all pylons, clear all weapons and remove weapons from selected pylons */} -
-
- {selectedPylons.length > 0 && ( - <> +
+ {Object.keys(props.weaponsByPylon).map((pylon) => { + let weapon = props.selectedWeapons[pylon]; + let letter = Object.entries(weaponLetters).find(([letter, name]) => name === weapon?.name)?.[0] || ""; + // If the currently hovered weapon is compatible with this pylon, show "Hovered" else "Not Hovered" + let isHovered = props.weaponsByPylon[pylon].some((w) => w.name === hoveredWeapon); + return ( +
+
{ + if (autofillPylons) return; + if (selectedPylons.includes(pylon)) { + setSelectedPylons(selectedPylons.filter((p) => p !== pylon)); + } else { + setSelectedPylons([...selectedPylons, pylon]); + } + }} + > +
{pylon}
+ +
+ {props.selectedWeapons[pylon] ? ( +
+ {/* Show the letter of the group the weapon belongs to from weaponLetters */} + + {letter} + +
+ ) : ( +
+ )} +
+
+ ); + })} +
+
+ {/* List all the groups from weaponLetters */} +
+ {Object.entries(weaponLetters).map(([letter, weapon]) => ( +
+ + {letter}: + + {weapon} +
+ ))} +
+ + {/* Buttons to select/deselect all pylons, clear all weapons and remove weapons from selected pylons */} +
+
+ {selectedPylons.length > 0 && ( + <> + + + { + /* Checjk if any of the selected pylons have a weapon selected */ + props.selectedWeapons && selectedPylons.some((pylon) => props.selectedWeapons[pylon] !== undefined) && ( + + ) + } + + )} + {props.selectedWeapons && Object.keys(props.selectedWeapons).length > 0 && ( - - { - /* Checjk if any of the selected pylons have a weapon selected */ - props.selectedWeapons && selectedPylons.some((pylon) => props.selectedWeapons[pylon] !== undefined) && ( - - ) - } - - )} - {props.selectedWeapons && Object.keys(props.selectedWeapons).length > 0 && ( - - )} + )} +
@@ -193,6 +344,17 @@ export function WeaponsWizard(props: { }} />
+ {autofillPylons && ( +
+ Only fill empty pylons + { + setFillEmptyOnly(!fillEmptyOnly); + }} + /> +
+ )} @@ -202,13 +364,35 @@ export function WeaponsWizard(props: { border-gray-700 px-2 `} > - {availableWeapons.length === 0 ? ( - selectedPylons.length === 0 ? ( -
No pylons selected
- ) : ( -
No weapons compatible with all selected pylons
- ) - ) : ( + {selectedPylons.length === 0 && !autofillPylons && ( +
+ No pylons selected +
+ )} + {availableWeapons.length === 0 && selectedPylons.length !== 0 && !autofillPylons && ( +
+ No weapons compatible with all selected pylons +
+ )} + {availableWeapons.length === 0 && selectedPylons.length === 0 && autofillPylons && ( +
+ No empty pylons available +
+ )} + + {availableWeapons.length !== 0 && availableWeapons.map((weapon) => (
{ const weaponsInPylon = props.weaponsByPylon[pylon]; + if (fillEmptyOnly && props.selectedWeapons[pylon]) { + // If "Only fill empty pylons" is enabled, skip filled pylons + return; + } if (weaponsInPylon.some((w) => w.name === weapon.name)) { newSelectedWeapons[pylon] = weapon; } @@ -233,6 +421,8 @@ export function WeaponsWizard(props: { setSelectedPylons([]); } }} + onMouseEnter={() => setHoveredWeapon(weapon.name)} + onMouseLeave={() => setHoveredWeapon("")} className={` cursor-pointer rounded-md p-1 text-sm hover:bg-gray-700 @@ -240,8 +430,7 @@ export function WeaponsWizard(props: { > {weapon.name}
- )) - )} + ))}
diff --git a/frontend/react/src/ui/panels/unitspawnmenu.tsx b/frontend/react/src/ui/panels/unitspawnmenu.tsx index 54951ee0..a1992d2e 100644 --- a/frontend/react/src/ui/panels/unitspawnmenu.tsx +++ b/frontend/react/src/ui/panels/unitspawnmenu.tsx @@ -5,7 +5,7 @@ import { OlNumberInput } from "../components/olnumberinput"; import { OlLabelToggle } from "../components/ollabeltoggle"; import { OlRangeSlider } from "../components/olrangeslider"; import { OlDropdownItem, OlDropdown } from "../components/oldropdown"; -import { LoadoutBlueprint, SpawnRequestTable, UnitBlueprint } from "../../interfaces"; +import { LoadoutBlueprint, SpawnRequestTable, UnitBlueprint, UnitSpawnTable } from "../../interfaces"; import { OlStateButton } from "../components/olstatebutton"; import { Coalition } from "../../types/types"; import { getApp } from "../../olympusapp"; @@ -17,11 +17,10 @@ import { faArrowLeft, faStar } from "@fortawesome/free-solid-svg-icons"; import { OlStringInput } from "../components/olstringinput"; import { countryCodes } from "../data/codes"; import { OlAccordion } from "../components/olaccordion"; -import { AppStateChangedEvent, SpawnHeadingChangedEvent } from "../../events"; +import { AppStateChangedEvent, CustomLoadoutsUpdatedEvent, SetLoadoutWizardBlueprintEvent, SpawnHeadingChangedEvent } from "../../events"; import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; -import { FaQuestionCircle } from "react-icons/fa"; +import { FaMagic, FaQuestionCircle } from "react-icons/fa"; import { OlExpandingTooltip } from "../components/olexpandingtooltip"; -import { WeaponsWizard } from "./components/weaponswizard"; import { LoadoutViewer } from "./components/loadoutviewer"; enum OpenAccordion { @@ -59,7 +58,7 @@ export function UnitSpawnMenu(props: { const [spawnAltitudeType, setSpawnAltitudeType] = useState(false); const [spawnLiveryID, setSpawnLiveryID] = useState(""); const [spawnSkill, setSpawnSkill] = useState("High"); - const [selectedWeapons, setSelectedWeapons] = useState({} as { [key: string]: { clsids: string; name: string; weight: number } }); + const [quickAccessName, setQuickAccessName] = useState("Preset 1"); const [key, setKey] = useState(""); const [spawnRequestTable, setSpawnRequestTable] = useState(null as null | SpawnRequestTable); @@ -70,13 +69,16 @@ export function UnitSpawnMenu(props: { useEffect(() => { setAppState(getApp()?.getState()); AppStateChangedEvent.on((state, subState) => setAppState(state)); + CustomLoadoutsUpdatedEvent.on((unitName, loadout) => { + setSpawnRole(loadout.roles[0]); + setSpawnLoadout(loadout); + }); }, []); useEffect(() => { setSpawnRole(""); setSpawnLoadout(null); setSpawnLiveryID(""); - setSelectedWeapons({}); }, [props.blueprint]); /* When the menu is opened show the unit preview on the map as a cursor */ @@ -115,16 +117,25 @@ export function UnitSpawnMenu(props: { /* Callback and effect to update the spawn request table */ const updateSpawnRequestTable = useCallback(() => { if (props.blueprint !== null) { + const loadoutCode = spawnLoadout ? (props.blueprint.loadouts?.find((loadout) => loadout.name === spawnLoadout.name)?.code ?? "") : ""; + const loadoutPayload = spawnLoadout + ? (props.blueprint.loadouts?.find((loadout) => loadout.name === spawnLoadout.name)?.payload ?? undefined) + : undefined; + + const unitTable: UnitSpawnTable = { + unitType: props.blueprint?.name, + location: props.latlng ?? new LatLng(0, 0), // This will be filled when the user clicks on the map to spawn the unit + skill: spawnSkill, + liveryID: spawnLiveryID, + altitude: ftToM(spawnAltitude), + loadout: loadoutCode, + }; + + if (loadoutPayload) unitTable.payload = loadoutPayload; + setSpawnRequestTable({ category: props.blueprint?.category, - unit: { - unitType: props.blueprint?.name, - location: props.latlng ?? new LatLng(0, 0), // This will be filled when the user clicks on the map to spawn the unit - skill: spawnSkill, - liveryID: spawnLiveryID, - altitude: ftToM(spawnAltitude), - loadout: props.blueprint?.loadouts?.find((loadout) => loadout.name === spawnLoadout?.name)?.code ?? "", - }, + unit: unitTable, amount: spawnNumber, coalition: spawnCoalition, }); @@ -192,10 +203,49 @@ export function UnitSpawnMenu(props: { /* Initialize the role */ let allRoles = props.blueprint?.loadouts?.flatMap((loadout) => loadout.roles).filter((role) => role !== "No task"); - let mainRole = roles[0]; + + // If there are loadouts with Custom role, include Custom in the role selection + const hasCustomRoleLoadouts = props.blueprint?.loadouts?.some((loadout) => loadout.roles.includes("Custom")); + if (hasCustomRoleLoadouts && allRoles) allRoles.push("Custom"); + + // If there are custom loadouts, select "Custom" as the main role + let mainRole = hasCustomRoleLoadouts ? "Custom" : roles[0]; if (allRoles !== undefined) mainRole = mode(allRoles); spawnRole === "" && roles.length > 0 && setSpawnRole(mainRole); + // Filter the loadouts based on the selected role + const filteredLoadouts = props.blueprint?.loadouts?.filter((loadout) => loadout.roles.includes(spawnRole)); + + // Order the loadouts so that custom loadouts appear first and "Empty loadout" appears last + if (filteredLoadouts) { + filteredLoadouts.sort((a, b) => { + if (a.isCustom && !b.isCustom) return -1; + if (!a.isCustom && b.isCustom) return 1; + if (a.name === "Empty loadout") return 1; + if (b.name === "Empty loadout") return -1; + return 0; + }); + } + + /* Effect to reset the loadout if the role changes */ + useEffect(() => { + // If the current loadout is not in the filtered loadouts, reset it + if (spawnLoadout && filteredLoadouts && !filteredLoadouts.includes(spawnLoadout)) { + setSpawnLoadout(null); + } + }, [spawnRole, props.blueprint]); + + /* Initialize the loadout */ + const initializeLoadout = useCallback(() => { + if (spawnLoadout && filteredLoadouts && filteredLoadouts.includes(spawnLoadout)) return; + if (filteredLoadouts && filteredLoadouts.length > 0) { + if (filteredLoadouts.filter((loadout) => loadout.name !== "Empty loadout").length > 0) + setSpawnLoadout(filteredLoadouts.filter((loadout) => loadout.name !== "Empty loadout")[0]); + else setSpawnLoadout(filteredLoadouts[0]); + } + }, [filteredLoadouts, spawnLoadout]); + useEffect(initializeLoadout, [filteredLoadouts]); + return ( <> {props.compact ? ( @@ -207,7 +257,7 @@ export function UnitSpawnMenu(props: { `} >
-
+
- Quick access:{" "} + Quick access:
{ @@ -372,6 +423,87 @@ export function UnitSpawnMenu(props: { })}
+
+ + Loadout + + ( + + )} + tooltipRelativeToParent={true} + > + {filteredLoadouts?.map((loadout) => { + return ( + { + setSpawnLoadout(loadout); + }} + className={` + w-full + `} + > + +
+ {loadout.name} +
+
+
+ ); + })} +
+
+
+ +
)} {props.blueprint ? : } - - { - setOpenAccordion(openAccordion === OpenAccordion.LOADOUT ? OpenAccordion.NONE : OpenAccordion.LOADOUT); - }} - open={openAccordion === OpenAccordion.LOADOUT} - title="Loadout wizard" - > - - - {spawnLoadout && spawnLoadout.items.length > 0 && ( { @@ -1056,6 +1173,75 @@ export function UnitSpawnMenu(props: { })}
+
+ + Loadout + + ( + + )} + tooltipRelativeToParent={true} + > + {filteredLoadouts?.map((loadout) => { + return ( + { + setSpawnLoadout(loadout); + }} + className={`w-full`} + > + +
{loadout.name}
+
+
+ ); + })} +
+
+
+ +
Spawn heading @@ -1069,13 +1255,7 @@ export function UnitSpawnMenu(props: { my-auto `} />{" "} -
- Drag to change -
+
Drag to change
@@ -1128,38 +1308,8 @@ export function UnitSpawnMenu(props: { >
-
- -
- { - setOpenAccordion(openAccordion === OpenAccordion.LOADOUT ? OpenAccordion.NONE : OpenAccordion.LOADOUT); - }} - open={openAccordion === OpenAccordion.LOADOUT} - title="Loadout wizard" - > - - - {spawnLoadout && spawnLoadout.items.length > 0 && ( - { - setShowLoadout(!showLoadout); - }} - open={showLoadout} - title="Loadout" - > - - - )} +
Loadout
+ {spawnLoadout && }
{props.airbase && ( diff --git a/frontend/react/src/ui/ui.tsx b/frontend/react/src/ui/ui.tsx index 858b5425..48a12bff 100644 --- a/frontend/react/src/ui/ui.tsx +++ b/frontend/react/src/ui/ui.tsx @@ -7,7 +7,7 @@ import { UnitControlMenu } from "./panels/unitcontrolmenu"; import { MainMenu } from "./panels/mainmenu"; import { SideBar } from "./panels/sidebar"; import { OptionsMenu } from "./panels/optionsmenu"; -import { NO_SUBSTATE, OlympusState, OlympusSubState, OptionsSubstate, UnitControlSubState } from "../constants/constants"; +import { NO_SUBSTATE, OlympusState, OlympusSubState, OptionsSubstate, SpawnSubState, UnitControlSubState } from "../constants/constants"; import { getApp, setupApp } from "../olympusapp"; import { LoginModal } from "./modals/loginmodal"; @@ -32,6 +32,7 @@ import { WarningModal } from "./modals/warningmodal"; import { TrainingModal } from "./modals/trainingmodal"; import { AdminModal } from "./modals/adminmodal"; import { ImageOverlayModal } from "./modals/imageoverlaymodal"; +import { LoadoutWizardModal } from "./modals/loadoutwizardmodal"; export function UI() { const [appState, setAppState] = useState(OlympusState.NOT_INITIALIZED); @@ -76,6 +77,7 @@ export function UI() { + )} diff --git a/frontend/react/src/unit/databases/unitdatabase.ts b/frontend/react/src/unit/databases/unitdatabase.ts index f2bc0ffd..8193a7c0 100644 --- a/frontend/react/src/unit/databases/unitdatabase.ts +++ b/frontend/react/src/unit/databases/unitdatabase.ts @@ -1,12 +1,26 @@ import { getApp } from "../../olympusapp"; import { GAME_MASTER } from "../../constants/constants"; import { UnitBlueprint } from "../../interfaces"; -import { UnitDatabaseLoadedEvent } from "../../events"; +import { SessionDataLoadedEvent, UnitDatabaseLoadedEvent } from "../../events"; export class UnitDatabase { blueprints: { [key: string]: UnitBlueprint } = {}; - constructor() {} + constructor() { + SessionDataLoadedEvent.on((sessionData) => { + // Check if the sessionData customloadouts contains any loadouts for units, and if so, update the blueprints + if (sessionData.customLoadouts) { + for (let unitName in sessionData.customLoadouts) { + if (this.blueprints[unitName]) { + if (!this.blueprints[unitName].loadouts) this.blueprints[unitName].loadouts = []; + sessionData.customLoadouts[unitName].forEach((loadout) => { + this.blueprints[unitName].loadouts?.push(loadout); + }); + } + } + } + }); + } load(url: string, category?: string) { if (url !== "") { @@ -204,7 +218,7 @@ export class UnitDatabase { getLoadoutNamesByRole(name: string, role: string) { var filteredBlueprints = this.getBlueprints(); var loadoutsByRole: string[] = []; - var loadouts = filteredBlueprints[name].loadouts; + var loadouts = filteredBlueprints[name as any].loadouts; if (loadouts) { for (let loadout of loadouts) { if (loadout.roles.includes(role) || loadout.roles.includes("")) {