Added keybinding menu and server side profiles

This commit is contained in:
Davide Passoni 2024-11-11 17:07:12 +01:00
parent 62af0f74e7
commit 68980651dc
18 changed files with 959 additions and 805 deletions

View File

@ -11,13 +11,13 @@ if (!window.AudioBuffer.prototype.copyFromChannel) {
export class Effect {
name: string;
context: AudioContext;
context: AudioContext | OfflineAudioContext;
input: GainNode;
effect: GainNode | BiquadFilterNode | null;
bypassed: boolean;
output: GainNode;
constructor(context: AudioContext) {
constructor(context: AudioContext | OfflineAudioContext) {
this.name = "effect";
this.context = context;
this.input = this.context.createGain();
@ -45,14 +45,14 @@ export class Effect {
}
export class Sample {
context: AudioContext;
context: AudioContext | OfflineAudioContext;
buffer: AudioBufferSourceNode;
sampleBuffer: AudioBuffer | null;
rawBuffer: ArrayBuffer | null;
loaded: boolean;
output: GainNode;
constructor(context: AudioContext) {
constructor(context: AudioContext | OfflineAudioContext) {
this.context = context;
this.buffer = this.context.createBufferSource();
this.buffer.start();
@ -94,7 +94,7 @@ export class Sample {
}
export class AmpEnvelope {
context: AudioContext;
context: AudioContext | OfflineAudioContext;
output: GainNode;
partials: any[];
velocity: number;
@ -104,7 +104,7 @@ export class AmpEnvelope {
#sustain: number;
#release: number;
constructor(context: AudioContext, gain: number = 1) {
constructor(context: AudioContext | OfflineAudioContext, gain: number = 1) {
this.context = context;
this.output = this.context.createGain();
this.output.gain.value = gain;
@ -179,7 +179,7 @@ export class AmpEnvelope {
}
export class Voice {
context: AudioContext;
context: AudioContext | OfflineAudioContext;
type: string;
value: number;
gain: number;
@ -187,7 +187,7 @@ export class Voice {
partials: any[];
ampEnvelope: AmpEnvelope;
constructor(context: AudioContext, gain: number = 0.1, type: string = "sawtooth") {
constructor(context: AudioContext | OfflineAudioContext, gain: number = 0.1, type: string = "sawtooth") {
this.context = context;
this.type = type;
this.value = -1;
@ -266,7 +266,7 @@ export class Voice {
export class Noise extends Voice {
#length: number;
constructor(context: AudioContext, gain: number) {
constructor(context: AudioContext | OfflineAudioContext, gain: number) {
super(context, gain);
this.#length = 2;
}
@ -307,7 +307,7 @@ export class Noise extends Voice {
}
export class Filter extends Effect {
constructor(context: AudioContext, type: string = "lowpass", cutoff: number = 1000, resonance: number = 0.9) {
constructor(context: AudioContext | OfflineAudioContext, type: string = "lowpass", cutoff: number = 1000, resonance: number = 0.9) {
super(context);
this.name = "filter";
if (this.effect instanceof BiquadFilterNode) {

View File

@ -38,12 +38,24 @@ export class AudioManager {
constructor() {
ConfigLoadedEvent.on((config: OlympusConfig) => {
config.WSPort ? this.setPort(config.WSPort) : this.setEndpoint(config.WSEndpoint);
config.audio.WSPort ? this.setPort(config.audio.WSPort) : this.setEndpoint(config.audio.WSEndpoint);
});
setInterval(() => {
this.#syncRadioSettings();
}, 1000);
let PTTKeys = ["KeyZ", "KeyX", "KeyC", "KeyV", "KeyB", "KeyN", "KeyM", "KeyK", "KeyL"];
PTTKeys.forEach((key, idx) => {
getApp()
.getShortcutManager()
.addShortcut(`PTT${idx}Active`, {
label: `PTT ${idx} active`,
keyDownCallback: () => this.getSinks()[idx]?.setPtt(true),
keyUpCallback: () => this.getSinks()[idx]?.setPtt(false),
code: key
});
});
}
start() {

View File

@ -276,7 +276,7 @@ export enum OlympusState {
OPTIONS = "Options",
AUDIO = "Audio",
AIRBASE = "Airbase",
GAME_MASTER = "Game master",
GAME_MASTER = "Game master"
}
export const NO_SUBSTATE = "No substate";
@ -310,7 +310,12 @@ export enum SpawnSubState {
SPAWN_EFFECT = "Effect",
}
export type OlympusSubState = DrawSubState | JTACSubState | SpawnSubState | string;
export enum OptionsSubstate {
NO_SUBSTATE = "No substate",
KEYBIND = "Keybind"
}
export type OlympusSubState = DrawSubState | JTACSubState | SpawnSubState | OptionsSubstate | string;
export const IADSTypes = ["AAA", "SAM Site", "Radar (EWR)"];
export const IADSDensities: { [key: string]: number } = {

View File

@ -5,6 +5,7 @@ import { CommandModeOptions, OlympusConfig, ServerStatus } from "./interfaces";
import { CoalitionCircle } from "./map/coalitionarea/coalitioncircle";
import { CoalitionPolygon } from "./map/coalitionarea/coalitionpolygon";
import { Airbase } from "./mission/airbase";
import { Shortcut } from "./shortcut/shortcut";
import { MapHiddenTypes, MapOptions } from "./types/types";
import { ContextAction } from "./unit/contextaction";
import { ContextActionSet } from "./unit/contextactionset";
@ -108,6 +109,45 @@ export class HideMenuEvent {
}
}
export class ShortcutsChangedEvent {
static on(callback: (shortcuts: {[key: string]: Shortcut}) => void) {
document.addEventListener(this.name, (ev: CustomEventInit) => {
callback(ev.detail.shortcuts);
});
}
static dispatch(shortcuts: {[key: string]: Shortcut}) {
document.dispatchEvent(new CustomEvent(this.name, {detail: {shortcuts}}));
console.log(`Event ${this.name} dispatched`);
}
}
export class ShortcutChangedEvent {
static on(callback: (shortcut: Shortcut) => void) {
document.addEventListener(this.name, (ev: CustomEventInit) => {
callback(ev.detail.shortcut);
});
}
static dispatch(shortcut: Shortcut) {
document.dispatchEvent(new CustomEvent(this.name, {detail: {shortcut}}));
console.log(`Event ${this.name} dispatched`);
}
}
export class BindShortcutRequestEvent {
static on(callback: (shortcut: Shortcut) => void) {
document.addEventListener(this.name, (ev: CustomEventInit) => {
callback(ev.detail.shortcut);
});
}
static dispatch(shortcut: Shortcut) {
document.dispatchEvent(new CustomEvent(this.name, {detail: {shortcut}}));
console.log(`Event ${this.name} dispatched`);
}
}
/************** Map events ***************/
export class HiddenTypesChangedEvent {
static on(callback: (hiddenTypes: MapHiddenTypes) => void) {

View File

@ -1,26 +1,37 @@
import { LatLng } from "leaflet";
import { MapOptions } from "./types/types";
export interface OlympusConfig {
port: number;
elevationProvider: {
provider: string;
username: string | null;
password: string | null;
};
mapLayers: {
[key: string]: {
urlTemplate: string;
minZoom: number;
maxZoom: number;
attribution?: string;
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;
};
};
mapMirrors: {
[key: string]: string;
audio: {
SRSPort: number;
WSPort?: number;
WSEndpoint?: string;
};
SRSPort: number;
WSPort?: number;
WSEndpoint?: string;
profiles?: ProfileOptions;
}
export interface ProfileOptions {
mapOptions: MapOptions,
shortcuts: {[key: string]: ShortcutOptions}
}
export interface ContextMenuOption {
@ -284,25 +295,13 @@ export interface AirbaseChartRunwayData {
}
export interface ShortcutOptions {
altKey?: boolean;
callback: CallableFunction;
ctrlKey?: boolean;
name?: string;
shiftKey?: boolean;
}
export interface ShortcutKeyboardOptions extends ShortcutOptions {
label: string;
keyUpCallback: (e: KeyboardEvent) => void;
keyDownCallback?: (e: KeyboardEvent) => void;
code: string;
event?: "keydown" | "keyup";
}
export interface ShortcutMouseOptions extends ShortcutOptions {
button: number;
event: "mousedown" | "mouseup";
}
export interface Manager {
add: CallableFunction;
altKey?: boolean;
ctrlKey?: boolean;
shiftKey?: boolean;
}
export interface ServerStatus {

View File

@ -22,6 +22,7 @@ import {
UnitControlSubState,
ContextActionTarget,
ContextActionType,
ContextActions,
} from "../constants/constants";
import { CoalitionPolygon } from "./coalitionarea/coalitionpolygon";
import { MapHiddenTypes, MapOptions } from "../types/types";
@ -88,9 +89,7 @@ export class Map extends L.Map {
#panRight: boolean = false;
#panUp: boolean = false;
#panDown: boolean = false;
/* Keyboard state */
#isShiftKeyDown: boolean = false;
#panFast: boolean = false;
/* Center on unit target */
#centeredUnit: Unit | null = null;
@ -193,9 +192,6 @@ export class Map extends L.Map {
this.on("mousemove", (e: any) => this.#onMouseMove(e));
this.on("keydown", (e: any) => this.#onKeyDown(e));
this.on("keyup", (e: any) => this.#onKeyUp(e));
this.on("move", (e: any) => this.#onMapMove(e));
/* Custom touch events for touchscreen support */
@ -239,8 +235,8 @@ export class Map extends L.Map {
let layerSet = false;
/* First load the map mirrors */
if (config.mapMirrors) {
let mapMirrors = config.mapMirrors;
if (config.frontend.mapMirrors) {
let mapMirrors = config.frontend.mapMirrors;
this.#mapMirrors = {
...this.#mapMirrors,
...mapMirrors,
@ -255,8 +251,8 @@ export class Map extends L.Map {
}
/* Then load the map layers */
if (config.mapLayers) {
let mapLayers = config.mapLayers;
if (config.frontend.mapLayers) {
let mapLayers = config.frontend.mapLayers;
this.#mapLayers = {
...this.#mapLayers,
...mapLayers,
@ -289,12 +285,118 @@ export class Map extends L.Map {
if (this.#panUp || this.#panDown || this.#panRight || this.#panLeft)
this.panBy(
new L.Point(
((this.#panLeft ? -1 : 0) + (this.#panRight ? 1 : 0)) * this.defaultPanDelta * (this.#isShiftKeyDown ? 3 : 1),
((this.#panUp ? -1 : 0) + (this.#panDown ? 1 : 0)) * this.defaultPanDelta * (this.#isShiftKeyDown ? 3 : 1)
((this.#panLeft ? -1 : 0) + (this.#panRight ? 1 : 0)) * this.defaultPanDelta * (this.#panFast ? 3 : 1),
((this.#panUp ? -1 : 0) + (this.#panDown ? 1 : 0)) * this.defaultPanDelta * (this.#panFast ? 3 : 1)
)
);
}, 20);
getApp()
.getShortcutManager()
.addShortcut("toggleUnitLabels", {
label: "Hide/show labels",
keyUpCallback: () => this.setOption("showUnitLabels", !this.getOptions().showUnitLabels),
code: "KeyL",
})
.addShortcut("toggleAcquisitionRings", {
label: "Hide/show acquisition rings",
keyUpCallback: () => this.setOption("showUnitsAcquisitionRings", !this.getOptions().showUnitsAcquisitionRings),
code: "KeyE",
})
.addShortcut("toggleEngagementRings", {
label: "Hide/show engagement rings",
keyUpCallback: () => this.setOption("showUnitsEngagementRings", !this.getOptions().showUnitsEngagementRings),
code: "KeyQ",
})
.addShortcut("toggleHideShortEngagementRings", {
label: "Hide/show short range rings",
keyUpCallback: () => this.setOption("hideUnitsShortRangeRings", !this.getOptions().hideUnitsShortRangeRings),
code: "KeyR",
})
.addShortcut("toggleDetectionLines", {
label: "Hide/show detection lines",
keyUpCallback: () => this.setOption("showUnitTargets", !this.getOptions().showUnitTargets),
code: "KeyF",
})
.addShortcut("toggleGroupMembers", {
label: "Hide/show group members",
keyUpCallback: () => this.setOption("hideGroupMembers", !this.getOptions().hideGroupMembers),
code: "KeyG",
})
.addShortcut("toggleRelativePositions", {
label: "Toggle group movement mode",
keyUpCallback: () => this.setOption("keepRelativePositions", !this.getOptions().keepRelativePositions),
code: "KeyP",
})
.addShortcut("increaseCameraZoom", {
label: "Increase camera zoom",
altKey: true,
keyUpCallback: () => this.increaseCameraZoom(),
code: "Equal",
})
.addShortcut("decreaseCameraZoom", {
label: "Decrease camera zoom",
altKey: true,
keyUpCallback: () => this.decreaseCameraZoom(),
code: "Minus",
});
for (let contextActionName in ContextActions) {
if (ContextActions[contextActionName].getOptions().hotkey) {
getApp()
.getShortcutManager()
.addShortcut(`${contextActionName}Hotkey`, {
label: ContextActions[contextActionName].getLabel(),
code: ContextActions[contextActionName].getOptions().hotkey,
shiftKey: true,
keyUpCallback: () => {
const contextActionSet = this.getContextActionSet();
if (
getApp().getState() === OlympusState.UNIT_CONTROL &&
contextActionSet &&
ContextActions[contextActionName].getId() in contextActionSet.getContextActions()
) {
if (ContextActions[contextActionName].getOptions().executeImmediately) ContextActions[contextActionName].executeCallback();
else this.setContextAction(ContextActions[contextActionName]);
}
},
});
}
}
/* Map panning shortcuts */
getApp()
.getShortcutManager()
.addShortcut(`panUp`, {
label: "Pan map up",
keyUpCallback: (ev: KeyboardEvent) => this.#panUp = false,
keyDownCallback: (ev: KeyboardEvent) => this.#panUp = true,
code: "KeyW",
})
.addShortcut(`panDown`, {
label: "Pan map down",
keyUpCallback: (ev: KeyboardEvent) => this.#panDown = false,
keyDownCallback: (ev: KeyboardEvent) => this.#panDown = true,
code: "KeyS",
})
.addShortcut(`panLeft`, {
label: "Pan map left",
keyUpCallback: (ev: KeyboardEvent) => this.#panLeft = false,
keyDownCallback: (ev: KeyboardEvent) => this.#panLeft = true,
code: "KeyA",
})
.addShortcut(`panRight`, {
label: "Pan map right",
keyUpCallback: (ev: KeyboardEvent) => this.#panRight = false,
keyDownCallback: (ev: KeyboardEvent) => this.#panRight = true,
code: "KeyD",
}).addShortcut(`panFast`, {
label: "Pan map fast",
keyUpCallback: (ev: KeyboardEvent) => this.#panFast = false,
keyDownCallback: (ev: KeyboardEvent) => this.#panFast = true,
code: "ShiftLeft",
});
/* Periodically check if the camera control endpoint is available */
this.#cameraControlTimer = window.setInterval(() => {
this.#checkCameraPort();
@ -715,48 +817,6 @@ export class Map extends L.Map {
return this.#miniMapLayerGroup;
}
handleMapPanning(e: any) {
if (e.type === "keyup") {
switch (e.code) {
case "KeyA":
case "ArrowLeft":
this.#panLeft = false;
break;
case "KeyD":
case "ArrowRight":
this.#panRight = false;
break;
case "KeyW":
case "ArrowUp":
this.#panUp = false;
break;
case "KeyS":
case "ArrowDown":
this.#panDown = false;
break;
}
} else {
switch (e.code) {
case "KeyA":
case "ArrowLeft":
this.#panLeft = true;
break;
case "KeyD":
case "ArrowRight":
this.#panRight = true;
break;
case "KeyW":
case "ArrowUp":
this.#panUp = true;
break;
case "KeyS":
case "ArrowDown":
this.#panDown = true;
break;
}
}
}
addTemporaryMarker(latlng: L.LatLng, name: string, coalition: string, commandHash?: string) {
var marker = new TemporaryUnitMarker(latlng, name, coalition, commandHash);
marker.addTo(this);
@ -781,6 +841,11 @@ export class Map extends L.Map {
MapOptionsChangedEvent.dispatch(this.#options);
}
setOptions(options) {
this.#options = { ...options };
MapOptionsChangedEvent.dispatch(this.#options);
}
getOptions() {
return this.#options;
}
@ -1089,7 +1154,7 @@ export class Map extends L.Map {
else document.dispatchEvent(new CustomEvent("forceboxselect", { detail: e.originalEvent }));
} else if (getApp().getState() === OlympusState.UNIT_CONTROL) {
if (e.originalEvent.button === 2) {
if (!getApp().getMap().getContextAction()) {
if (!this.getContextAction()) {
getApp().setState(OlympusState.UNIT_CONTROL, UnitControlSubState.MAP_CONTEXT_MENU);
MapContextMenuRequestEvent.dispatch(pressLocation);
}
@ -1127,14 +1192,6 @@ export class Map extends L.Map {
if (this.#slaveDCSCamera) this.#broadcastPosition();
}
#onKeyDown(e: any) {
this.#isShiftKeyDown = e.originalEvent.shiftKey;
}
#onKeyUp(e: any) {
this.#isShiftKeyDown = e.originalEvent.shiftKey;
}
#onZoomStart(e: any) {
this.#previousZoom = this.getZoom();
if (this.#centeredUnit != null) this.#panToUnit(this.#centeredUnit);

View File

@ -21,8 +21,8 @@ import { ServerManager } from "./server/servermanager";
import { AudioManager } from "./audio/audiomanager";
import { NO_SUBSTATE, OlympusState, OlympusSubState } from "./constants/constants";
import { AppStateChangedEvent, ConfigLoadedEvent, InfoPopupEvent, SelectedUnitsChangedEvent } from "./events";
import { OlympusConfig } from "./interfaces";
import { AppStateChangedEvent, ConfigLoadedEvent, InfoPopupEvent, MapOptionsChangedEvent, SelectedUnitsChangedEvent, ShortcutsChangedEvent } from "./events";
import { OlympusConfig, ProfileOptions } from "./interfaces";
export var VERSION = "{{OLYMPUS_VERSION_NUMBER}}";
export var IP = window.location.toString();
@ -34,6 +34,7 @@ export class OlympusApp {
#state: OlympusState = OlympusState.NOT_INITIALIZED;
#subState: OlympusSubState = NO_SUBSTATE;
#infoMessages: string[] = [];
#profileName: string | null = null;
/* Main leaflet map, extended by custom methods */
#map: Map | null = null;
@ -52,6 +53,9 @@ export class OlympusApp {
if (selectedUnits.length > 0) this.setState(OlympusState.UNIT_CONTROL);
else this.getState() === OlympusState.UNIT_CONTROL && this.setState(OlympusState.IDLE);
});
MapOptionsChangedEvent.on((options) => getApp().saveProfile());
ShortcutsChangedEvent.on((options) => getApp().saveProfile());
}
getMap() {
@ -90,11 +94,12 @@ export class OlympusApp {
start() {
/* Initialize base functionalitites */
this.#shortcutManager = new ShortcutManager(); /* Keep first */
this.#map = new Map("map-container");
this.#missionManager = new MissionManager();
this.#serverManager = new ServerManager();
this.#shortcutManager = new ShortcutManager();
this.#unitsManager = new UnitsManager();
this.#weaponsManager = new WeaponsManager();
this.#audioManager = new AudioManager();
@ -143,6 +148,54 @@ export class OlympusApp {
return this.#config;
}
setProfile(profileName: string) {
this.#profileName = profileName;
}
saveProfile() {
if (this.#profileName !== null) {
let profile = {};
profile["mapOptions"] = this.#map?.getOptions();
profile["shortcuts"] = this.#shortcutManager?.getShortcutsOptions();
const requestOptions = {
method: "PUT", // Specify the request method
headers: { "Content-Type": "application/json" }, // Specify the content type
body: JSON.stringify(profile), // Send the data in JSON format
};
fetch(window.location.href.split("?")[0].replace("vite/", "") + `resources/profile/${this.#profileName}`, requestOptions)
.then((response) => {
if (response.status === 200) {
console.log(`Profile ${this.#profileName} saved correctly`);
} else {
this.addInfoMessage("Error saving profile");
throw new Error("Error saving profile file");
}
}) // Parse the response as JSON
.catch((error) => console.error(error)); // Handle errors
}
}
getProfile() {
if (this.#profileName && this.#config?.profiles && this.#config?.profiles[this.#profileName])
return this.#config?.profiles[this.#profileName] as ProfileOptions;
else return null;
}
loadProfile() {
const profile = this.getProfile();
if (profile) {
this.#map?.setOptions(profile.mapOptions);
this.#shortcutManager?.setShortcutsOptions(profile.shortcuts);
this.addInfoMessage("Profile loaded correctly");
console.log(`Profile ${this.#profileName} saved correctly`);
} else {
this.addInfoMessage("Error loading profile");
console.log(`Error loading profile`);
}
}
setState(state: OlympusState, subState: OlympusSubState = NO_SUBSTATE) {
if (state !== this.#state || subState !== this.#subState) {
this.#state = state;

View File

@ -14,7 +14,7 @@ import {
reactionsToThreat,
} from "../constants/constants";
import { AirbasesData, BullseyesData, CommandModeOptions, GeneralSettings, MissionData, Radio, ServerRequestOptions, ServerStatus, TACAN } from "../interfaces";
import { InfoPopupEvent, ServerStatusUpdatedEvent } from "../events";
import { ServerStatusUpdatedEvent } from "../events";
export class ServerManager {
#connected: boolean = false;
@ -36,6 +36,14 @@ export class ServerManager {
this.#lastUpdateTimes[AIRBASES_URI] = Date.now();
this.#lastUpdateTimes[BULLSEYE_URI] = Date.now();
this.#lastUpdateTimes[MISSION_URI] = Date.now();
getApp().getShortcutManager().addShortcut("togglePause", {
label: "Pause data update",
callback: () => {
this.setPaused(!this.getPaused());
},
code: "Space"
})
}
setUsername(newUsername: string) {

View File

@ -1,46 +1,49 @@
import { getApp } from "../olympusapp";
import { ShortcutKeyboardOptions, ShortcutMouseOptions, ShortcutOptions } from "../interfaces";
import { ShortcutChangedEvent, ShortcutsChangedEvent } from "../events";
import { ShortcutOptions } from "../interfaces";
import { keyEventWasInInput } from "../other/utils";
export abstract class Shortcut {
#config: ShortcutOptions;
export class Shortcut {
#id: string;
#options: ShortcutOptions;
constructor(config: ShortcutOptions) {
this.#config = config;
}
getConfig() {
return this.#config;
}
}
export class ShortcutKeyboard extends Shortcut {
constructor(config: ShortcutKeyboardOptions) {
config.event = config.event || "keyup";
super(config);
document.addEventListener(config.event, (ev: any) => {
if (ev instanceof KeyboardEvent === false || keyEventWasInInput(ev)) {
return;
}
if (config.code !== ev.code) {
return;
}
constructor(id, options: ShortcutOptions) {
this.#id = id;
this.#options = options;
/* Key up event is mandatory */
document.addEventListener("keyup", (ev: any) => {
if (keyEventWasInInput(ev) || options.code !== ev.code) return;
if (
(typeof config.altKey !== "boolean" || (typeof config.altKey === "boolean" && ev.altKey === config.altKey)) &&
(typeof config.ctrlKey !== "boolean" || (typeof config.ctrlKey === "boolean" && ev.ctrlKey === config.ctrlKey)) &&
(typeof config.shiftKey !== "boolean" || (typeof config.shiftKey === "boolean" && ev.shiftKey === config.shiftKey))
) {
config.callback(ev);
}
(typeof options.altKey !== "boolean" || (typeof options.altKey === "boolean" && ev.altKey === options.altKey)) &&
(typeof options.ctrlKey !== "boolean" || (typeof options.ctrlKey === "boolean" && ev.ctrlKey === options.ctrlKey)) &&
(typeof options.shiftKey !== "boolean" || (typeof options.shiftKey === "boolean" && ev.shiftKey === options.shiftKey))
)
options.keyUpCallback(ev);
});
}
}
export class ShortcutMouse extends Shortcut {
constructor(config: ShortcutMouseOptions) {
super(config);
/* Key down event is optional */
if (options.keyDownCallback) {
document.addEventListener("keydown", (ev: any) => {
if (keyEventWasInInput(ev) || options.code !== ev.code) return;
if (
(typeof options.altKey !== "boolean" || (typeof options.altKey === "boolean" && ev.altKey === options.altKey)) &&
(typeof options.ctrlKey !== "boolean" || (typeof options.ctrlKey === "boolean" && ev.ctrlKey === options.ctrlKey)) &&
(typeof options.shiftKey !== "boolean" || (typeof options.shiftKey === "boolean" && ev.shiftKey === options.shiftKey))
)
if (options.keyDownCallback) options.keyDownCallback(ev);
});
}
}
getOptions() {
return this.#options;
}
setOptions(options: ShortcutOptions) {
this.#options = { ...options };
}
getId() {
return this.#id;
}
}

View File

@ -1,252 +1,42 @@
import { ContextActions, OlympusState } from "../constants/constants";
import { ShortcutKeyboardOptions, ShortcutMouseOptions } from "../interfaces";
import { getApp } from "../olympusapp";
import { ShortcutKeyboard, ShortcutMouse } from "./shortcut";
import { ShortcutChangedEvent, ShortcutsChangedEvent } from "../events";
import { ShortcutOptions } from "../interfaces";
import { Shortcut } from "./shortcut";
export class ShortcutManager {
#items: { [key: string]: any } = {};
#keysBeingHeld: string[] = [];
#keyDownCallbacks: CallableFunction[] = [];
#keyUpCallbacks: CallableFunction[] = [];
#shortcuts: { [key: string]: Shortcut } = {};
constructor() {
// Stop ctrl+digits from sending the browser to another tab
document.addEventListener("keydown", (ev: KeyboardEvent) => {
if (this.#keysBeingHeld.indexOf(ev.code) < 0) {
this.#keysBeingHeld.push(ev.code);
if (ev.code.indexOf("Digit") >= 0 && ev.ctrlKey === true && ev.altKey === false && ev.shiftKey === false) {
ev.preventDefault();
}
this.#keyDownCallbacks.forEach((callback) => callback(ev));
});
document.addEventListener("keyup", (ev: KeyboardEvent) => {
this.#keysBeingHeld = this.#keysBeingHeld.filter((held) => held !== ev.code);
this.#keyUpCallbacks.forEach((callback) => callback(ev));
});
this.addKeyboardShortcut("togglePause", {
altKey: false,
callback: () => {
getApp().getServerManager().setPaused(!getApp().getServerManager().getPaused());
},
code: "Space",
ctrlKey: false,
})
.addKeyboardShortcut("deselectAll", {
callback: (ev: KeyboardEvent) => {
getApp().getUnitsManager().deselectAllUnits();
},
code: "Escape",
})
.addKeyboardShortcut("toggleUnitLabels", {
altKey: false,
callback: () => {
getApp().getMap().setOption("showUnitLabels", !getApp().getMap().getOptions().showUnitLabels);
},
code: "KeyL",
ctrlKey: false,
shiftKey: false,
})
.addKeyboardShortcut("toggleAcquisitionRings", {
altKey: false,
callback: () => {
getApp().getMap().setOption("showUnitsAcquisitionRings", !getApp().getMap().getOptions().showUnitsAcquisitionRings);
},
code: "KeyE",
ctrlKey: false,
shiftKey: false,
})
.addKeyboardShortcut("toggleEngagementRings", {
altKey: false,
callback: () => {
getApp().getMap().setOption("showUnitsEngagementRings", !getApp().getMap().getOptions().showUnitsEngagementRings);
},
code: "KeyQ",
ctrlKey: false,
shiftKey: false,
})
.addKeyboardShortcut("toggleHideShortEngagementRings", {
altKey: false,
callback: () => {
getApp().getMap().setOption("hideUnitsShortRangeRings", !getApp().getMap().getOptions().hideUnitsShortRangeRings);
},
code: "KeyR",
ctrlKey: false,
shiftKey: false,
})
.addKeyboardShortcut("toggleDetectionLines", {
altKey: false,
callback: () => {
getApp().getMap().setOption("showUnitTargets", !getApp().getMap().getOptions().showUnitTargets);
},
code: "KeyF",
ctrlKey: false,
shiftKey: false,
})
.addKeyboardShortcut("toggleGroupMembers", {
altKey: false,
callback: () => {
getApp().getMap().setOption("hideGroupMembers", !getApp().getMap().getOptions().hideGroupMembers);
},
code: "KeyG",
ctrlKey: false,
shiftKey: false,
})
.addKeyboardShortcut("toggleRelativePositions", {
altKey: false,
callback: () => {
getApp().getMap().setOption("keepRelativePositions", !getApp().getMap().getOptions().keepRelativePositions);
},
code: "KeyP",
ctrlKey: false,
shiftKey: false,
})
.addKeyboardShortcut("increaseCameraZoom", {
altKey: true,
callback: () => {
//getApp().getMap().increaseCameraZoom();
},
code: "Equal",
ctrlKey: false,
shiftKey: false,
})
.addKeyboardShortcut("decreaseCameraZoom", {
altKey: true,
callback: () => {
//getApp().getMap().decreaseCameraZoom();
},
code: "Minus",
ctrlKey: false,
shiftKey: false,
});
for (let contextActionName in ContextActions) {
if (ContextActions[contextActionName].getOptions().hotkey) {
this.addKeyboardShortcut(`${contextActionName}Hotkey`, {
code: ContextActions[contextActionName].getOptions().hotkey,
shiftKey: true,
callback: () => {
const contextActionSet = getApp().getMap().getContextActionSet();
if (
getApp().getState() === OlympusState.UNIT_CONTROL &&
contextActionSet &&
ContextActions[contextActionName].getId() in contextActionSet.getContextActions()
) {
if (ContextActions[contextActionName].getOptions().executeImmediately) ContextActions[contextActionName].executeCallback();
else getApp().getMap().setContextAction(ContextActions[contextActionName]);
}
},
});
}
}
let PTTKeys = ["KeyZ", "KeyX", "KeyC", "KeyV", "KeyB", "KeyN", "KeyM", "KeyK", "KeyL"];
PTTKeys.forEach((key, idx) => {
this.addKeyboardShortcut(`PTT${idx}Active`, {
altKey: false,
callback: () => {
getApp().getAudioManager().getSinks()[idx]?.setPtt(true);
},
code: key,
ctrlKey: false,
shiftKey: false,
event: "keydown",
}).addKeyboardShortcut(`PTT${idx}Active`, {
altKey: false,
callback: () => {
getApp().getAudioManager().getSinks()[idx]?.setPtt(false);
},
code: key,
ctrlKey: false,
shiftKey: false,
event: "keyup",
});
});
let panKeys = ["KeyW", "KeyA", "KeyS", "KeyD", "ArrowLeft", "ArrowRight", "ArrowUp", "ArrowDown"];
panKeys.forEach((code) => {
this.addKeyboardShortcut(`pan${code}keydown`, {
altKey: false,
callback: (ev: KeyboardEvent) => {
getApp().getMap().handleMapPanning(ev);
},
code: code,
ctrlKey: false,
event: "keydown",
});
this.addKeyboardShortcut(`pan${code}keyup`, {
callback: (ev: KeyboardEvent) => {
getApp().getMap().handleMapPanning(ev);
},
code: code,
});
});
const digits = ["Digit1", "Digit2", "Digit3", "Digit4", "Digit5", "Digit6", "Digit7", "Digit8", "Digit9"];
digits.forEach((code) => {
this.addKeyboardShortcut(`hotgroup${code}`, {
altKey: false,
callback: (ev: KeyboardEvent) => {
if (ev.ctrlKey && ev.shiftKey)
getApp()
.getUnitsManager()
.selectUnitsByHotgroup(parseInt(ev.code.substring(5)), false);
// "Select hotgroup X in addition to any units already selected"
else if (ev.ctrlKey && !ev.shiftKey)
getApp()
.getUnitsManager()
.setHotgroup(parseInt(ev.code.substring(5)));
// "These selected units are hotgroup X (forget any previous membership)"
else if (!ev.ctrlKey && ev.shiftKey)
getApp()
.getUnitsManager()
.addToHotgroup(parseInt(ev.code.substring(5)));
// "Add (append) these units to hotgroup X (in addition to any existing members)"
else
getApp()
.getUnitsManager()
.selectUnitsByHotgroup(parseInt(ev.code.substring(5))); // "Select hotgroup X, deselect any units not in it."
},
code: code,
});
// Stop hotgroup controls sending the browser to another tab
document.addEventListener("keydown", (ev: KeyboardEvent) => {
if (ev.code === code && ev.ctrlKey === true && ev.altKey === false && ev.shiftKey === false) {
ev.preventDefault();
}
});
});
}
addKeyboardShortcut(name: string, shortcutKeyboardOptions: ShortcutKeyboardOptions) {
this.#items[name] = new ShortcutKeyboard(shortcutKeyboardOptions);
addShortcut(id: string, shortcutOptions: ShortcutOptions) {
this.#shortcuts[id] = new Shortcut(id, shortcutOptions);
ShortcutsChangedEvent.dispatch(this.#shortcuts);
return this;
}
addMouseShortcut(name: string, shortcutMouseOptions: ShortcutMouseOptions) {
this.#items[name] = new ShortcutMouse(shortcutMouseOptions);
return this;
}
getKeysBeingHeld() {
return this.#keysBeingHeld;
}
keyComboMatches(combo: string[]) {
const heldKeys = this.getKeysBeingHeld();
if (combo.length !== heldKeys.length) {
return false;
getShortcutsOptions() {
let shortcutsOptions = {};
for (let id in this.#shortcuts) {
shortcutsOptions[id] = this.#shortcuts[id].getOptions();
}
return combo.every((key) => heldKeys.indexOf(key) > -1);
return shortcutsOptions;
}
onKeyDown(callback: CallableFunction) {
this.#keyDownCallbacks.push(callback);
setShortcutsOptions(shortcutOptions: { [key: string]: ShortcutOptions }) {
for (let id in shortcutOptions) {
this.#shortcuts[id].setOptions(shortcutOptions[id]);
}
ShortcutsChangedEvent.dispatch(this.#shortcuts);
}
onKeyUp(callback: CallableFunction) {
this.#keyUpCallbacks.push(callback);
setShortcutOption(id: string, shortcutOptions: ShortcutOptions) {
this.#shortcuts[id].setOptions(shortcutOptions);
ShortcutsChangedEvent.dispatch(this.#shortcuts);
}
}

View File

@ -0,0 +1,151 @@
import React, { useEffect, useState } from "react";
import { Modal } from "./components/modal";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { faArrowRight } from "@fortawesome/free-solid-svg-icons";
import { getApp } from "../../olympusapp";
import { OlympusState } from "../../constants/constants";
import { Shortcut } from "../../shortcut/shortcut";
import { BindShortcutRequestEvent, ShortcutsChangedEvent } from "../../events";
export function KeybindModal(props: { open: boolean }) {
const [shortcuts, setShortcuts] = useState({} as { [key: string]: Shortcut });
const [shortcut, setShortcut] = useState(null as null | Shortcut);
const [code, setCode] = useState(null as null | string);
const [shiftKey, setShiftKey] = useState(false);
const [ctrlKey, setCtrlKey] = useState(false);
const [altKey, setAltKey] = useState(false);
useEffect(() => {
ShortcutsChangedEvent.on((shortcuts) => setShortcuts({ ...shortcuts }));
BindShortcutRequestEvent.on((shortcut) => setShortcut(shortcut));
document.addEventListener("keydown", (ev) => {
setCode(ev.code);
if (!(ev.code.indexOf("Shift") >= 0 || ev.code.indexOf("Alt") >= 0 || ev.code.indexOf("Control") >= 0)) {
setShiftKey(ev.shiftKey);
setAltKey(ev.altKey);
setCtrlKey(ev.ctrlKey);
}
});
}, []);
let available: null | boolean = code ? true : null;
let inUseShortcut: null | Shortcut = null;
for (let id in shortcuts) {
if (
code === shortcuts[id].getOptions().code &&
shiftKey == shortcuts[id].getOptions().shiftKey &&
altKey == shortcuts[id].getOptions().altKey &&
ctrlKey == shortcuts[id].getOptions().shiftKey
) {
available = false;
inUseShortcut = shortcuts[id];
}
}
return (
<>
{props.open && (
<>
<Modal
className={`
inline-flex h-fit w-[600px] overflow-y-auto scroll-smooth bg-white
p-10
dark:bg-olympus-800
max-md:h-full max-md:max-h-full max-md:w-full max-md:rounded-none
max-md:border-none
`}
>
<div className="flex h-full w-full flex-col gap-4">
<div className={`flex flex-col items-start gap-2`}>
<span
className={`
text-gray-800 text-md
dark:text-white
`}
>
{shortcut?.getOptions().label}
</span>
<span
className={`
text-gray-800 text-md
dark:text-gray-500
`}
>
Press the key you want to bind to this event
</span>
</div>
<div className="w-full text-center text-white">
{ctrlKey ? "Ctrl + " : ""}
{shiftKey ? "Shift + " : ""}
{altKey ? "Alt + " : ""}
{code}
</div>
<div className="text-white">
{available === true && <div className="text-green-600">Keybind is free!</div>}
{available === false && (
<div>
Keybind is already in use:{" "}
<span
className={`font-bold text-red-600`}
>
{inUseShortcut?.getOptions().label}
</span>
</div>
)}
</div>
<div className="flex justify-end">
{available && shortcut && (
<button
type="button"
onClick={() => {
if (shortcut && code) {
let options = shortcut.getOptions()
options.code = code;
options.altKey = altKey;
options.shiftKey = shiftKey;
options.ctrlKey = ctrlKey;
getApp().getShortcutManager().setShortcutOption(shortcut.getId(), options)
getApp().setState(OlympusState.OPTIONS);
}
}}
className={`
mb-2 me-2 ml-auto flex content-center items-center gap-2
rounded-sm bg-blue-700 px-5 py-2.5 text-sm font-medium
text-white
dark:bg-blue-600 dark:hover:bg-blue-700
dark:focus:ring-blue-800
focus:outline-none focus:ring-4 focus:ring-blue-300
hover:bg-blue-800
`}
>
Continue
<FontAwesomeIcon icon={faArrowRight} />
</button>
)}
<button
type="button"
onClick={() => getApp().setState(OlympusState.OPTIONS)}
className={`
mb-2 me-2 flex content-center items-center gap-2 rounded-sm
border-[1px] bg-blue-700 px-5 py-2.5 text-sm font-medium
text-white
dark:border-gray-600 dark:bg-gray-800 dark:text-gray-400
dark:hover:bg-gray-700 dark:focus:ring-blue-800
focus:outline-none focus:ring-4 focus:ring-blue-300
hover:bg-blue-800
`}
>
Back
</button>
</div>
</div>
</Modal>
<div className={`fixed left-0 top-0 z-30 h-full w-full bg-[#111111]/95`}></div>
</>
)}
</>
);
}

View File

@ -4,19 +4,54 @@ import { Card } from "./components/card";
import { ErrorCallout } from "../../ui/components/olcallout";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { faArrowRight, faCheckCircle, faExternalLink } from "@fortawesome/free-solid-svg-icons";
import { VERSION } from "../../olympusapp";
import { getApp, VERSION } from "../../olympusapp";
import { sha256 } from "js-sha256";
import { BLUE_COMMANDER, GAME_MASTER, OlympusState, RED_COMMANDER } from "../../constants/constants";
export function LoginModal(props: {
checkingPassword: boolean;
loginError: boolean;
commandMode: string | null;
onLogin: (password: string) => void;
onContinue: (username: string) => void;
onBack: () => void;
}) {
export function LoginModal(props: {}) {
// TODO: add warning if not in secure context and some features are disabled
const [password, setPassword] = useState("");
const [displayName, setDisplayName] = useState("Game Master");
const [profileName, setProfileName] = useState("Game Master");
const [checkingPassword, setCheckingPassword] = useState(false);
const [loginError, setLoginError] = useState(false);
const [commandMode, setCommandMode] = useState(null as null | string);
function checkPassword(password: string) {
setCheckingPassword(true);
var hash = sha256.create();
getApp().getServerManager().setPassword(hash.update(password).hex());
getApp()
.getServerManager()
.getMission(
(response) => {
const commandMode = response.mission.commandModeOptions.commandMode;
try {
[GAME_MASTER, BLUE_COMMANDER, RED_COMMANDER].includes(commandMode) ? setCommandMode(commandMode) : setLoginError(true);
} catch {
setLoginError(true);
}
setCheckingPassword(false);
},
() => {
setLoginError(true);
setCheckingPassword(false);
}
);
}
function connect() {
getApp().getServerManager().setUsername(profileName);
getApp().getServerManager().startUpdate();
getApp().setState(OlympusState.IDLE);
/* Set the profile name */
getApp().setProfile(profileName);
/* If no profile exists already with that name, create it from scratch from the defaults */
if (getApp().getProfile() === null)
getApp().saveProfile();
/* Load the profile */
getApp().loadProfile();
}
return (
<Modal
@ -62,7 +97,7 @@ export function LoginModal(props: {
max-lg:w-[100%]
`}
>
{!props.checkingPassword ? (
{!checkingPassword ? (
<>
<div className="flex flex-col items-start">
<div
@ -115,9 +150,9 @@ export function LoginModal(props: {
</div>
</div>
</div>
{!props.loginError ? (
{!loginError ? (
<>
{props.commandMode === null ? (
{commandMode === null ? (
<>
<div className={`flex flex-col items-start gap-2`}>
<label
@ -148,7 +183,7 @@ export function LoginModal(props: {
<div className="flex">
<button
type="button"
onClick={() => props.onLogin(password)}
onClick={() => checkPassword(password)}
className={`
mb-2 me-2 flex content-center items-center gap-2
rounded-sm bg-blue-700 px-5 py-2.5 text-sm
@ -172,24 +207,19 @@ export function LoginModal(props: {
</>
) : (
<>
<div
className={`
flex flex-col items-start
gap-2
`}
>
<div className={`flex flex-col items-start gap-2`}>
<label
className={`
text-gray-800 text-md
dark:text-white
`}
>
Set display name
Set profile name
</label>
<input
type="text"
autoComplete="username"
onChange={(ev) => setDisplayName(ev.currentTarget.value)}
onChange={(ev) => setProfileName(ev.currentTarget.value)}
className={`
block w-full max-w-80 rounded-lg border
border-gray-300 bg-gray-50 p-2.5 text-sm
@ -201,14 +231,17 @@ export function LoginModal(props: {
focus:border-blue-500 focus:ring-blue-500
`}
placeholder="Enter display name"
value={displayName}
value={profileName}
required
/>
</div>
<div className="text-xs text-gray-400">
The profile name you choose determines what keybinds/groups/options get loaded and edited. Be careful!
</div>
<div className="flex">
<button
type="button"
onClick={() => props.onContinue(displayName)}
onClick={() => connect()}
className={`
mb-2 me-2 flex content-center items-center gap-2
rounded-sm bg-blue-700 px-5 py-2.5 text-sm
@ -249,11 +282,7 @@ export function LoginModal(props: {
title="Server could not be reached"
description="The Olympus Server at this address could not be reached. Check the address is correct, restart the Olympus server or reinstall Olympus. Ensure the ports set are not already used."
></ErrorCallout>
<div
className={`
text-sm font-medium text-gray-200
`}
>
<div className={`text-sm font-medium text-gray-200`}>
Still having issues? See our
<a
href=""

View File

@ -2,100 +2,110 @@ import React from "react";
import { Modal } from "./components/modal";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { faArrowRight } from "@fortawesome/free-solid-svg-icons";
import { Unit } from "../../unit/unit";
import { FaLock } from "react-icons/fa6";
import { getApp } from "../../olympusapp";
import { OlympusState } from "../../constants/constants";
export function ProtectionPrompt(props: {}) {
export function ProtectionPrompt(props: { open: boolean }) {
return (
<Modal
className={`
inline-flex h-fit w-[600px] overflow-y-auto scroll-smooth bg-white p-10
dark:bg-olympus-800
max-md:h-full max-md:max-h-full max-md:w-full max-md:rounded-none
max-md:border-none
`}
>
<div className="flex h-full w-full flex-col gap-12">
<div className={`flex flex-col items-start gap-2`}>
<span
<>
{props.open && (
<>
<Modal
className={`
text-gray-800 text-md
dark:text-white
inline-flex h-fit w-[600px] overflow-y-auto scroll-smooth bg-white
p-10
dark:bg-olympus-800
max-md:h-full max-md:max-h-full max-md:w-full max-md:rounded-none
max-md:border-none
`}
>
Your selection contains protected units, are you sure you want to continue?
</span>
<span
className={`
text-gray-800 text-md
dark:text-gray-500
`}
>
Pressing "Continue" will cause all DCS controlled units in the current selection to abort their mission and start following Olympus commands only.
</span>
<span
className={`
text-gray-800 text-md
dark:text-gray-500
`}
>
If you are trying to delete a human player unit, they will be killed and de-slotted. Be careful!
</span>
<span
className={`
text-gray-800 text-md
dark:text-gray-500
`}
>
To disable this warning, press on the{" "}
<span
className={`
inline-block translate-y-3 rounded-full border-[1px]
border-gray-900 bg-red-500 p-2 text-olympus-900
`}
>
<FaLock />
</span>{" "}
button
</span>
</div>
<div className="flex">
<button
type="button"
onClick={() => {
getApp().getUnitsManager().executeProtectionCallback();
getApp().setState(OlympusState.UNIT_CONTROL);
}}
className={`
mb-2 me-2 ml-auto flex content-center items-center gap-2
rounded-sm bg-blue-700 px-5 py-2.5 text-sm font-medium text-white
dark:bg-blue-600 dark:hover:bg-blue-700 dark:focus:ring-blue-800
focus:outline-none focus:ring-4 focus:ring-blue-300
hover:bg-blue-800
`}
>
Continue
<FontAwesomeIcon className={`my-auto`} icon={faArrowRight} />
</button>
<button
type="button"
onClick={() => getApp().setState(OlympusState.UNIT_CONTROL)}
className={`
mb-2 me-2 flex content-center items-center gap-2 rounded-sm
border-[1px] bg-blue-700 px-5 py-2.5 text-sm font-medium
text-white
dark:border-gray-600 dark:bg-gray-800 dark:text-gray-400
dark:hover:bg-gray-700 dark:focus:ring-blue-800
focus:outline-none focus:ring-4 focus:ring-blue-300
hover:bg-blue-800
`}
>
Back
</button>
</div>
</div>
</Modal>
<div className="flex h-full w-full flex-col gap-12">
<div className={`flex flex-col items-start gap-2`}>
<span
className={`
text-gray-800 text-md
dark:text-white
`}
>
Your selection contains protected units, are you sure you want to continue?
</span>
<span
className={`
text-gray-800 text-md
dark:text-gray-500
`}
>
Pressing "Continue" will cause all DCS controlled units in the current selection to abort their mission and start following Olympus commands
only.
</span>
<span
className={`
text-gray-800 text-md
dark:text-gray-500
`}
>
If you are trying to delete a human player unit, they will be killed and de-slotted. Be careful!
</span>
<span
className={`
text-gray-800 text-md
dark:text-gray-500
`}
>
To disable this warning, press on the{" "}
<span
className={`
inline-block translate-y-3 rounded-full border-[1px]
border-gray-900 bg-red-500 p-2 text-olympus-900
`}
>
<FaLock />
</span>{" "}
button
</span>
</div>
<div className="flex">
<button
type="button"
onClick={() => {
getApp().getUnitsManager().executeProtectionCallback();
getApp().setState(OlympusState.UNIT_CONTROL);
}}
className={`
mb-2 me-2 ml-auto flex content-center items-center gap-2
rounded-sm bg-blue-700 px-5 py-2.5 text-sm font-medium
text-white
dark:bg-blue-600 dark:hover:bg-blue-700
dark:focus:ring-blue-800
focus:outline-none focus:ring-4 focus:ring-blue-300
hover:bg-blue-800
`}
>
Continue
<FontAwesomeIcon className={`my-auto`} icon={faArrowRight} />
</button>
<button
type="button"
onClick={() => getApp().setState(OlympusState.UNIT_CONTROL)}
className={`
mb-2 me-2 flex content-center items-center gap-2 rounded-sm
border-[1px] bg-blue-700 px-5 py-2.5 text-sm font-medium
text-white
dark:border-gray-600 dark:bg-gray-800 dark:text-gray-400
dark:hover:bg-gray-700 dark:focus:ring-blue-800
focus:outline-none focus:ring-4 focus:ring-blue-300
hover:bg-blue-800
`}
>
Back
</button>
</div>
</div>
</Modal>
<div className={`fixed left-0 top-0 z-30 h-full w-full bg-[#111111]/95`}></div>
</>
)}
</>
);
}

View File

@ -35,7 +35,7 @@ export function Header() {
MapOptionsChangedEvent.on((mapOptions) => setMapOptions({ ...mapOptions }));
MapSourceChangedEvent.on((source) => setMapSource(source));
ConfigLoadedEvent.on((config: OlympusConfig) => {
var sources = Object.keys(config.mapMirrors).concat(Object.keys(config.mapLayers));
var sources = Object.keys(config.frontend.mapMirrors).concat(Object.keys(config.frontend.mapLayers));
setMapSources(sources);
});
CommandModeOptionsChangedEvent.on((commandModeOptions) => {
@ -112,9 +112,7 @@ export function Header() {
</div>
{commandModeOptions.commandMode === BLUE_COMMANDER && (
<div
className={`
flex h-full rounded-md bg-blue-600 px-4 text-white
`}
className={`flex h-full rounded-md bg-blue-600 px-4 text-white`}
>
<span className="my-auto font-bold">BLUE Commander ({commandModeOptions.spawnPoints.blue} points)</span>
</div>

View File

@ -4,14 +4,28 @@ import { OlCheckbox } from "../components/olcheckbox";
import { OlRangeSlider } from "../components/olrangeslider";
import { OlNumberInput } from "../components/olnumberinput";
import { getApp } from "../../olympusapp";
import { MAP_OPTIONS_DEFAULTS } from "../../constants/constants";
import { MapOptionsChangedEvent } from "../../events";
import { MAP_OPTIONS_DEFAULTS, OlympusState, OptionsSubstate } from "../../constants/constants";
import { BindShortcutRequestEvent, MapOptionsChangedEvent, ShortcutsChangedEvent } from "../../events";
import { OlAccordion } from "../components/olaccordion";
import { Shortcut } from "../../shortcut/shortcut";
import { OlSearchBar } from "../components/olsearchbar";
const enum Accordion {
NONE,
BINDINGS,
MAP_OPTIONS,
CAMERA_PLUGIN,
}
export function OptionsMenu(props: { open: boolean; onClose: () => void; children?: JSX.Element | JSX.Element[] }) {
const [mapOptions, setMapOptions] = useState(MAP_OPTIONS_DEFAULTS);
const [shortcuts, setShortcuts] = useState({} as { [key: string]: Shortcut });
const [openAccordion, setOpenAccordion] = useState(Accordion.NONE);
const [filterString, setFilterString] = useState("");
useEffect(() => {
MapOptionsChangedEvent.on((mapOptions) => setMapOptions({ ...mapOptions }));
ShortcutsChangedEvent.on((shortcuts) => setShortcuts({ ...shortcuts }));
}, []);
return (
@ -22,226 +36,201 @@ export function OptionsMenu(props: { open: boolean; onClose: () => void; childre
dark:text-white
`}
>
<div
className={`
group flex flex-row rounded-md justify-content cursor-pointer gap-4
p-2
dark:hover:bg-olympus-400
`}
onClick={() => {
getApp().getMap().setOption("showUnitLabels", !mapOptions.showUnitLabels);
}}
<OlAccordion
onClick={() =>
setOpenAccordion(openAccordion === Accordion.NONE ? Accordion.BINDINGS: Accordion.NONE )
}
open={openAccordion === Accordion.BINDINGS}
title="Key bindings"
>
<OlCheckbox checked={mapOptions.showUnitLabels} onChange={() => {}}></OlCheckbox>
<span>Show Unit Labels</span>
<kbd
<OlSearchBar onChange={(value) => setFilterString(value)} text={filterString} />
<div
className={`
ml-auto rounded-lg border border-gray-200 bg-gray-100 px-2 py-1.5
text-xs font-semibold text-gray-800
dark:border-gray-500 dark:bg-gray-600 dark:text-gray-100
flex max-h-[450px] flex-col gap-1 overflow-y-scroll no-scrollbar
`}
>
L
</kbd>
</div>
<div
className={`
group flex flex-row rounded-md justify-content cursor-pointer gap-4
p-2
dark:hover:bg-olympus-400
`}
onClick={() => {
getApp().getMap().setOption("showUnitsEngagementRings", !mapOptions.showUnitsEngagementRings);
}}
>
<OlCheckbox checked={mapOptions.showUnitsEngagementRings} onChange={() => {}}></OlCheckbox>
<span>Show Threat Rings</span>
<kbd
className={`
ml-auto rounded-lg border border-gray-200 bg-gray-100 px-2 py-1.5
text-xs font-semibold text-gray-800
dark:border-gray-500 dark:bg-gray-600 dark:text-gray-100
`}
>
Q
</kbd>
</div>
<div
className={`
group flex flex-row rounded-md justify-content cursor-pointer gap-4
p-2
dark:hover:bg-olympus-400
`}
onClick={() => {
getApp().getMap().setOption("showUnitsAcquisitionRings", !mapOptions.showUnitsAcquisitionRings);
}}
>
<OlCheckbox checked={mapOptions.showUnitsAcquisitionRings} onChange={() => {}}></OlCheckbox>
<span>Show Detection rings</span>
<kbd
className={`
ml-auto rounded-lg border border-gray-200 bg-gray-100 px-2 py-1.5
text-xs font-semibold text-gray-800
dark:border-gray-500 dark:bg-gray-600 dark:text-gray-100
`}
>
E
</kbd>
</div>
<div
className={`
group flex flex-row rounded-md justify-content cursor-pointer gap-4
p-2
dark:hover:bg-olympus-400
`}
onClick={() => {
getApp().getMap().setOption("showUnitTargets", !mapOptions.showUnitTargets);
}}
>
<OlCheckbox checked={mapOptions.showUnitTargets} onChange={() => {}}></OlCheckbox>
<span>Show Detection lines</span>
<kbd
className={`
ml-auto rounded-lg border border-gray-200 bg-gray-100 px-2 py-1.5
text-xs font-semibold text-gray-800
dark:border-gray-500 dark:bg-gray-600 dark:text-gray-100
`}
>
F
</kbd>
</div>
<div
className={`
group flex flex-row rounded-md justify-content cursor-pointer gap-4
p-2
dark:hover:bg-olympus-400
`}
onClick={() => {
getApp().getMap().setOption("hideUnitsShortRangeRings", !mapOptions.hideUnitsShortRangeRings);
}}
>
<OlCheckbox checked={mapOptions.hideUnitsShortRangeRings} onChange={() => {}}></OlCheckbox>
<span>Hide Short range Rings</span>
<kbd
className={`
ml-auto rounded-lg border border-gray-200 bg-gray-100 px-2 py-1.5
text-xs font-semibold text-gray-800
dark:border-gray-500 dark:bg-gray-600 dark:text-gray-100
`}
>
R
</kbd>
</div>
<div
className={`
group flex flex-row rounded-md justify-content cursor-pointer gap-4
p-2
dark:hover:bg-olympus-400
`}
onClick={() => {
getApp().getMap().setOption("keepRelativePositions", !mapOptions.keepRelativePositions);
}}
>
<OlCheckbox checked={mapOptions.keepRelativePositions} onChange={() => {}}></OlCheckbox>
<span>Keep units relative positions</span>
<kbd
className={`
ml-auto rounded-lg border border-gray-200 bg-gray-100 px-2 py-1.5
text-xs font-semibold text-gray-800
dark:border-gray-500 dark:bg-gray-600 dark:text-gray-100
`}
>
P
</kbd>
</div>
<div
className={`
group flex flex-row rounded-md justify-content cursor-pointer gap-4
p-2
dark:hover:bg-olympus-400
`}
onClick={() => {
getApp().getMap().setOption("hideGroupMembers", !mapOptions.hideGroupMembers);
}}
>
<OlCheckbox checked={mapOptions.hideGroupMembers} onChange={() => {}}></OlCheckbox>
<span>Hide Group members</span>
<kbd
className={`
ml-auto rounded-lg border border-gray-200 bg-gray-100 px-2 py-1.5
text-xs font-semibold text-gray-800
dark:border-gray-500 dark:bg-gray-600 dark:text-gray-100
`}
>
G
</kbd>
</div>
<div
className={`
group flex flex-row rounded-md justify-content cursor-pointer gap-4
p-2
dark:hover:bg-olympus-400
`}
onClick={() => {
getApp().getMap().setOption("showMinimap", !mapOptions.showMinimap);
}}
>
<OlCheckbox checked={mapOptions.showMinimap} onChange={() => {}}></OlCheckbox>
<span>Show minimap</span>
<kbd
className={`
ml-auto rounded-lg border border-gray-200 bg-gray-100 px-2 py-1.5
text-xs font-semibold text-gray-800
dark:border-gray-500 dark:bg-gray-600 dark:text-gray-100
`}
>
?
</kbd>
</div>
{Object.entries(shortcuts)
.filter(([id, shortcut]) => shortcut.getOptions().label.toLowerCase().indexOf(filterString.toLowerCase()) >= 0)
.map(([id, shortcut]) => {
return (
<div
className={`
group relative mr-2 flex cursor-pointer select-none
items-center justify-between rounded-sm px-2 py-2 text-sm
dark:text-gray-300 dark:hover:bg-olympus-500
`}
onClick={() => {
getApp().setState(OlympusState.OPTIONS, OptionsSubstate.KEYBIND);
BindShortcutRequestEvent.dispatch(shortcut);
}}
>
<span>{shortcut.getOptions().label}</span>
<span>
{shortcut.getOptions().altKey ? "Alt + " : ""}
{shortcut.getOptions().ctrlKey ? "Ctrl + " : ""}
{shortcut.getOptions().shiftKey ? "Shift + " : ""}
{shortcut.getOptions().code}
</span>
</div>
);
})}
</div>
</OlAccordion>
<hr className={`
<OlAccordion onClick={() => setOpenAccordion(openAccordion === Accordion.NONE ? Accordion.MAP_OPTIONS: Accordion.NONE )} open={openAccordion === Accordion.MAP_OPTIONS} title="Map options">
<div
className={`
group flex flex-row rounded-md justify-content cursor-pointer
gap-4 p-2
dark:hover:bg-olympus-400
`}
onClick={() => getApp().getMap().setOption("showUnitLabels", !mapOptions.showUnitLabels)}
>
<OlCheckbox checked={mapOptions.showUnitLabels} onChange={() => {}}></OlCheckbox>
<span>Show Unit Labels</span>
</div>
<div
className={`
group flex flex-row rounded-md justify-content cursor-pointer
gap-4 p-2
dark:hover:bg-olympus-400
`}
onClick={() => getApp().getMap().setOption("showUnitsEngagementRings", !mapOptions.showUnitsEngagementRings)}
>
<OlCheckbox checked={mapOptions.showUnitsEngagementRings} onChange={() => {}}></OlCheckbox>
<span>Show Threat Rings</span>
</div>
<div
className={`
group flex flex-row rounded-md justify-content cursor-pointer
gap-4 p-2
dark:hover:bg-olympus-400
`}
onClick={() => getApp().getMap().setOption("showUnitsAcquisitionRings", !mapOptions.showUnitsAcquisitionRings)}
>
<OlCheckbox checked={mapOptions.showUnitsAcquisitionRings} onChange={() => {}}></OlCheckbox>
<span>Show Detection rings</span>
</div>
<div
className={`
group flex flex-row rounded-md justify-content cursor-pointer
gap-4 p-2
dark:hover:bg-olympus-400
`}
onClick={() => getApp().getMap().setOption("showUnitTargets", !mapOptions.showUnitTargets)}
>
<OlCheckbox checked={mapOptions.showUnitTargets} onChange={() => {}}></OlCheckbox>
<span>Show Detection lines</span>
</div>
<div
className={`
group flex flex-row gap-4 rounded-md justify-content
cursor-pointer p-2 text-sm
dark:hover:bg-olympus-400
`}
onClick={() => getApp().getMap().setOption("hideUnitsShortRangeRings", !mapOptions.hideUnitsShortRangeRings)}
>
<OlCheckbox checked={mapOptions.hideUnitsShortRangeRings} onChange={() => {}}></OlCheckbox>
<span>Hide Short range Rings</span>
</div>
<div
className={`
group flex flex-row gap-4 rounded-md justify-content
cursor-pointer p-2 text-sm
dark:hover:bg-olympus-400
`}
onClick={() => getApp().getMap().setOption("keepRelativePositions", !mapOptions.keepRelativePositions)}
>
<OlCheckbox checked={mapOptions.keepRelativePositions} onChange={() => {}}></OlCheckbox>
<span>Keep units relative positions</span>
</div>
<div
className={`
group flex flex-row gap-4 rounded-md justify-content
cursor-pointer p-2 text-sm
dark:hover:bg-olympus-400
`}
onClick={() => getApp().getMap().setOption("hideGroupMembers", !mapOptions.hideGroupMembers)}
>
<OlCheckbox checked={mapOptions.hideGroupMembers} onChange={() => {}}></OlCheckbox>
<span>Hide Group members</span>
</div>
<div
className={`
group flex flex-row gap-4 rounded-md justify-content
cursor-pointer p-2 text-sm
dark:hover:bg-olympus-400
`}
onClick={() => getApp().getMap().setOption("showMinimap", !mapOptions.showMinimap)}
>
<OlCheckbox checked={mapOptions.showMinimap} onChange={() => {}}></OlCheckbox>
<span>Show minimap</span>
</div>
</OlAccordion>
<OlAccordion onClick={() => setOpenAccordion(openAccordion === Accordion.NONE ? Accordion.CAMERA_PLUGIN: Accordion.NONE )} open={openAccordion === Accordion.CAMERA_PLUGIN} title="Camera plugin options">
<hr
className={`
m-2 my-1 w-auto border-[1px] bg-gray-700
dark:border-olympus-500
`}></hr>
<div className={`
`}
></hr>
<div
className={`
flex flex-col content-center items-start justify-between gap-2 p-2
`}>
<div className="flex flex-col">
<span className={`
font-normal
dark:text-white
`}>DCS Camera Zoom Scaling</span>
</div>
<OlRangeSlider
onChange={(ev) => {
getApp().getMap().setOption("cameraPluginRatio", parseInt(ev.target.value))
}}
value={mapOptions.cameraPluginRatio}
min={0}
max={100}
step={1}
/>
</div>
<div className={`
flex flex-col content-center items-start justify-between gap-2 p-2
`}>
<span className={`
`}
>
<div className="flex flex-col">
<span
className={`
font-normal
dark:text-white
`}>DCS Camera Port</span>
<div className="flex">
<OlNumberInput
value={mapOptions.cameraPluginPort}
min={0}
max={9999}
onDecrease={() => { getApp().getMap().setOption("cameraPluginPort", mapOptions.cameraPluginPort - 1) }}
onIncrease={() => { getApp().getMap().setOption("cameraPluginPort", mapOptions.cameraPluginPort + 1) }}
onChange={(ev) => { getApp().getMap().setOption("cameraPluginPort", ev.target.value)}}
/>
</div>
</div>
`}
>
DCS Camera Zoom Scaling
</span>
</div>
<OlRangeSlider
onChange={(ev) => getApp().getMap().setOption("cameraPluginRatio", parseInt(ev.target.value))}
value={mapOptions.cameraPluginRatio}
min={0}
max={100}
step={1}
/>
</div>
<div
className={`
flex flex-col content-center items-start justify-between gap-2 p-2
`}
>
<span
className={`
font-normal
dark:text-white
`}
>
DCS Camera Port
</span>
<div className="flex">
<OlNumberInput
value={mapOptions.cameraPluginPort}
min={0}
max={9999}
onDecrease={() =>
getApp()
.getMap()
.setOption("cameraPluginPort", mapOptions.cameraPluginPort - 1)
}
onIncrease={() =>
getApp()
.getMap()
.setOption("cameraPluginPort", mapOptions.cameraPluginPort + 1)
}
onChange={(ev) => getApp().getMap().setOption("cameraPluginPort", ev.target.value)}
/>
</div>
</div>
</OlAccordion>
</div>
</Menu>
);

View File

@ -9,29 +9,25 @@ import { SideBar } from "./panels/sidebar";
import { OptionsMenu } from "./panels/optionsmenu";
import { MapHiddenTypes, MapOptions } from "../types/types";
import {
BLUE_COMMANDER,
GAME_MASTER,
MAP_OPTIONS_DEFAULTS,
NO_SUBSTATE,
OlympusState,
OlympusSubState,
RED_COMMANDER,
OptionsSubstate,
UnitControlSubState,
} from "../constants/constants";
import { getApp, setupApp } from "../olympusapp";
import { LoginModal } from "./modals/login";
import { sha256 } from "js-sha256";
import { MiniMapPanel } from "./panels/minimappanel";
import { UnitControlBar } from "./panels/unitcontrolbar";
import { DrawingMenu } from "./panels/drawingmenu";
import { ControlsPanel } from "./panels/controlspanel";
import { MapContextMenu } from "./contextmenus/mapcontextmenu";
import { AirbaseMenu } from "./panels/airbasemenu";
import { Airbase } from "../mission/airbase";
import { AudioMenu } from "./panels/audiomenu";
import { FormationMenu } from "./panels/formationmenu";
import { Unit } from "../unit/unit";
import { ProtectionPrompt } from "./modals/protectionprompt";
import { KeybindModal } from "./modals/keybindmodal";
import { UnitExplosionMenu } from "./panels/unitexplosionmenu";
import { JTACMenu } from "./panels/jtacmenu";
import { AppStateChangedEvent, MapOptionsChangedEvent } from "../events";
@ -54,10 +50,6 @@ export type OlympusUIState = {
export function UI() {
const [appState, setAppState] = useState(OlympusState.NOT_INITIALIZED);
const [appSubState, setAppSubState] = useState(NO_SUBSTATE as OlympusSubState);
const [checkingPassword, setCheckingPassword] = useState(false);
const [loginError, setLoginError] = useState(false);
const [commandMode, setCommandMode] = useState(null as null | string);
useEffect(() => {
AppStateChangedEvent.on((state, subState) => {
@ -66,34 +58,7 @@ export function UI() {
});
}, []);
function checkPassword(password: string) {
setCheckingPassword(true);
var hash = sha256.create();
getApp().getServerManager().setPassword(hash.update(password).hex());
getApp()
.getServerManager()
.getMission(
(response) => {
const commandMode = response.mission.commandModeOptions.commandMode;
try {
[GAME_MASTER, BLUE_COMMANDER, RED_COMMANDER].includes(commandMode) ? setCommandMode(commandMode) : setLoginError(true);
} catch {
setLoginError(true);
}
setCheckingPassword(false);
},
() => {
setLoginError(true);
setCheckingPassword(false);
}
);
}
function connect(username: string) {
getApp().getServerManager().setUsername(username);
getApp().getServerManager().startUpdate();
getApp().setState(OlympusState.IDLE);
}
return (
<div
@ -111,36 +76,25 @@ export function UI() {
fixed left-0 top-0 z-30 h-full w-full bg-[#111111]/95
`}></div>
<LoginModal
onLogin={(password) => {
checkPassword(password);
}}
onContinue={(username) => {
connect(username);
}}
onBack={() => {
setCommandMode(null);
}}
checkingPassword={checkingPassword}
loginError={loginError}
commandMode={commandMode}
/>
</>
)}
{appState === OlympusState.UNIT_CONTROL && appSubState == UnitControlSubState.PROTECTION && (
<>
<div className={`
fixed left-0 top-0 z-30 h-full w-full bg-[#111111]/95
`}></div>
<ProtectionPrompt />
</>
)}
<ProtectionPrompt open={appState === OlympusState.UNIT_CONTROL && appSubState == UnitControlSubState.PROTECTION} />
<KeybindModal open={appState === OlympusState.OPTIONS && appSubState === OptionsSubstate.KEYBIND} />
<div id="map-container" className="z-0 h-full w-screen" />
<MainMenu open={appState === OlympusState.MAIN_MENU} onClose={() => getApp().setState(OlympusState.IDLE)} />
<SpawnMenu open={appState === OlympusState.SPAWN} onClose={() => getApp().setState(OlympusState.IDLE)} />
<OptionsMenu open={appState === OlympusState.OPTIONS} onClose={() => getApp().setState(OlympusState.IDLE)}/>
<OptionsMenu open={appState === OlympusState.OPTIONS} onClose={() => getApp().setState(OlympusState.IDLE)} />
<UnitControlMenu
open={appState === OlympusState.UNIT_CONTROL && ![UnitControlSubState.FORMATION, UnitControlSubState.UNIT_EXPLOSION_MENU].includes(appSubState as UnitControlSubState)}
open={
appState === OlympusState.UNIT_CONTROL &&
![UnitControlSubState.FORMATION, UnitControlSubState.UNIT_EXPLOSION_MENU].includes(appSubState as UnitControlSubState)
}
onClose={() => getApp().setState(OlympusState.IDLE)}
/>
<FormationMenu
@ -149,11 +103,14 @@ export function UI() {
/>
<DrawingMenu open={appState === OlympusState.DRAW} onClose={() => getApp().setState(OlympusState.IDLE)} />
<AirbaseMenu open={appState === OlympusState.AIRBASE} onClose={() => getApp().setState(OlympusState.IDLE)}/>
<AirbaseMenu open={appState === OlympusState.AIRBASE} onClose={() => getApp().setState(OlympusState.IDLE)} />
<AudioMenu open={appState === OlympusState.AUDIO} onClose={() => getApp().setState(OlympusState.IDLE)} />
<GameMasterMenu open={appState === OlympusState.GAME_MASTER} onClose={() => getApp().setState(OlympusState.IDLE)} />
<UnitExplosionMenu open={appState === OlympusState.UNIT_CONTROL && appSubState === UnitControlSubState.UNIT_EXPLOSION_MENU} onClose={() => getApp().setState(OlympusState.IDLE)} />
<UnitExplosionMenu
open={appState === OlympusState.UNIT_CONTROL && appSubState === UnitControlSubState.UNIT_EXPLOSION_MENU}
onClose={() => getApp().setState(OlympusState.IDLE)}
/>
<JTACMenu open={appState === OlympusState.JTAC} onClose={() => getApp().setState(OlympusState.IDLE)} />
<MiniMapPanel />

View File

@ -30,10 +30,8 @@ import {
CommandModeOptionsChangedEvent,
ContactsUpdatedEvent,
HotgroupsChangedEvent,
InfoPopupEvent,
SelectedUnitsChangedEvent,
SelectionClearedEvent,
UnitDatabaseLoadedEvent,
UnitDeselectedEvent,
UnitSelectedEvent,
} from "../events";
@ -72,9 +70,63 @@ export class UnitsManager {
UnitSelectedEvent.on((unit) => this.#onUnitSelection(unit));
UnitDeselectedEvent.on((unit) => this.#onUnitDeselection(unit));
document.addEventListener("copy", () => this.copy());
document.addEventListener("keyup", (event) => this.#onKeyUp(event));
document.addEventListener("paste", () => this.paste());
getApp()
.getShortcutManager()
.addShortcut("deselectAll", {
label: "Deselect all units",
keyUpCallback: (ev: KeyboardEvent) => {
getApp().getUnitsManager().deselectAllUnits();
},
code: "Escape",
})
.addShortcut("delete", {
label: "Delete selected units",
keyUpCallback: () => this.delete(),
code: "Delete",
})
.addShortcut("selectAll", {
label: "Select all units",
keyUpCallback: () => {
Object.values(this.getUnits())
.filter((unit: Unit) => {
return !unit.getHidden();
})
.forEach((unit: Unit) => unit.setSelected(true));
},
code: "KeyA",
ctrlKey: true,
})
.addShortcut("copyUnits", {
label: "Copy units",
keyUpCallback: () => this.copy(),
code: "KeyC",
ctrlKey: true,
})
.addShortcut("pasteUnits", {
label: "Paste units",
keyUpCallback: () => this.paste(),
code: "KeyV",
ctrlKey: true,
});
const digits = ["Digit1", "Digit2", "Digit3", "Digit4", "Digit5", "Digit6", "Digit7", "Digit8", "Digit9"];
digits.forEach((code, idx) => {
getApp()
.getShortcutManager()
.addShortcut(`hotgroup${idx}`, {
label: `Hotgroup ${idx} management`,
keyUpCallback: (ev: KeyboardEvent) => {
if (ev.ctrlKey && ev.shiftKey) this.selectUnitsByHotgroup(parseInt(ev.code.substring(5)), false);
// "Select hotgroup X in addition to any units already selected"
else if (ev.ctrlKey && !ev.shiftKey) this.setHotgroup(parseInt(ev.code.substring(5)));
// "These selected units are hotgroup X (forget any previous membership)"
else if (!ev.ctrlKey && ev.shiftKey) this.addToHotgroup(parseInt(ev.code.substring(5)));
// "Add (append) these units to hotgroup X (in addition to any existing members)"
else this.selectUnitsByHotgroup(parseInt(ev.code.substring(5))); // "Select hotgroup X, deselect any units not in it."
},
code: code,
});
});
//this.#slowDeleteDialog = new Dialog("slow-delete-dialog");
}
@ -1059,21 +1111,19 @@ export class UnitsManager {
units.forEach((unit: Unit) => unit.setHotgroup(hotgroup));
this.#showActionMessage(units, `added to hotgroup ${hotgroup}`);
let hotgroups: {[key: number]: number} = {};
let hotgroups: { [key: number]: number } = {};
for (let ID in this.#units) {
const unit = this.#units[ID]
const unit = this.#units[ID];
if (unit.getAlive() && !unit.getHuman()) {
const hotgroup = unit.getHotgroup()
const hotgroup = unit.getHotgroup();
if (hotgroup) {
if (!(hotgroup in hotgroups)) {
hotgroups[hotgroup] = 1;
}
else
hotgroups[hotgroup] += 1;
} else hotgroups[hotgroup] += 1;
}
}
}
HotgroupsChangedEvent.dispatch(hotgroups)
HotgroupsChangedEvent.dispatch(hotgroups);
}
/** Delete the selected units
@ -1481,18 +1531,6 @@ export class UnitsManager {
}
/***********************************************/
#onKeyUp(event: KeyboardEvent) {
if (!keyEventWasInInput(event)) {
if (event.key === "Delete") this.delete();
else if (event.key === "a" && event.ctrlKey)
Object.values(this.getUnits())
.filter((unit: Unit) => {
return !unit.getHidden();
})
.forEach((unit: Unit) => unit.setSelected(true));
}
}
#onUnitSelection(unit: Unit) {
if (this.getSelectedUnits().length > 0) {
/* Disable the firing of the selection event for a certain amount of time. This avoids firing many events if many units are selected */

View File

@ -7,12 +7,27 @@ module.exports = function (configLocation) {
if (fs.existsSync(configLocation)) {
let rawdata = fs.readFileSync(configLocation, "utf-8");
const config = JSON.parse(rawdata);
res.send(JSON.stringify({...config.frontend, ...(config.audio ?? {}) }));
res.send(JSON.stringify({frontend:{...config.frontend}, audio:{...(config.audio ?? {})}, profiles: {...(config.profiles ?? {})} }));
res.end()
} else {
res.sendStatus(404);
}
});
router.put('/profile/:profileName', function (req, res, next) {
if (fs.existsSync(configLocation)) {
let rawdata = fs.readFileSync(configLocation, "utf-8");
const config = JSON.parse(rawdata);
if (config.profiles === undefined)
config.profiles = {}
config.profiles[req.params.profileName] = req.body;
fs.writeFileSync(configLocation, JSON.stringify(config, null, 2), "utf-8");
res.end()
} else {
res.sendStatus(404);
}
});
return router;
}