Multiple bugfixes

This commit is contained in:
Davide Passoni
2024-11-22 14:56:26 +01:00
parent 5c3960868a
commit 897afb2fef
25 changed files with 498 additions and 532 deletions

View File

@@ -199,7 +199,7 @@ export class AudioManager {
addRadio() { addRadio() {
console.log("Adding new radio"); console.log("Adding new radio");
if (!this.#running) { if (!this.#running || this.#sources[0] === undefined) {
console.log("Audio manager not started, aborting..."); console.log("Audio manager not started, aborting...");
return; return;
} }

View File

@@ -13,7 +13,7 @@ export class RadioSink extends AudioSink {
#frequency = 251000000; #frequency = 251000000;
#modulation = 0; #modulation = 0;
#ptt = false; #ptt = false;
#tuned = false; #tuned = true;
#volume = 0.5; #volume = 0.5;
#receiving = false; #receiving = false;
#clearReceivingTimeout: number; #clearReceivingTimeout: number;

View File

@@ -529,8 +529,8 @@ export namespace ContextActions {
executeImmediately: true, executeImmediately: true,
type: ContextActionType.DELETE, type: ContextActionType.DELETE,
code: "Delete", code: "Delete",
ctrlKey: true, ctrlKey: false,
shiftKey: false, shiftKey: true,
altKey: false, altKey: false,
} }
); );
@@ -562,7 +562,7 @@ export namespace ContextActions {
export const FOLLOW = new ContextAction( export const FOLLOW = new ContextAction(
"follow", "follow",
"Follow unit", "Follow unit",
"Click on a unit to follow it in formation", "Right-click on a unit to follow it in formation",
olButtonsContextFollow, olButtonsContextFollow,
ContextActionTarget.UNIT, ContextActionTarget.UNIT,
(units: Unit[], targetUnit: Unit | null, _) => { (units: Unit[], targetUnit: Unit | null, _) => {
@@ -580,7 +580,7 @@ export namespace ContextActions {
export const BOMB = new ContextAction( export const BOMB = new ContextAction(
"bomb", "bomb",
"Precision bomb location", "Precision bomb location",
"Click on a point to execute a precision bombing attack", "Right-click on a point to execute a precision bombing attack",
faLocationCrosshairs, faLocationCrosshairs,
ContextActionTarget.POINT, ContextActionTarget.POINT,
(units: Unit[], _, targetPosition: LatLng | null) => { (units: Unit[], _, targetPosition: LatLng | null) => {
@@ -595,7 +595,7 @@ export namespace ContextActions {
export const CARPET_BOMB = new ContextAction( export const CARPET_BOMB = new ContextAction(
"carpet-bomb", "carpet-bomb",
"Carpet bomb location", "Carpet bomb location",
"Click on a point to execute a carpet bombing attack", "Right-click on a point to execute a carpet bombing attack",
faXmarksLines, faXmarksLines,
ContextActionTarget.POINT, ContextActionTarget.POINT,
(units: Unit[], _, targetPosition: LatLng | null) => { (units: Unit[], _, targetPosition: LatLng | null) => {
@@ -610,7 +610,7 @@ export namespace ContextActions {
export const LAND = new ContextAction( export const LAND = new ContextAction(
"land", "land",
"Land", "Land",
"Click on a point to land at the nearest airbase", "Right-click on a point to land at the nearest airbase",
faPlaneArrival, faPlaneArrival,
ContextActionTarget.POINT, ContextActionTarget.POINT,
(units: Unit[], _, targetPosition: LatLng | null) => { (units: Unit[], _, targetPosition: LatLng | null) => {
@@ -622,7 +622,7 @@ export namespace ContextActions {
export const LAND_AT_POINT = new ContextAction( export const LAND_AT_POINT = new ContextAction(
"land-at-point", "land-at-point",
"Land at location", "Land at location",
"Click on a point to land there", "Right-click on a point to land there",
olButtonsContextLandAtPoint, olButtonsContextLandAtPoint,
ContextActionTarget.POINT, ContextActionTarget.POINT,
(units: Unit[], _, targetPosition: LatLng | null) => { (units: Unit[], _, targetPosition: LatLng | null) => {
@@ -649,7 +649,7 @@ export namespace ContextActions {
export const ATTACK = new ContextAction( export const ATTACK = new ContextAction(
"attack", "attack",
"Attack unit", "Attack unit",
"Click on a unit to attack it", "Right-click on a unit to attack it",
olButtonsContextAttack, olButtonsContextAttack,
ContextActionTarget.UNIT, ContextActionTarget.UNIT,
(units: Unit[], targetUnit: Unit | null, _) => { (units: Unit[], targetUnit: Unit | null, _) => {
@@ -661,7 +661,7 @@ export namespace ContextActions {
export const FIRE_AT_AREA = new ContextAction( export const FIRE_AT_AREA = new ContextAction(
"fire-at-area", "fire-at-area",
"Fire at area", "Fire at area",
"Click on a point to precisely fire at it (if possible)", "Right-click on a point to precisely fire at it (if possible)",
faLocationCrosshairs, faLocationCrosshairs,
ContextActionTarget.POINT, ContextActionTarget.POINT,
(units: Unit[], _, targetPosition: LatLng | null) => { (units: Unit[], _, targetPosition: LatLng | null) => {

View File

@@ -137,6 +137,19 @@ export class BindShortcutRequestEvent {
} }
} }
export class ModalEvent {
static on(callback: (modal: boolean) => void) {
document.addEventListener(this.name, (ev: CustomEventInit) => {
callback(ev.detail.modal);
});
}
static dispatch(modal: boolean) {
document.dispatchEvent(new CustomEvent(this.name, { detail: { modal } }));
console.log(`Event ${this.name} dispatched`);
}
}
/************** Map events ***************/ /************** Map events ***************/
export class HiddenTypesChangedEvent { export class HiddenTypesChangedEvent {
static on(callback: (hiddenTypes: MapHiddenTypes) => void) { static on(callback: (hiddenTypes: MapHiddenTypes) => void) {

View File

@@ -5,9 +5,10 @@ import { DomEvent } from "leaflet";
import { LatLngBounds } from "leaflet"; import { LatLngBounds } from "leaflet";
import { Bounds } from "leaflet"; import { Bounds } from "leaflet";
import { SELECT_TOLERANCE_PX } from "../constants/constants"; import { SELECT_TOLERANCE_PX } from "../constants/constants";
import { Map } from "./map";
export var BoxSelect = Handler.extend({ export var BoxSelect = Handler.extend({
initialize: function (map) { initialize: function (map: Map) {
this._map = map; this._map = map;
this._container = map.getContainer(); this._container = map.getContainer();
this._pane = map.getPanes().overlayPane; this._pane = map.getPanes().overlayPane;
@@ -45,13 +46,12 @@ export var BoxSelect = Handler.extend({
}, },
_onMouseDown: function (e: any) { _onMouseDown: function (e: any) {
if (e.which == 1 && e.button == 0) { if (this._map.getEnableSelection() && e.button == 0) {
// Clear the deferred resetState if it hasn't executed yet, otherwise it // Clear the deferred resetState if it hasn't executed yet, otherwise it
// will interrupt the interaction and orphan a box element in the container. // will interrupt the interaction and orphan a box element in the container.
this._clearDeferredResetState(); this._clearDeferredResetState();
this._resetState(); this._resetState();
DomUtil.disableTextSelection();
DomUtil.disableImageDrag(); DomUtil.disableImageDrag();
this._map.dragging.disable(); this._map.dragging.disable();
@@ -65,10 +65,8 @@ export var BoxSelect = Handler.extend({
contextmenu: DomEvent.stop, contextmenu: DomEvent.stop,
touchmove: this._onMouseMove, touchmove: this._onMouseMove,
touchend: this._onMouseUp, touchend: this._onMouseUp,
touchstart: this._onKeyDown,
mousemove: this._onMouseMove, mousemove: this._onMouseMove,
mouseup: this._onMouseUp, mouseup: this._onMouseUp
keydown: this._onKeyDown,
}, },
this this
); );
@@ -77,6 +75,24 @@ export var BoxSelect = Handler.extend({
} }
}, },
_onMouseUp: function (e: any) {
if (e.button !== 0) {
return;
}
this._finish();
if (!this._moved) {
return;
}
// Postpone to next JS tick so internal click event handling
// still see it as "moved".
window.setTimeout(Util.bind(this._resetState, this), 0);
var bounds = new LatLngBounds(this._map.containerPointToLatLng(this._startPoint), this._map.containerPointToLatLng(this._point));
this._map.fire("selectionend", { selectionBounds: bounds });
},
_onMouseMove: function (e: any) { _onMouseMove: function (e: any) {
if (e.type === "touchmove") this._point = this._map.mouseEventToContainerPoint(e.touches[0]); if (e.type === "touchmove") this._point = this._map.mouseEventToContainerPoint(e.touches[0]);
else this._point = this._map.mouseEventToContainerPoint(e); else this._point = this._map.mouseEventToContainerPoint(e);
@@ -110,7 +126,6 @@ export var BoxSelect = Handler.extend({
DomUtil.removeClass(this._container, "leaflet-crosshair"); DomUtil.removeClass(this._container, "leaflet-crosshair");
} }
DomUtil.enableTextSelection();
DomUtil.enableImageDrag(); DomUtil.enableImageDrag();
this._map.dragging.enable(); this._map.dragging.enable();
@@ -121,38 +136,10 @@ export var BoxSelect = Handler.extend({
contextmenu: DomEvent.stop, contextmenu: DomEvent.stop,
touchmove: this._onMouseMove, touchmove: this._onMouseMove,
touchend: this._onMouseUp, touchend: this._onMouseUp,
touchstart: this._onKeyDown,
mousemove: this._onMouseMove, mousemove: this._onMouseMove,
mouseup: this._onMouseUp, mouseup: this._onMouseUp,
keydown: this._onKeyDown,
}, },
this this
); );
}, },
_onMouseUp: function (e: any) {
if (e.which !== 1 && e.button !== 0 && e.type !== "touchend") {
return;
}
this._finish();
if (!this._moved) {
return;
}
// Postpone to next JS tick so internal click event handling
// still see it as "moved".
window.setTimeout(Util.bind(this._resetState, this), 0);
var bounds = new LatLngBounds(this._map.containerPointToLatLng(this._startPoint), this._map.containerPointToLatLng(this._point));
this._map.fire("selectionend", { selectionBounds: bounds });
},
_onKeyDown: function (e: any) {
if (e.keyCode === 27) {
this._finish();
this._clearDeferredResetState();
this._resetState();
}
},
}); });

View File

@@ -43,7 +43,6 @@ import { ExplosionMarker } from "./markers/explosionmarker";
import { TextMarker } from "./markers/textmarker"; import { TextMarker } from "./markers/textmarker";
import { TargetMarker } from "./markers/targetmarker"; import { TargetMarker } from "./markers/targetmarker";
import { import {
AirbaseSelectedEvent,
AppStateChangedEvent, AppStateChangedEvent,
CoalitionAreaSelectedEvent, CoalitionAreaSelectedEvent,
ConfigLoadedEvent, ConfigLoadedEvent,
@@ -114,6 +113,7 @@ export class Map extends L.Map {
#lastMouseCoordinates: L.LatLng = new L.LatLng(0, 0); #lastMouseCoordinates: L.LatLng = new L.LatLng(0, 0);
#previousZoom: number = 0; #previousZoom: number = 0;
#keepRelativePositions: boolean = false; #keepRelativePositions: boolean = false;
#enableSelection: boolean = false;
/* Camera control plugin */ /* Camera control plugin */
#slaveDCSCamera: boolean = false; #slaveDCSCamera: boolean = false;
@@ -360,6 +360,14 @@ export class Map extends L.Map {
shiftKey: false, shiftKey: false,
ctrlKey: false, ctrlKey: false,
}) })
.addShortcut("toggleEnableSelection", {
label: "Toggle box selection",
keyUpCallback: () => this.setEnableSelection(false),
keyDownCallback: () => this.setEnableSelection(true),
code: "ShiftLeft",
altKey: false,
ctrlKey: false,
})
.addShortcut("increaseCameraZoom", { .addShortcut("increaseCameraZoom", {
label: "Increase camera zoom", label: "Increase camera zoom",
keyUpCallback: () => this.increaseCameraZoom(), keyUpCallback: () => this.increaseCameraZoom(),
@@ -725,6 +733,14 @@ export class Map extends L.Map {
return this.#keepRelativePositions; return this.#keepRelativePositions;
} }
setEnableSelection(enableSelection: boolean) {
this.#enableSelection = enableSelection;
}
getEnableSelection() {
return this.#enableSelection;
}
increaseCameraZoom() { increaseCameraZoom() {
//const slider = document.querySelector(`label[title="${DCS_LINK_RATIO}"] input`); //const slider = document.querySelector(`label[title="${DCS_LINK_RATIO}"] input`);
//if (slider instanceof HTMLInputElement) { //if (slider instanceof HTMLInputElement) {
@@ -769,7 +785,6 @@ export class Map extends L.Map {
this.#currentEffectMarker = null; this.#currentEffectMarker = null;
if (state !== OlympusState.UNIT_CONTROL) getApp().getUnitsManager().deselectAllUnits(); if (state !== OlympusState.UNIT_CONTROL) getApp().getUnitsManager().deselectAllUnits();
if (state !== OlympusState.DRAW || (state === OlympusState.DRAW && subState !== DrawSubState.EDIT)) this.deselectAllCoalitionAreas(); if (state !== OlympusState.DRAW || (state === OlympusState.DRAW && subState !== DrawSubState.EDIT)) this.deselectAllCoalitionAreas();
AirbaseSelectedEvent.dispatch(null);
/* Operations to perform when entering a state */ /* Operations to perform when entering a state */
if (state === OlympusState.IDLE) { if (state === OlympusState.IDLE) {

View File

@@ -1,4 +1,4 @@
import { DivIcon } from "leaflet"; import { DivIcon, DomEvent } from "leaflet";
import { CustomMarker } from "../map/markers/custommarker"; import { CustomMarker } from "../map/markers/custommarker";
import { SVGInjector } from "@tanem/svg-injector"; import { SVGInjector } from "@tanem/svg-injector";
import { AirbaseChartData, AirbaseOptions } from "../interfaces"; import { AirbaseChartData, AirbaseOptions } from "../interfaces";
@@ -29,20 +29,28 @@ export class Airbase extends CustomMarker {
AppStateChangedEvent.on((state, subState) => { AppStateChangedEvent.on((state, subState) => {
const element = this.getElement(); const element = this.getElement();
if (element) if (element) element.style.pointerEvents = state === OlympusState.IDLE || state === OlympusState.AIRBASE ? "all" : "none";
element.style.pointerEvents = (state === OlympusState.IDLE || state === OlympusState.AIRBASE)? "all": "none"; });
})
AirbaseSelectedEvent.on((airbase) => { AirbaseSelectedEvent.on((airbase) => {
this.#selected = airbase == this; this.#selected = (airbase === this);
if (this.getElement()?.querySelector(".airbase-icon")) if (this.getElement()?.querySelector(".airbase-icon"))
(this.getElement()?.querySelector(".airbase-icon") as HTMLElement).dataset.selected = `${this.#selected}`; (this.getElement()?.querySelector(".airbase-icon") as HTMLElement).dataset.selected = `${this.#selected}`;
}) });
this.addEventListener("click", (ev) => { this.addEventListener("mousedown", (ev) => {
if (getApp().getState() === OlympusState.IDLE || getApp().getState() === OlympusState.AIRBASE) { if (getApp().getState() === OlympusState.IDLE || getApp().getState() === OlympusState.AIRBASE) {
getApp().setState(OlympusState.AIRBASE) DomEvent.stop(ev);
AirbaseSelectedEvent.dispatch(this) ev.originalEvent.stopImmediatePropagation();
}
});
this.addEventListener("mouseup", (ev) => {
if (getApp().getState() === OlympusState.IDLE || getApp().getState() === OlympusState.AIRBASE) {
DomEvent.stop(ev);
ev.originalEvent.stopImmediatePropagation();
getApp().setState(OlympusState.AIRBASE);
AirbaseSelectedEvent.dispatch(this);
} }
}); });
} }
@@ -110,4 +118,8 @@ export class Airbase extends CustomMarker {
getImg() { getImg() {
return this.#img; return this.#img;
} }
getSelected() {
return this.#selected;
}
} }

View File

@@ -6,8 +6,7 @@ import { BLUE_COMMANDER, GAME_MASTER, NONE, RED_COMMANDER } from "../constants/c
import { AirbasesData, BullseyesData, CommandModeOptions, DateAndTime, MissionData } from "../interfaces"; import { AirbasesData, BullseyesData, CommandModeOptions, DateAndTime, MissionData } from "../interfaces";
import { Coalition } from "../types/types"; import { Coalition } from "../types/types";
import { Carrier } from "./carrier"; import { Carrier } from "./carrier";
import { NavyUnit } from "../unit/unit"; import { AirbaseSelectedEvent, AppStateChangedEvent, BullseyesDataChanged, CommandModeOptionsChangedEvent, InfoPopupEvent } from "../events";
import { BullseyesDataChanged, CommandModeOptionsChangedEvent, InfoPopupEvent } from "../events";
/** The MissionManager */ /** The MissionManager */
export class MissionManager { export class MissionManager {
@@ -34,7 +33,12 @@ export class MissionManager {
#spentSpawnPoint: number = 0; #spentSpawnPoint: number = 0;
#coalitions: { red: string[]; blue: string[] } = { red: [], blue: [] }; #coalitions: { red: string[]; blue: string[] } = { red: [], blue: [] };
constructor() {} constructor() {
AppStateChangedEvent.on((state, subState) => {
if (this.getSelectedAirbase() !== null) AirbaseSelectedEvent.dispatch(null);
})
}
/** Update location of bullseyes /** Update location of bullseyes
* *
@@ -209,6 +213,14 @@ export class MissionManager {
return this.#frameRate; return this.#frameRate;
} }
getSelectedAirbase() {
const airbase = Object.values(this.#airbases).find((airbase: Airbase | Carrier) => {
return airbase.getSelected();
});
return airbase ?? null;
}
#setcommandModeOptions(commandModeOptions: CommandModeOptions) { #setcommandModeOptions(commandModeOptions: CommandModeOptions) {
/* Refresh all the data if we have exited the NONE state */ /* Refresh all the data if we have exited the NONE state */
var requestRefresh = false; var requestRefresh = false;

View File

@@ -1,4 +1,5 @@
import { AppStateChangedEvent, ShortcutChangedEvent, ShortcutsChangedEvent } from "../events"; import { OlympusState } from "../constants/constants";
import { AppStateChangedEvent, ModalEvent, ShortcutChangedEvent, ShortcutsChangedEvent } from "../events";
import { ShortcutOptions } from "../interfaces"; import { ShortcutOptions } from "../interfaces";
import { keyEventWasInInput } from "../other/utils"; import { keyEventWasInInput } from "../other/utils";
@@ -6,34 +7,40 @@ export class Shortcut {
#id: string; #id: string;
#options: ShortcutOptions; #options: ShortcutOptions;
#keydown: boolean = false; #keydown: boolean = false;
#modal: boolean = false;
constructor(id, options: ShortcutOptions) { constructor(id, options: ShortcutOptions) {
this.#id = id; this.#id = id;
this.#options = options; this.#options = options;
AppStateChangedEvent.on(() => (this.#keydown = false)); AppStateChangedEvent.on((state, subState) => (this.#keydown = false));
ModalEvent.on((modal) => (this.#modal = modal))
/* On keyup, it is enough to check the code only, not the entire combination */ /* On keyup, it is enough to check the code only, not the entire combination */
document.addEventListener("keyup", (ev: any) => { document.addEventListener("keyup", (ev: any) => {
if (this.#keydown && options.code === ev.code) { if (this.#modal) return;
if (this.#keydown && this.getOptions().code === ev.code) {
console.log(`Keydown up for shortcut ${this.#id}`)
ev.preventDefault(); ev.preventDefault();
options.keyUpCallback(ev);
this.#keydown = false; this.#keydown = false;
this.getOptions().keyUpCallback(ev);
} }
}); });
/* On keydown, check exactly if the requested key combination is being pressed */ /* On keydown, check exactly if the requested key combination is being pressed */
document.addEventListener("keydown", (ev: any) => { document.addEventListener("keydown", (ev: any) => {
if (this.#modal) return;
if ( if (
!(this.#keydown || keyEventWasInInput(ev) || options.code !== ev.code) && !(this.#keydown || keyEventWasInInput(ev) || this.getOptions().code !== ev.code) &&
(options.altKey === undefined || ev.altKey === (options.altKey ?? ev.code.indexOf("Alt") >= 0)) && (this.getOptions().altKey === undefined || ev.altKey === (this.getOptions().altKey ?? ev.code.indexOf("Alt") >= 0)) &&
(options.ctrlKey === undefined || ev.ctrlKey === (options.ctrlKey ?? ev.code.indexOf("Control") >= 0)) && (this.getOptions().ctrlKey === undefined || ev.ctrlKey === (this.getOptions().ctrlKey ?? ev.code.indexOf("Control") >= 0)) &&
(options.shiftKey === undefined || ev.shiftKey === (options.shiftKey ?? ev.code.indexOf("Shift") >= 0)) (this.getOptions().shiftKey === undefined || ev.shiftKey === (this.getOptions().shiftKey ?? ev.code.indexOf("Shift") >= 0))
) { ) {
console.log(`Keydown event for shortcut ${this.#id}`)
ev.preventDefault(); ev.preventDefault();
this.#keydown = true; this.#keydown = true;
const keyDownCallback = this.getOptions().keyDownCallback
if (options.keyDownCallback) options.keyDownCallback(ev); /* Key down event is optional */ if (keyDownCallback) keyDownCallback(ev); /* Key down event is optional */
} }
}); });
} }
@@ -43,7 +50,7 @@ export class Shortcut {
} }
setOptions(options: ShortcutOptions) { setOptions(options: ShortcutOptions) {
this.#options = { ...options }; this.#options = { ...this.#options, ...options };
} }
getId() { getId() {
@@ -54,14 +61,17 @@ export class Shortcut {
let actions: string[] = []; let actions: string[] = [];
if (this.getOptions().shiftKey) actions.push("Shift"); if (this.getOptions().shiftKey) actions.push("Shift");
if (this.getOptions().altKey) actions.push("Alt"); if (this.getOptions().altKey) actions.push("Alt");
if (this.getOptions().ctrlKey) actions.push("Ctrl") if (this.getOptions().ctrlKey) actions.push("Ctrl");
actions.push(this.getOptions().code.replace("Key", "") actions.push(
.replace("ControlLeft", "Left Ctrl") this.getOptions()
.replace("AltLeft", "Left Alt") .code.replace("Key", "")
.replace("ShiftLeft", "Left Shift") .replace("ControlLeft", "Left Ctrl")
.replace("ControlRight", "Right Ctrl") .replace("AltLeft", "Left Alt")
.replace("AltRight", "Right Alt") .replace("ShiftLeft", "Left Shift")
.replace("ShiftRight", "Right Shift")) .replace("ControlRight", "Right Ctrl")
return actions .replace("AltRight", "Right Alt")
.replace("ShiftRight", "Right Shift")
);
return actions;
} }
} }

View File

@@ -1,4 +1,4 @@
import { ShortcutChangedEvent, ShortcutsChangedEvent } from "../events"; import { ShortcutsChangedEvent } from "../events";
import { ShortcutOptions } from "../interfaces"; import { ShortcutOptions } from "../interfaces";
import { Shortcut } from "./shortcut"; import { Shortcut } from "./shortcut";

View File

@@ -1,15 +1,28 @@
import React from "react"; import React, { useEffect } from "react";
import { ModalEvent } from "../../../events";
export function Modal(props: { open: boolean; children?: JSX.Element | JSX.Element[]; className?: string }) {
useEffect(() => {
ModalEvent.dispatch(props.open);
}, [props.open]);
export function Modal(props: { grayout?: boolean; children?: JSX.Element | JSX.Element[]; className?: string }) {
return ( return (
<div <>
className={` {props.open && (
${props.className} <>
fixed left-[50%] top-[50%] z-40 translate-x-[-50%] translate-y-[-50%] <div className={`fixed left-0 top-0 z-30 h-full w-full bg-[#111111]/95`}></div>
rounded-xl border-[1px] border-solid border-gray-700 drop-shadow-md <div
`} className={`
> ${props.className}
{props.children} fixed left-[50%] top-[50%] z-40 translate-x-[-50%]
</div> translate-y-[-50%] rounded-xl border-[1px] border-solid
border-gray-700 drop-shadow-md
`}
>
{props.children}
</div>
</>
)}
</>
); );
} }

View File

@@ -6,7 +6,6 @@ import { getApp } from "../../olympusapp";
import { OlympusState } from "../../constants/constants"; import { OlympusState } from "../../constants/constants";
import { Shortcut } from "../../shortcut/shortcut"; import { Shortcut } from "../../shortcut/shortcut";
import { BindShortcutRequestEvent, ShortcutsChangedEvent } from "../../events"; import { BindShortcutRequestEvent, ShortcutsChangedEvent } from "../../events";
import { OlToggle } from "../components/oltoggle";
export function KeybindModal(props: { open: boolean }) { export function KeybindModal(props: { open: boolean }) {
const [shortcuts, setShortcuts] = useState({} as { [key: string]: Shortcut }); const [shortcuts, setShortcuts] = useState({} as { [key: string]: Shortcut });
@@ -23,180 +22,139 @@ export function KeybindModal(props: { open: boolean }) {
document.addEventListener("keydown", (ev) => { document.addEventListener("keydown", (ev) => {
if (ev.code) { if (ev.code) {
setCode(ev.code); setCode(ev.code);
if (ev.code.indexOf("Control") < 0) setCtrlKey(ev.ctrlKey);
else setCtrlKey(false);
if (ev.code.indexOf("Shift") < 0) setShiftKey(ev.shiftKey);
else setShiftKey(false);
if (ev.code.indexOf("Alt") < 0) setAltKey(ev.altKey);
else setAltKey(false);
} }
}); });
}, []); }, []);
useEffect(() => { useEffect(() => {
setCode(shortcut?.getOptions().code ?? null) setCode(shortcut?.getOptions().code ?? null);
setShiftKey(shortcut?.getOptions().shiftKey) setShiftKey(shortcut?.getOptions().shiftKey);
setAltKey(shortcut?.getOptions().altKey) setAltKey(shortcut?.getOptions().altKey);
setCtrlKey(shortcut?.getOptions().ctrlKey) setCtrlKey(shortcut?.getOptions().ctrlKey);
}, [shortcut]) }, [shortcut]);
let available: null | boolean = code ? true : null; let available: null | boolean = code ? true : null;
let inUseShortcut: null | Shortcut = null; let inUseShortcuts: Shortcut[] = [];
for (let id in shortcuts) { for (let id in shortcuts) {
if (id !== shortcut?.getId() && if (
id !== shortcut?.getId() &&
code === shortcuts[id].getOptions().code && code === shortcuts[id].getOptions().code &&
((shiftKey === undefined && shortcuts[id].getOptions().shiftKey !== undefined) || ((shiftKey === undefined && shortcuts[id].getOptions().shiftKey !== undefined) ||
(shiftKey !== undefined && shortcuts[id].getOptions().shiftKey === undefined) || (shiftKey !== undefined && shortcuts[id].getOptions().shiftKey === undefined) ||
shiftKey === shortcuts[id].getOptions().shiftKey) && ( shiftKey === shortcuts[id].getOptions().shiftKey) &&
(altKey === undefined && shortcuts[id].getOptions().altKey !== undefined) || ((altKey === undefined && shortcuts[id].getOptions().altKey !== undefined) ||
(altKey !== undefined && shortcuts[id].getOptions().altKey === undefined) || (altKey !== undefined && shortcuts[id].getOptions().altKey === undefined) ||
altKey === shortcuts[id].getOptions().altKey) && ( altKey === shortcuts[id].getOptions().altKey) &&
(ctrlKey === undefined && shortcuts[id].getOptions().ctrlKey !== undefined) || ((ctrlKey === undefined && shortcuts[id].getOptions().ctrlKey !== undefined) ||
(ctrlKey !== undefined && shortcuts[id].getOptions().ctrlKey === undefined) || (ctrlKey !== undefined && shortcuts[id].getOptions().ctrlKey === undefined) ||
ctrlKey === shortcuts[id].getOptions().ctrlKey) ctrlKey === shortcuts[id].getOptions().ctrlKey)
) { ) {
available = false; available = false;
inUseShortcut = shortcuts[id]; inUseShortcuts.push(shortcuts[id]);
} }
} }
return ( return (
<> <Modal
{props.open && ( open={props.open}
<> className={`
<Modal 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={` className={`
inline-flex h-fit w-[600px] overflow-y-auto scroll-smooth bg-white text-gray-800 text-md
p-10 dark:text-white
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"> {shortcut?.getOptions().label}
<div className={`flex flex-col items-start gap-2`}> </span>
<span <span
className={` className={`
text-gray-800 text-md text-gray-800 text-md
dark:text-white dark:text-gray-500
`} `}
> >
{shortcut?.getOptions().label} Press the key you want to bind to this event
</span> </span>
<span </div>
className={` <div className="w-full text-center text-white">
text-gray-800 text-md {ctrlKey && "Ctrl + "}
dark:text-gray-500 {altKey && "Alt + "}
`} {shiftKey && "Shift + "}
> {code}
Press the key you want to bind to this event </div>
</span> <div className="text-white">
</div> {available === true && <div className="text-green-600">Keybind is free!</div>}
<div className="w-full text-center text-white">{code}</div> {available === false && (
<div className="flex flex-col gap-2"> <div className="flex flex-col gap-2">
<div className="flex gap-2"> <div className="flex gap-2">
<OlToggle Keybind is already in use: <div className={`
onClick={() => { flex flex-wrap gap-2 font-bold text-orange-600
if (shiftKey === false) setShiftKey(undefined); `}>{inUseShortcuts.map((shortcut) => <span>{shortcut.getOptions().label}</span>)}</div>
else if (shiftKey === undefined) setShiftKey(true);
else setShiftKey(false);
}}
toggled={shiftKey}
></OlToggle>
<div className="text-white">
{shiftKey === true && "Shift key must be pressed"}
{shiftKey === undefined && "Shift key can be anything"}
{shiftKey === false && "Shift key must NOT be pressed"}
</div>
</div>
<div className="flex gap-2">
<OlToggle
onClick={() => {
if (altKey === false) setAltKey(undefined);
else if (altKey === undefined) setAltKey(true);
else setAltKey(false);
}}
toggled={altKey}
></OlToggle>
<div className="text-white">
{altKey === true && "Alt key must be pressed"}
{altKey === undefined && "Alt key can be anything"}
{altKey === false && "Alt key must NOT be pressed"}
</div>
</div>
<div className="flex gap-2">
<OlToggle
onClick={() => {
if (ctrlKey === false) setCtrlKey(undefined);
else if (ctrlKey === undefined) setCtrlKey(true);
else setCtrlKey(false);
}}
toggled={ctrlKey}
></OlToggle>
<div className="text-white">
{ctrlKey === true && "Ctrl key must be pressed"}
{ctrlKey === undefined && "Ctrl key can be anything"}
{ctrlKey === false && "Ctrl key must NOT be pressed"}
</div>
</div>
</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>
<div className="text-gray-500">A key combination can be assigned to multiple actions, and all bound actions will fire</div>
</div> </div>
</Modal> )}
<div className={`fixed left-0 top-0 z-30 h-full w-full bg-[#111111]/95`}></div> </div>
</>
)} <div className="flex justify-end">
</> {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>
); );
} }

View File

@@ -1,26 +1,25 @@
import React, { useEffect, useState } from "react"; import React, { useEffect, useState } from "react";
import { Modal } from "./components/modal"; import { Modal } from "./components/modal";
import { Card } from "./components/card"; import { Card } from "./components/card";
import { ErrorCallout } from "../../ui/components/olcallout"; import { ErrorCallout } from "../components/olcallout";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { faArrowRight, faCheckCircle, faExternalLink } from "@fortawesome/free-solid-svg-icons"; import { faArrowRight, faCheckCircle, faExternalLink } from "@fortawesome/free-solid-svg-icons";
import { getApp, VERSION } from "../../olympusapp"; import { getApp, VERSION } from "../../olympusapp";
import { sha256 } from "js-sha256"; import { sha256 } from "js-sha256";
import { BLUE_COMMANDER, GAME_MASTER, OlympusState, RED_COMMANDER } from "../../constants/constants"; import { BLUE_COMMANDER, GAME_MASTER, OlympusState, RED_COMMANDER } from "../../constants/constants";
import { FaTrash, FaXmark } from "react-icons/fa6";
export function LoginModal(props: {}) { export function LoginModal(props: { open: boolean }) {
// TODO: add warning if not in secure context and some features are disabled // TODO: add warning if not in secure context and some features are disabled
const [password, setPassword] = useState(""); const [password, setPassword] = useState("");
const [profileName, setProfileName] = useState("Game Master"); const [profileName, setProfileName] = useState("");
const [checkingPassword, setCheckingPassword] = useState(false); const [checkingPassword, setCheckingPassword] = useState(false);
const [loginError, setLoginError] = useState(false); const [loginError, setLoginError] = useState(false);
const [commandMode, setCommandMode] = useState(null as null | string); const [commandMode, setCommandMode] = useState(null as null | string);
useEffect(() => { useEffect(() => {
/* Set the profile name */ /* Set the profile name */
getApp().setProfile(profileName); if (profileName !== "") getApp().setProfile(profileName);
}, [profileName]) }, [profileName]);
function checkPassword(password: string) { function checkPassword(password: string) {
setCheckingPassword(true); setCheckingPassword(true);
@@ -51,14 +50,14 @@ export function LoginModal(props: {}) {
getApp().setState(OlympusState.IDLE); getApp().setState(OlympusState.IDLE);
/* If no profile exists already with that name, create it from scratch from the defaults */ /* If no profile exists already with that name, create it from scratch from the defaults */
if (getApp().getProfile() === null) if (getApp().getProfile() === null) getApp().saveProfile();
getApp().saveProfile();
/* Load the profile */ /* Load the profile */
getApp().loadProfile(); getApp().loadProfile();
} }
return ( return (
<Modal <Modal
open={props.open}
className={` className={`
inline-flex h-[75%] max-h-[570px] w-[80%] max-w-[1100px] overflow-y-auto inline-flex h-[75%] max-h-[570px] w-[80%] max-w-[1100px] overflow-y-auto
scroll-smooth bg-white scroll-smooth bg-white
@@ -239,9 +238,7 @@ export function LoginModal(props: {}) {
required required
/> />
</div> </div>
<div className="text-xs text-gray-400"> <div className="text-xs text-gray-400">The profile name you choose determines the saved key binds, groups and options you see.</div>
The profile name you choose determines the saved key binds, groups and options you see.
</div>
<div className="flex"> <div className="flex">
<button <button
type="button" type="button"

View File

@@ -1,111 +0,0 @@
import React from "react";
import { Modal } from "./components/modal";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { faArrowRight } from "@fortawesome/free-solid-svg-icons";
import { FaLock } from "react-icons/fa6";
import { getApp } from "../../olympusapp";
import { OlympusState } from "../../constants/constants";
export function ProtectionPrompt(props: { open: boolean }) {
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-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

@@ -0,0 +1,101 @@
import React from "react";
import { Modal } from "./components/modal";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { faArrowRight } from "@fortawesome/free-solid-svg-icons";
import { FaLock } from "react-icons/fa6";
import { getApp } from "../../olympusapp";
import { OlympusState } from "../../constants/constants";
export function ProtectionPromptModal(props: { open: boolean }) {
return (
<Modal
open={props.open}
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
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>
);
}

View File

@@ -114,7 +114,7 @@ export function AirbaseMenu(props: { open: boolean; onClose: () => void; childre
<div className="flex flex-col gap-2"> <div className="flex flex-col gap-2">
{airbase?.getChartData().runways.map((runway, idx) => { {airbase?.getChartData().runways.map((runway, idx) => {
return ( return (
<> <div key={idx}>
{Object.keys(runway.headings[0]).map((runwayName) => { {Object.keys(runway.headings[0]).map((runwayName) => {
return ( return (
<div key={`${idx}-${runwayName}`} className={` <div key={`${idx}-${runwayName}`} className={`
@@ -137,7 +137,7 @@ export function AirbaseMenu(props: { open: boolean; onClose: () => void; childre
</div> </div>
); );
})} })}
</> </div>
); );
})} })}
</div> </div>

View File

@@ -19,7 +19,7 @@ export function AudioMenu(props: { open: boolean; onClose: () => void; children?
const [audioManagerEnabled, setAudioManagerEnabled] = useState(false); const [audioManagerEnabled, setAudioManagerEnabled] = useState(false);
const [activeSource, setActiveSource] = useState(null as AudioSource | null); const [activeSource, setActiveSource] = useState(null as AudioSource | null);
const [count, setCount] = useState(0); const [count, setCount] = useState(0);
const [shortcuts, setShortcuts] = useState({}) const [shortcuts, setShortcuts] = useState({});
/* Preallocate 128 references for the source and sink panels. If the number of references changes, React will give an error */ /* Preallocate 128 references for the source and sink panels. If the number of references changes, React will give an error */
const sourceRefs = Array(128) const sourceRefs = Array(128)
@@ -251,20 +251,19 @@ export function AudioMenu(props: { open: boolean; onClose: () => void; children?
right: (end as HTMLDivElement).offsetLeft + (end as HTMLDivElement).clientWidth, right: (end as HTMLDivElement).offsetLeft + (end as HTMLDivElement).clientWidth,
}; };
return ( return (
<> <div
<div key={idx}
className={` className={`
absolute rounded-br-md rounded-tr-md border-2 border-l-0 absolute rounded-br-md rounded-tr-md border-2 border-l-0
`} `}
style={{ style={{
top: `${(startRect.bottom + startRect.top) / 2}px`, top: `${(startRect.bottom + startRect.top) / 2}px`,
left: `${startRect.right}px`, left: `${startRect.right}px`,
height: `${endRect.top - startRect.top + (endRect.height - startRect.height) / 2}px`, height: `${endRect.top - startRect.top + (endRect.height - startRect.height) / 2}px`,
width: `${(lineCounters[idx] - 1) * lineDistance + 30}px`, width: `${(lineCounters[idx] - 1) * lineDistance + 30}px`,
borderColor: lineColors[idx], borderColor: lineColors[idx],
}} }}
></div> />
</>
); );
} }
}) })
@@ -281,29 +280,26 @@ export function AudioMenu(props: { open: boolean; onClose: () => void; children?
right: (div as HTMLDivElement).offsetLeft + (div as HTMLDivElement).clientWidth, right: (div as HTMLDivElement).offsetLeft + (div as HTMLDivElement).clientWidth,
}; };
return ( return (
<> <div
<div> key={idx}
<div data-active={activeSource === sources[idx]}
data-active={activeSource === sources[idx]} className={`
className={` absolute translate-y-[-50%] cursor-pointer rounded-full
absolute translate-y-[-50%] cursor-pointer rounded-full bg-blue-600 p-1 text-xs text-white
bg-blue-600 p-1 text-xs text-white data-[active='true']:bg-white
data-[active='true']:bg-white data-[active='true']:text-blue-600
data-[active='true']:text-blue-600 hover:bg-blue-800
hover:bg-blue-800 `}
`} style={{
style={{ top: `${(divRect.bottom + divRect.top) / 2}px`,
top: `${(divRect.bottom + divRect.top) / 2}px`, left: `${divRect.right - 10}px`,
left: `${divRect.right - 10}px`, }}
}} onClick={() => {
onClick={() => { activeSource !== sources[idx] ? setActiveSource(sources[idx]) : setActiveSource(null);
activeSource !== sources[idx] ? setActiveSource(sources[idx]) : setActiveSource(null); }}
}} >
> <FaPlus></FaPlus>
<FaPlus></FaPlus> </div>
</div>
</div>
</>
); );
} }
})} })}
@@ -318,28 +314,25 @@ export function AudioMenu(props: { open: boolean; onClose: () => void; children?
right: (div as HTMLDivElement).offsetLeft + (div as HTMLDivElement).clientWidth, right: (div as HTMLDivElement).offsetLeft + (div as HTMLDivElement).clientWidth,
}; };
return ( return (
<> <div
<div> key={idx}
<div className={`
className={` absolute translate-y-[-50%] cursor-pointer rounded-full
absolute translate-y-[-50%] cursor-pointer bg-blue-600 p-1 text-xs text-white
rounded-full bg-blue-600 p-1 text-xs text-white hover:bg-blue-800
hover:bg-blue-800 `}
`} style={{
style={{ top: `${(divRect.bottom + divRect.top) / 2}px`,
top: `${(divRect.bottom + divRect.top) / 2}px`, left: `${divRect.right - 10}px`,
left: `${divRect.right - 10}px`, }}
}} onClick={() => {
onClick={() => { if (activeSource.getConnectedTo().includes(sinks[idx])) activeSource.disconnect(sinks[idx]);
if (activeSource.getConnectedTo().includes(sinks[idx])) activeSource.disconnect(sinks[idx]); else activeSource.connect(sinks[idx]);
else activeSource.connect(sinks[idx]); }}
}} >
> {" "}
{" "} {activeSource.getConnectedTo().includes(sinks[idx]) ? <FaMinus></FaMinus> : <FaPlus></FaPlus>}
{activeSource.getConnectedTo().includes(sinks[idx]) ? <FaMinus></FaMinus> : <FaPlus></FaPlus>} </div>
</div>
</div>
</>
); );
} }
})} })}

View File

@@ -46,21 +46,14 @@ export function ControlsPanel(props: {}) {
text: "Select unit", text: "Select unit",
}, },
{ {
actions: [touch ? faHandPointer : "LMB", "Drag"], actions: ["Shift", "LMB", "Drag"],
text: "Box selection", text: "Box selection",
}, },
{ {
actions: [touch ? faHandPointer : "Wheel", "Drag"], actions: [touch ? faHandPointer : "LMB", "Drag"],
text: "Move map", text: "Move map",
}, },
]; ];
if (!touch) {
controls.push({
actions: ["Shift", "LMB", "Drag"],
text: "Box selection",
});
}
if (appState === OlympusState.IDLE) { if (appState === OlympusState.IDLE) {
controls = baseControls; controls = baseControls;

View File

@@ -38,20 +38,12 @@ export function GameMasterMenu(props: { open: boolean; onClose: () => void; chil
GAME MASTER GAME MASTER
</div> </div>
)} )}
{commandModeOptions.commandMode === BLUE_COMMANDER && ( {commandModeOptions.commandMode === BLUE_COMMANDER && <div className={`
<div w-full rounded-md bg-blue-600 p-2 text-center font-bold
className={`w-full rounded-md bg-blue-600 p-2 text-center font-bold`} `}>BLUE COMMANDER</div>}
> {commandModeOptions.commandMode === RED_COMMANDER && <div className={`
BLUE COMMANDER w-full rounded-md bg-red-700 p-2 text-center font-bold
</div> `}>RED COMMANDER</div>}
)}
{commandModeOptions.commandMode === RED_COMMANDER && (
<div
className={`w-full rounded-md bg-red-700 p-2 text-center font-bold`}
>
RED COMMANDER
</div>
)}
{serverStatus.elapsedTime > currentSetupTime && ( {serverStatus.elapsedTime > currentSetupTime && (
<div <div
className={` className={`
@@ -125,6 +117,7 @@ export function GameMasterMenu(props: { open: boolean; onClose: () => void; chil
.map((era) => { .map((era) => {
return ( return (
<div <div
key={era}
className={` className={`
group flex flex-row rounded-md justify-content group flex flex-row rounded-md justify-content
cursor-pointer gap-4 p-2 cursor-pointer gap-4 p-2
@@ -274,9 +267,12 @@ export function GameMasterMenu(props: { open: boolean; onClose: () => void; chil
group flex flex-row rounded-md justify-content gap-4 px-4 py-2 group flex flex-row rounded-md justify-content gap-4 px-4 py-2
`} `}
> >
<span className="mr-auto">Elapsed time (seconds)</span> <span className={` <span className="mr-auto">Elapsed time (seconds)</span>{" "}
w-32 text-center <span
`}>{serverStatus.elapsedTime?.toFixed()}</span> className={`w-32 text-center`}
>
{serverStatus.elapsedTime?.toFixed()}
</span>
</div> </div>
{commandModeOptions.commandMode === GAME_MASTER && ( {commandModeOptions.commandMode === GAME_MASTER && (
<button <button

View File

@@ -211,7 +211,7 @@ export function Header() {
</div> </div>
<OlLabelToggle <OlLabelToggle
toggled={mapOptions.cameraPluginMode === "live"} toggled={mapOptions.cameraPluginMode === "map"}
leftLabel={"Live"} leftLabel={"Live"}
rightLabel={"Map"} rightLabel={"Map"}
onClick={() => { onClick={() => {

View File

@@ -15,12 +15,11 @@ export function InfoBar(props: {}) {
}, []); }, []);
return ( return (
<div <div className={`absolute left-[50%] top-16`}>
className={`absolute left-[50%] top-16`}
>
{messages.slice(Math.max(0, messages.length - 4), Math.max(0, messages.length)).map((message, idx) => { {messages.slice(Math.max(0, messages.length - 4), Math.max(0, messages.length)).map((message, idx) => {
return ( return (
<div <div
key={idx}
className={` className={`
absolute w-fit translate-x-[-50%] gap-2 text-nowrap rounded-full absolute w-fit translate-x-[-50%] gap-2 text-nowrap rounded-full
bg-olympus-800/90 px-4 py-2 text-center text-sm text-white bg-olympus-800/90 px-4 py-2 text-center text-sm text-white

View File

@@ -38,7 +38,7 @@ export function OptionsMenu(props: { open: boolean; onClose: () => void; childre
`} `}
> >
<OlAccordion <OlAccordion
onClick={() => setOpenAccordion(openAccordion === Accordion.NONE ? Accordion.BINDINGS : Accordion.NONE)} onClick={() => setOpenAccordion(openAccordion !== Accordion.BINDINGS ? Accordion.BINDINGS : Accordion.NONE)}
open={openAccordion === Accordion.BINDINGS} open={openAccordion === Accordion.BINDINGS}
title="Key bindings" title="Key bindings"
> >
@@ -53,9 +53,10 @@ export function OptionsMenu(props: { open: boolean; onClose: () => void; childre
.map(([id, shortcut]) => { .map(([id, shortcut]) => {
return ( return (
<div <div
key={id}
className={` className={`
group relative mr-2 flex cursor-pointer select-none group relative mr-2 flex cursor-pointer select-none
items-center justify-between rounded-sm px-2 py-2 text-sm items-center justify-between rounded-sm px-2 py-2
dark:text-gray-300 dark:hover:bg-olympus-500 dark:text-gray-300 dark:hover:bg-olympus-500
`} `}
onClick={() => { onClick={() => {
@@ -65,39 +66,9 @@ export function OptionsMenu(props: { open: boolean; onClose: () => void; childre
> >
<span>{shortcut.getOptions().label}</span> <span>{shortcut.getOptions().label}</span>
<span className="flex gap-1"> <span className="flex gap-1">
{shortcut.getOptions().altKey ? ( {shortcut.getOptions().ctrlKey && "Ctrl + "}
<div className="flex gap-1"> {shortcut.getOptions().altKey && "Alt + "}
<div className={`text-green-500`}>Alt</div> +{" "} {shortcut.getOptions().shiftKey && "Shift + "}
</div>
) : shortcut.getOptions().altKey === false ? (
<div className={`flex gap-1`}>
<div className={`text-red-500`}>Alt</div> +{" "}
</div>
) : (
""
)}
{shortcut.getOptions().ctrlKey ? (
<div className="flex gap-1">
<div className={`text-green-500`}>Shift</div> +{" "}
</div>
) : shortcut.getOptions().ctrlKey === false ? (
<div className={`flex gap-1`}>
<div className={`text-red-500`}>Shift</div> +{" "}
</div>
) : (
""
)}
{shortcut.getOptions().shiftKey ? (
<div className="flex gap-1">
<div className={`text-green-500`}>Ctrl</div> +{" "}
</div>
) : shortcut.getOptions().shiftKey === false ? (
<div className={`flex gap-1`}>
<div className={`text-red-500`}>Ctrl</div> +{" "}
</div>
) : (
""
)}
{shortcut.getOptions().code} {shortcut.getOptions().code}
</span> </span>
</div> </div>
@@ -107,7 +78,7 @@ export function OptionsMenu(props: { open: boolean; onClose: () => void; childre
</OlAccordion> </OlAccordion>
<OlAccordion <OlAccordion
onClick={() => setOpenAccordion(openAccordion === Accordion.NONE ? Accordion.MAP_OPTIONS : Accordion.NONE)} onClick={() => setOpenAccordion(openAccordion !== Accordion.MAP_OPTIONS ? Accordion.MAP_OPTIONS : Accordion.NONE)}
open={openAccordion === Accordion.MAP_OPTIONS} open={openAccordion === Accordion.MAP_OPTIONS}
title="Map options" title="Map options"
> >
@@ -158,7 +129,7 @@ export function OptionsMenu(props: { open: boolean; onClose: () => void; childre
<div <div
className={` className={`
group flex flex-row gap-4 rounded-md justify-content group flex flex-row gap-4 rounded-md justify-content
cursor-pointer p-2 text-sm cursor-pointer p-2
dark:hover:bg-olympus-400 dark:hover:bg-olympus-400
`} `}
onClick={() => getApp().getMap().setOption("hideUnitsShortRangeRings", !mapOptions.hideUnitsShortRangeRings)} onClick={() => getApp().getMap().setOption("hideUnitsShortRangeRings", !mapOptions.hideUnitsShortRangeRings)}
@@ -166,11 +137,11 @@ export function OptionsMenu(props: { open: boolean; onClose: () => void; childre
<OlCheckbox checked={mapOptions.hideUnitsShortRangeRings} onChange={() => {}}></OlCheckbox> <OlCheckbox checked={mapOptions.hideUnitsShortRangeRings} onChange={() => {}}></OlCheckbox>
<span>Hide Short range Rings</span> <span>Hide Short range Rings</span>
</div> </div>
<div <div
className={` className={`
group flex flex-row gap-4 rounded-md justify-content group flex flex-row gap-4 rounded-md justify-content
cursor-pointer p-2 text-sm cursor-pointer p-2
dark:hover:bg-olympus-400 dark:hover:bg-olympus-400
`} `}
onClick={() => getApp().getMap().setOption("hideGroupMembers", !mapOptions.hideGroupMembers)} onClick={() => getApp().getMap().setOption("hideGroupMembers", !mapOptions.hideGroupMembers)}
@@ -181,7 +152,7 @@ export function OptionsMenu(props: { open: boolean; onClose: () => void; childre
<div <div
className={` className={`
group flex flex-row gap-4 rounded-md justify-content group flex flex-row gap-4 rounded-md justify-content
cursor-pointer p-2 text-sm cursor-pointer p-2
dark:hover:bg-olympus-400 dark:hover:bg-olympus-400
`} `}
onClick={() => getApp().getMap().setOption("showMinimap", !mapOptions.showMinimap)} onClick={() => getApp().getMap().setOption("showMinimap", !mapOptions.showMinimap)}
@@ -192,7 +163,7 @@ export function OptionsMenu(props: { open: boolean; onClose: () => void; childre
</OlAccordion> </OlAccordion>
<OlAccordion <OlAccordion
onClick={() => setOpenAccordion(openAccordion === Accordion.NONE ? Accordion.CAMERA_PLUGIN : Accordion.NONE)} onClick={() => setOpenAccordion(openAccordion !== Accordion.CAMERA_PLUGIN ? Accordion.CAMERA_PLUGIN : Accordion.NONE)}
open={openAccordion === Accordion.CAMERA_PLUGIN} open={openAccordion === Accordion.CAMERA_PLUGIN}
title="Camera plugin options" title="Camera plugin options"
> >

View File

@@ -32,6 +32,7 @@ export function RadiosSummaryPanel(props: {}) {
.map((radioSink, idx) => { .map((radioSink, idx) => {
return ( return (
<OlStateButton <OlStateButton
key={idx}
checked={radioSink.getPtt()} checked={radioSink.getPtt()}
onClick={() => {}} onClick={() => {}}
onMouseDown={() => { onMouseDown={() => {
@@ -41,28 +42,23 @@ export function RadiosSummaryPanel(props: {}) {
radioSink.setPtt(false); radioSink.setPtt(false);
}} }}
tooltip="Click to talk, lights up when receiving" tooltip="Click to talk, lights up when receiving"
buttonColor={ buttonColor={radioSink.getReceiving() ? "white" : null}
radioSink.getReceiving()
? "white"
: null
}
> >
<span className={`font-bold text-gray-200`}>{idx + 1}</span> <span className={`font-bold text-gray-200`}>{idx + 1}</span>
</OlStateButton> </OlStateButton>
); );
})} })}
{audioSinks.filter((audioSinks) => audioSinks instanceof UnitSink).length > 0 && ( {audioSinks.filter((audioSinks) => audioSinks instanceof UnitSink).length > 0 && <FaJetFighter className={`
<FaJetFighter text-xl
className={`text-xl`} `} />}
/>
)}
{audioSinks.filter((audioSinks) => audioSinks instanceof UnitSink).length > 0 && {audioSinks.filter((audioSinks) => audioSinks instanceof UnitSink).length > 0 &&
audioSinks audioSinks
.filter((audioSinks) => audioSinks instanceof UnitSink) .filter((audioSinks) => audioSinks instanceof UnitSink)
.map((unitSink, idx) => { .map((unitSink, idx) => {
return ( return (
<OlStateButton <OlStateButton
key={idx}
checked={unitSink.getPtt()} checked={unitSink.getPtt()}
onClick={() => {}} onClick={() => {}}
onMouseDown={() => { onMouseDown={() => {

View File

@@ -17,6 +17,7 @@ import { faStar } from "@fortawesome/free-solid-svg-icons";
import { OlStringInput } from "../components/olstringinput"; import { OlStringInput } from "../components/olstringinput";
import { countryCodes } from "../data/codes"; import { countryCodes } from "../data/codes";
import { OlAccordion } from "../components/olaccordion"; import { OlAccordion } from "../components/olaccordion";
import { AppStateChangedEvent } from "../../events";
export function UnitSpawnMenu(props: { export function UnitSpawnMenu(props: {
starredSpawns: { [key: string]: SpawnRequestTable }; starredSpawns: { [key: string]: SpawnRequestTable };
@@ -32,6 +33,7 @@ export function UnitSpawnMenu(props: {
const altitudeStep = altitudeIncrements[props.blueprint.category]; const altitudeStep = altitudeIncrements[props.blueprint.category];
/* State initialization */ /* State initialization */
const [appState, setAppState] = useState(OlympusState.NOT_INITIALIZED);
const [spawnCoalition, setSpawnCoalition] = useState("blue" as Coalition); const [spawnCoalition, setSpawnCoalition] = useState("blue" as Coalition);
const [spawnNumber, setSpawnNumber] = useState(1); const [spawnNumber, setSpawnNumber] = useState(1);
const [spawnRole, setSpawnRole] = useState(""); const [spawnRole, setSpawnRole] = useState("");
@@ -46,9 +48,14 @@ export function UnitSpawnMenu(props: {
const [key, setKey] = useState(""); const [key, setKey] = useState("");
const [spawnRequestTable, setSpawnRequestTable] = useState(null as null | SpawnRequestTable); const [spawnRequestTable, setSpawnRequestTable] = useState(null as null | SpawnRequestTable);
/* When the menu is opened show the unit preview on the map as a cursor */
useEffect(() => { useEffect(() => {
if (!props.airbase && spawnRequestTable) { setAppState(getApp()?.getState())
AppStateChangedEvent.on((state, subState) => setAppState(state));
}, []);
/* When the menu is opened show the unit preview on the map as a cursor */
const setSpawnRequestTableCallback = useCallback(() => {
if (!props.airbase && spawnRequestTable && appState === OlympusState.SPAWN) {
/* Refresh the unique key identified */ /* Refresh the unique key identified */
const newKey = hash(JSON.stringify(spawnRequestTable)); const newKey = hash(JSON.stringify(spawnRequestTable));
setKey(newKey); setKey(newKey);
@@ -56,7 +63,9 @@ export function UnitSpawnMenu(props: {
getApp()?.getMap()?.setSpawnRequestTable(spawnRequestTable); getApp()?.getMap()?.setSpawnRequestTable(spawnRequestTable);
getApp().setState(OlympusState.SPAWN, SpawnSubState.SPAWN_UNIT); getApp().setState(OlympusState.SPAWN, SpawnSubState.SPAWN_UNIT);
} }
}, [spawnRequestTable]); }, [spawnRequestTable, appState]);
useEffect(setSpawnRequestTableCallback, [spawnRequestTable]);
/* Callback and effect to update the quick access name of the starredSpawn */ /* Callback and effect to update the quick access name of the starredSpawn */
const updateStarredSpawnQuickAccessNameS = useCallback(() => { const updateStarredSpawnQuickAccessNameS = useCallback(() => {
@@ -231,6 +240,7 @@ export function UnitSpawnMenu(props: {
{roles.map((role) => { {roles.map((role) => {
return ( return (
<OlDropdownItem <OlDropdownItem
key={role}
onClick={() => { onClick={() => {
setSpawnRole(role); setSpawnRole(role);
setSpawnLoadout(""); setSpawnLoadout("");
@@ -256,6 +266,7 @@ export function UnitSpawnMenu(props: {
{loadouts.map((loadout) => { {loadouts.map((loadout) => {
return ( return (
<OlDropdownItem <OlDropdownItem
key={loadout.name}
onClick={() => { onClick={() => {
setSpawnLoadout(loadout.name); setSpawnLoadout(loadout.name);
}} }}
@@ -285,10 +296,9 @@ export function UnitSpawnMenu(props: {
> >
Livery Livery
</span> </span>
<OlDropdown <OlDropdown label={props.blueprint.liveries ? (props.blueprint.liveries[spawnLiveryID]?.name ?? "Default") : "No livery"} className={`
label={props.blueprint.liveries ? (props.blueprint.liveries[spawnLiveryID]?.name ?? "Default") : "No livery"} w-64
className={`w-64`} `}>
>
{props.blueprint.liveries && {props.blueprint.liveries &&
Object.keys(props.blueprint.liveries) Object.keys(props.blueprint.liveries)
.sort((ida, idb) => { .sort((ida, idb) => {
@@ -303,6 +313,7 @@ export function UnitSpawnMenu(props: {
}); });
return ( return (
<OlDropdownItem <OlDropdownItem
key={id}
onClick={() => { onClick={() => {
setSpawnLiveryID(id); setSpawnLiveryID(id);
}} }}
@@ -315,9 +326,10 @@ export function UnitSpawnMenu(props: {
`} `}
> >
{props.blueprint.liveries && props.blueprint.liveries[id].countries.length == 1 && ( {props.blueprint.liveries && props.blueprint.liveries[id].countries.length == 1 && (
<img src={`images/countries/${country?.flagCode.toLowerCase()}.svg`} className={` <img
h-6 src={`images/countries/${country?.flagCode.toLowerCase()}.svg`}
`} /> className={`h-6`}
/>
)} )}
<div className="my-auto truncate"> <div className="my-auto truncate">
@@ -348,6 +360,7 @@ export function UnitSpawnMenu(props: {
{["Average", "Good", "High", "Excellent"].map((skill) => { {["Average", "Good", "High", "Excellent"].map((skill) => {
return ( return (
<OlDropdownItem <OlDropdownItem
key={skill}
onClick={() => { onClick={() => {
setSpawnSkill(skill); setSpawnSkill(skill);
}} }}
@@ -383,7 +396,7 @@ export function UnitSpawnMenu(props: {
> >
{spawnLoadout.items.map((item) => { {spawnLoadout.items.map((item) => {
return ( return (
<div className="flex content-center gap-2"> <div className="flex content-center gap-2" key={item.name}>
<div <div
className={` className={`
my-auto w-6 min-w-6 rounded-full py-0.5 text-center my-auto w-6 min-w-6 rounded-full py-0.5 text-center
@@ -421,7 +434,13 @@ export function UnitSpawnMenu(props: {
if (spawnRequestTable) if (spawnRequestTable)
getApp() getApp()
.getUnitsManager() .getUnitsManager()
.spawnUnits(spawnRequestTable.category, Array(spawnRequestTable.amount).fill(spawnRequestTable.unit), spawnRequestTable.coalition, false, props.airbase?.getName()); .spawnUnits(
spawnRequestTable.category,
Array(spawnRequestTable.amount).fill(spawnRequestTable.unit),
spawnRequestTable.coalition,
false,
props.airbase?.getName()
);
}} }}
> >
Spawn Spawn

View File

@@ -10,7 +10,7 @@ import { OptionsMenu } from "./panels/optionsmenu";
import { MapHiddenTypes, MapOptions } from "../types/types"; import { MapHiddenTypes, MapOptions } from "../types/types";
import { NO_SUBSTATE, OlympusState, OlympusSubState, OptionsSubstate, SpawnSubState, UnitControlSubState } from "../constants/constants"; import { NO_SUBSTATE, OlympusState, OlympusSubState, OptionsSubstate, SpawnSubState, UnitControlSubState } from "../constants/constants";
import { getApp, setupApp } from "../olympusapp"; import { getApp, setupApp } from "../olympusapp";
import { LoginModal } from "./modals/login"; import { LoginModal } from "./modals/loginmodal";
import { MiniMapPanel } from "./panels/minimappanel"; import { MiniMapPanel } from "./panels/minimappanel";
import { UnitControlBar } from "./panels/unitcontrolbar"; import { UnitControlBar } from "./panels/unitcontrolbar";
@@ -20,7 +20,7 @@ import { MapContextMenu } from "./contextmenus/mapcontextmenu";
import { AirbaseMenu } from "./panels/airbasemenu"; import { AirbaseMenu } from "./panels/airbasemenu";
import { AudioMenu } from "./panels/audiomenu"; import { AudioMenu } from "./panels/audiomenu";
import { FormationMenu } from "./panels/formationmenu"; import { FormationMenu } from "./panels/formationmenu";
import { ProtectionPrompt } from "./modals/protectionprompt"; import { ProtectionPromptModal } from "./modals/protectionpromptmodal";
import { KeybindModal } from "./modals/keybindmodal"; import { KeybindModal } from "./modals/keybindmodal";
import { UnitExplosionMenu } from "./panels/unitexplosionmenu"; import { UnitExplosionMenu } from "./panels/unitexplosionmenu";
import { JTACMenu } from "./panels/jtacmenu"; import { JTACMenu } from "./panels/jtacmenu";
@@ -65,16 +65,8 @@ export function UI() {
> >
<Header /> <Header />
<div className="flex h-full w-full flex-row-reverse"> <div className="flex h-full w-full flex-row-reverse">
{appState === OlympusState.LOGIN && ( <LoginModal open={appState === OlympusState.LOGIN} />
<> <ProtectionPromptModal open={appState === OlympusState.UNIT_CONTROL && appSubState == UnitControlSubState.PROTECTION} />
<div className={`
fixed left-0 top-0 z-30 h-full w-full bg-[#111111]/95
`}></div>
<LoginModal />
</>
)}
<ProtectionPrompt open={appState === OlympusState.UNIT_CONTROL && appSubState == UnitControlSubState.PROTECTION} />
<KeybindModal open={appState === OlympusState.OPTIONS && appSubState === OptionsSubstate.KEYBIND} /> <KeybindModal open={appState === OlympusState.OPTIONS && appSubState === OptionsSubstate.KEYBIND} />
<div id="map-container" className="z-0 h-full w-screen" /> <div id="map-container" className="z-0 h-full w-screen" />