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() {
console.log("Adding new radio");
if (!this.#running) {
if (!this.#running || this.#sources[0] === undefined) {
console.log("Audio manager not started, aborting...");
return;
}

View File

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

View File

@ -529,8 +529,8 @@ export namespace ContextActions {
executeImmediately: true,
type: ContextActionType.DELETE,
code: "Delete",
ctrlKey: true,
shiftKey: false,
ctrlKey: false,
shiftKey: true,
altKey: false,
}
);
@ -562,7 +562,7 @@ export namespace ContextActions {
export const FOLLOW = new ContextAction(
"follow",
"Follow unit",
"Click on a unit to follow it in formation",
"Right-click on a unit to follow it in formation",
olButtonsContextFollow,
ContextActionTarget.UNIT,
(units: Unit[], targetUnit: Unit | null, _) => {
@ -580,7 +580,7 @@ export namespace ContextActions {
export const BOMB = new ContextAction(
"bomb",
"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,
ContextActionTarget.POINT,
(units: Unit[], _, targetPosition: LatLng | null) => {
@ -595,7 +595,7 @@ export namespace ContextActions {
export const CARPET_BOMB = new ContextAction(
"carpet-bomb",
"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,
ContextActionTarget.POINT,
(units: Unit[], _, targetPosition: LatLng | null) => {
@ -610,7 +610,7 @@ export namespace ContextActions {
export const LAND = new ContextAction(
"land",
"Land",
"Click on a point to land at the nearest airbase",
"Right-click on a point to land at the nearest airbase",
faPlaneArrival,
ContextActionTarget.POINT,
(units: Unit[], _, targetPosition: LatLng | null) => {
@ -622,7 +622,7 @@ export namespace ContextActions {
export const LAND_AT_POINT = new ContextAction(
"land-at-point",
"Land at location",
"Click on a point to land there",
"Right-click on a point to land there",
olButtonsContextLandAtPoint,
ContextActionTarget.POINT,
(units: Unit[], _, targetPosition: LatLng | null) => {
@ -649,7 +649,7 @@ export namespace ContextActions {
export const ATTACK = new ContextAction(
"attack",
"Attack unit",
"Click on a unit to attack it",
"Right-click on a unit to attack it",
olButtonsContextAttack,
ContextActionTarget.UNIT,
(units: Unit[], targetUnit: Unit | null, _) => {
@ -661,7 +661,7 @@ export namespace ContextActions {
export const FIRE_AT_AREA = new ContextAction(
"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,
ContextActionTarget.POINT,
(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 ***************/
export class HiddenTypesChangedEvent {
static on(callback: (hiddenTypes: MapHiddenTypes) => void) {

View File

@ -5,9 +5,10 @@ import { DomEvent } from "leaflet";
import { LatLngBounds } from "leaflet";
import { Bounds } from "leaflet";
import { SELECT_TOLERANCE_PX } from "../constants/constants";
import { Map } from "./map";
export var BoxSelect = Handler.extend({
initialize: function (map) {
initialize: function (map: Map) {
this._map = map;
this._container = map.getContainer();
this._pane = map.getPanes().overlayPane;
@ -45,13 +46,12 @@ export var BoxSelect = Handler.extend({
},
_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
// will interrupt the interaction and orphan a box element in the container.
this._clearDeferredResetState();
this._resetState();
DomUtil.disableTextSelection();
DomUtil.disableImageDrag();
this._map.dragging.disable();
@ -65,10 +65,8 @@ export var BoxSelect = Handler.extend({
contextmenu: DomEvent.stop,
touchmove: this._onMouseMove,
touchend: this._onMouseUp,
touchstart: this._onKeyDown,
mousemove: this._onMouseMove,
mouseup: this._onMouseUp,
keydown: this._onKeyDown,
mouseup: this._onMouseUp
},
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) {
if (e.type === "touchmove") this._point = this._map.mouseEventToContainerPoint(e.touches[0]);
else this._point = this._map.mouseEventToContainerPoint(e);
@ -110,7 +126,6 @@ export var BoxSelect = Handler.extend({
DomUtil.removeClass(this._container, "leaflet-crosshair");
}
DomUtil.enableTextSelection();
DomUtil.enableImageDrag();
this._map.dragging.enable();
@ -121,38 +136,10 @@ export var BoxSelect = Handler.extend({
contextmenu: DomEvent.stop,
touchmove: this._onMouseMove,
touchend: this._onMouseUp,
touchstart: this._onKeyDown,
mousemove: this._onMouseMove,
mouseup: this._onMouseUp,
keydown: this._onKeyDown,
},
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 { TargetMarker } from "./markers/targetmarker";
import {
AirbaseSelectedEvent,
AppStateChangedEvent,
CoalitionAreaSelectedEvent,
ConfigLoadedEvent,
@ -114,6 +113,7 @@ export class Map extends L.Map {
#lastMouseCoordinates: L.LatLng = new L.LatLng(0, 0);
#previousZoom: number = 0;
#keepRelativePositions: boolean = false;
#enableSelection: boolean = false;
/* Camera control plugin */
#slaveDCSCamera: boolean = false;
@ -360,6 +360,14 @@ export class Map extends L.Map {
shiftKey: 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", {
label: "Increase camera zoom",
keyUpCallback: () => this.increaseCameraZoom(),
@ -725,6 +733,14 @@ export class Map extends L.Map {
return this.#keepRelativePositions;
}
setEnableSelection(enableSelection: boolean) {
this.#enableSelection = enableSelection;
}
getEnableSelection() {
return this.#enableSelection;
}
increaseCameraZoom() {
//const slider = document.querySelector(`label[title="${DCS_LINK_RATIO}"] input`);
//if (slider instanceof HTMLInputElement) {
@ -769,7 +785,6 @@ export class Map extends L.Map {
this.#currentEffectMarker = null;
if (state !== OlympusState.UNIT_CONTROL) getApp().getUnitsManager().deselectAllUnits();
if (state !== OlympusState.DRAW || (state === OlympusState.DRAW && subState !== DrawSubState.EDIT)) this.deselectAllCoalitionAreas();
AirbaseSelectedEvent.dispatch(null);
/* Operations to perform when entering a state */
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 { SVGInjector } from "@tanem/svg-injector";
import { AirbaseChartData, AirbaseOptions } from "../interfaces";
@ -29,20 +29,28 @@ export class Airbase extends CustomMarker {
AppStateChangedEvent.on((state, subState) => {
const element = this.getElement();
if (element)
element.style.pointerEvents = (state === OlympusState.IDLE || state === OlympusState.AIRBASE)? "all": "none";
})
if (element) element.style.pointerEvents = state === OlympusState.IDLE || state === OlympusState.AIRBASE ? "all" : "none";
});
AirbaseSelectedEvent.on((airbase) => {
this.#selected = airbase == this;
this.#selected = (airbase === this);
if (this.getElement()?.querySelector(".airbase-icon"))
(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) {
getApp().setState(OlympusState.AIRBASE)
AirbaseSelectedEvent.dispatch(this)
DomEvent.stop(ev);
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() {
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 { Coalition } from "../types/types";
import { Carrier } from "./carrier";
import { NavyUnit } from "../unit/unit";
import { BullseyesDataChanged, CommandModeOptionsChangedEvent, InfoPopupEvent } from "../events";
import { AirbaseSelectedEvent, AppStateChangedEvent, BullseyesDataChanged, CommandModeOptionsChangedEvent, InfoPopupEvent } from "../events";
/** The MissionManager */
export class MissionManager {
@ -34,7 +33,12 @@ export class MissionManager {
#spentSpawnPoint: number = 0;
#coalitions: { red: string[]; blue: string[] } = { red: [], blue: [] };
constructor() {}
constructor() {
AppStateChangedEvent.on((state, subState) => {
if (this.getSelectedAirbase() !== null) AirbaseSelectedEvent.dispatch(null);
})
}
/** Update location of bullseyes
*
@ -209,6 +213,14 @@ export class MissionManager {
return this.#frameRate;
}
getSelectedAirbase() {
const airbase = Object.values(this.#airbases).find((airbase: Airbase | Carrier) => {
return airbase.getSelected();
});
return airbase ?? null;
}
#setcommandModeOptions(commandModeOptions: CommandModeOptions) {
/* Refresh all the data if we have exited the NONE state */
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 { keyEventWasInInput } from "../other/utils";
@ -6,34 +7,40 @@ export class Shortcut {
#id: string;
#options: ShortcutOptions;
#keydown: boolean = false;
#modal: boolean = false;
constructor(id, options: ShortcutOptions) {
this.#id = id;
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 */
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();
options.keyUpCallback(ev);
this.#keydown = false;
this.getOptions().keyUpCallback(ev);
}
});
/* On keydown, check exactly if the requested key combination is being pressed */
document.addEventListener("keydown", (ev: any) => {
if (this.#modal) return;
if (
!(this.#keydown || keyEventWasInInput(ev) || options.code !== ev.code) &&
(options.altKey === undefined || ev.altKey === (options.altKey ?? ev.code.indexOf("Alt") >= 0)) &&
(options.ctrlKey === undefined || ev.ctrlKey === (options.ctrlKey ?? ev.code.indexOf("Control") >= 0)) &&
(options.shiftKey === undefined || ev.shiftKey === (options.shiftKey ?? ev.code.indexOf("Shift") >= 0))
!(this.#keydown || keyEventWasInInput(ev) || this.getOptions().code !== ev.code) &&
(this.getOptions().altKey === undefined || ev.altKey === (this.getOptions().altKey ?? ev.code.indexOf("Alt") >= 0)) &&
(this.getOptions().ctrlKey === undefined || ev.ctrlKey === (this.getOptions().ctrlKey ?? ev.code.indexOf("Control") >= 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();
this.#keydown = true;
if (options.keyDownCallback) options.keyDownCallback(ev); /* Key down event is optional */
const keyDownCallback = this.getOptions().keyDownCallback
if (keyDownCallback) keyDownCallback(ev); /* Key down event is optional */
}
});
}
@ -43,7 +50,7 @@ export class Shortcut {
}
setOptions(options: ShortcutOptions) {
this.#options = { ...options };
this.#options = { ...this.#options, ...options };
}
getId() {
@ -54,14 +61,17 @@ export class Shortcut {
let actions: string[] = [];
if (this.getOptions().shiftKey) actions.push("Shift");
if (this.getOptions().altKey) actions.push("Alt");
if (this.getOptions().ctrlKey) actions.push("Ctrl")
actions.push(this.getOptions().code.replace("Key", "")
.replace("ControlLeft", "Left Ctrl")
.replace("AltLeft", "Left Alt")
.replace("ShiftLeft", "Left Shift")
.replace("ControlRight", "Right Ctrl")
.replace("AltRight", "Right Alt")
.replace("ShiftRight", "Right Shift"))
return actions
if (this.getOptions().ctrlKey) actions.push("Ctrl");
actions.push(
this.getOptions()
.code.replace("Key", "")
.replace("ControlLeft", "Left Ctrl")
.replace("AltLeft", "Left Alt")
.replace("ShiftLeft", "Left Shift")
.replace("ControlRight", "Right Ctrl")
.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 { 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 (
<div
className={`
${props.className}
fixed left-[50%] top-[50%] z-40 translate-x-[-50%] translate-y-[-50%]
rounded-xl border-[1px] border-solid border-gray-700 drop-shadow-md
`}
>
{props.children}
</div>
<>
{props.open && (
<>
<div className={`fixed left-0 top-0 z-30 h-full w-full bg-[#111111]/95`}></div>
<div
className={`
${props.className}
fixed left-[50%] top-[50%] z-40 translate-x-[-50%]
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 { Shortcut } from "../../shortcut/shortcut";
import { BindShortcutRequestEvent, ShortcutsChangedEvent } from "../../events";
import { OlToggle } from "../components/oltoggle";
export function KeybindModal(props: { open: boolean }) {
const [shortcuts, setShortcuts] = useState({} as { [key: string]: Shortcut });
@ -23,180 +22,139 @@ export function KeybindModal(props: { open: boolean }) {
document.addEventListener("keydown", (ev) => {
if (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(() => {
setCode(shortcut?.getOptions().code ?? null)
setShiftKey(shortcut?.getOptions().shiftKey)
setAltKey(shortcut?.getOptions().altKey)
setCtrlKey(shortcut?.getOptions().ctrlKey)
}, [shortcut])
setCode(shortcut?.getOptions().code ?? null);
setShiftKey(shortcut?.getOptions().shiftKey);
setAltKey(shortcut?.getOptions().altKey);
setCtrlKey(shortcut?.getOptions().ctrlKey);
}, [shortcut]);
let available: null | boolean = code ? true : null;
let inUseShortcut: null | Shortcut = null;
let inUseShortcuts: Shortcut[] = [];
for (let id in shortcuts) {
if (id !== shortcut?.getId() &&
if (
id !== shortcut?.getId() &&
code === shortcuts[id].getOptions().code &&
((shiftKey === undefined && shortcuts[id].getOptions().shiftKey !== undefined) ||
(shiftKey !== undefined && shortcuts[id].getOptions().shiftKey === undefined) ||
shiftKey === shortcuts[id].getOptions().shiftKey) && (
(altKey === undefined && shortcuts[id].getOptions().altKey !== undefined) ||
shiftKey === shortcuts[id].getOptions().shiftKey) &&
((altKey === undefined && shortcuts[id].getOptions().altKey !== undefined) ||
(altKey !== undefined && shortcuts[id].getOptions().altKey === undefined) ||
altKey === shortcuts[id].getOptions().altKey) && (
(ctrlKey === undefined && shortcuts[id].getOptions().ctrlKey !== undefined) ||
altKey === shortcuts[id].getOptions().altKey) &&
((ctrlKey === undefined && shortcuts[id].getOptions().ctrlKey !== undefined) ||
(ctrlKey !== undefined && shortcuts[id].getOptions().ctrlKey === undefined) ||
ctrlKey === shortcuts[id].getOptions().ctrlKey)
) {
available = false;
inUseShortcut = shortcuts[id];
inUseShortcuts.push(shortcuts[id]);
}
}
return (
<>
{props.open && (
<>
<Modal
<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-4">
<div className={`flex flex-col items-start gap-2`}>
<span
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
text-gray-800 text-md
dark:text-white
`}
>
<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">{code}</div>
<div className="flex flex-col gap-2">
<div className="flex gap-2">
<OlToggle
onClick={() => {
if (shiftKey === false) setShiftKey(undefined);
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>
{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 + "}
{altKey && "Alt + "}
{shiftKey && "Shift + "}
{code}
</div>
<div className="text-white">
{available === true && <div className="text-green-600">Keybind is free!</div>}
{available === false && (
<div className="flex flex-col gap-2">
<div className="flex gap-2">
Keybind is already in use: <div className={`
flex flex-wrap gap-2 font-bold text-orange-600
`}>{inUseShortcuts.map((shortcut) => <span>{shortcut.getOptions().label}</span>)}</div>
</div>
<div className="text-gray-500">A key combination can be assigned to multiple actions, and all bound actions will fire</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 { Modal } from "./components/modal";
import { Card } from "./components/card";
import { ErrorCallout } from "../../ui/components/olcallout";
import { ErrorCallout } from "../components/olcallout";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { faArrowRight, faCheckCircle, faExternalLink } from "@fortawesome/free-solid-svg-icons";
import { getApp, VERSION } from "../../olympusapp";
import { sha256 } from "js-sha256";
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
const [password, setPassword] = useState("");
const [profileName, setProfileName] = useState("Game Master");
const [profileName, setProfileName] = useState("");
const [checkingPassword, setCheckingPassword] = useState(false);
const [loginError, setLoginError] = useState(false);
const [commandMode, setCommandMode] = useState(null as null | string);
useEffect(() => {
/* Set the profile name */
getApp().setProfile(profileName);
}, [profileName])
if (profileName !== "") getApp().setProfile(profileName);
}, [profileName]);
function checkPassword(password: string) {
setCheckingPassword(true);
@ -51,14 +50,14 @@ export function LoginModal(props: {}) {
getApp().setState(OlympusState.IDLE);
/* If no profile exists already with that name, create it from scratch from the defaults */
if (getApp().getProfile() === null)
getApp().saveProfile();
if (getApp().getProfile() === null) getApp().saveProfile();
/* Load the profile */
getApp().loadProfile();
}
return (
<Modal
open={props.open}
className={`
inline-flex h-[75%] max-h-[570px] w-[80%] max-w-[1100px] overflow-y-auto
scroll-smooth bg-white
@ -239,9 +238,7 @@ export function LoginModal(props: {}) {
required
/>
</div>
<div className="text-xs text-gray-400">
The profile name you choose determines the saved key binds, groups and options you see.
</div>
<div className="text-xs text-gray-400">The profile name you choose determines the saved key binds, groups and options you see.</div>
<div className="flex">
<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">
{airbase?.getChartData().runways.map((runway, idx) => {
return (
<>
<div key={idx}>
{Object.keys(runway.headings[0]).map((runwayName) => {
return (
<div key={`${idx}-${runwayName}`} className={`
@ -137,7 +137,7 @@ export function AirbaseMenu(props: { open: boolean; onClose: () => void; childre
</div>
);
})}
</>
</div>
);
})}
</div>

View File

@ -19,7 +19,7 @@ export function AudioMenu(props: { open: boolean; onClose: () => void; children?
const [audioManagerEnabled, setAudioManagerEnabled] = useState(false);
const [activeSource, setActiveSource] = useState(null as AudioSource | null);
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 */
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,
};
return (
<>
<div
className={`
absolute rounded-br-md rounded-tr-md border-2 border-l-0
`}
style={{
top: `${(startRect.bottom + startRect.top) / 2}px`,
left: `${startRect.right}px`,
height: `${endRect.top - startRect.top + (endRect.height - startRect.height) / 2}px`,
width: `${(lineCounters[idx] - 1) * lineDistance + 30}px`,
borderColor: lineColors[idx],
}}
></div>
</>
<div
key={idx}
className={`
absolute rounded-br-md rounded-tr-md border-2 border-l-0
`}
style={{
top: `${(startRect.bottom + startRect.top) / 2}px`,
left: `${startRect.right}px`,
height: `${endRect.top - startRect.top + (endRect.height - startRect.height) / 2}px`,
width: `${(lineCounters[idx] - 1) * lineDistance + 30}px`,
borderColor: lineColors[idx],
}}
/>
);
}
})
@ -281,29 +280,26 @@ export function AudioMenu(props: { open: boolean; onClose: () => void; children?
right: (div as HTMLDivElement).offsetLeft + (div as HTMLDivElement).clientWidth,
};
return (
<>
<div>
<div
data-active={activeSource === sources[idx]}
className={`
absolute translate-y-[-50%] cursor-pointer rounded-full
bg-blue-600 p-1 text-xs text-white
data-[active='true']:bg-white
data-[active='true']:text-blue-600
hover:bg-blue-800
`}
style={{
top: `${(divRect.bottom + divRect.top) / 2}px`,
left: `${divRect.right - 10}px`,
}}
onClick={() => {
activeSource !== sources[idx] ? setActiveSource(sources[idx]) : setActiveSource(null);
}}
>
<FaPlus></FaPlus>
</div>
</div>
</>
<div
key={idx}
data-active={activeSource === sources[idx]}
className={`
absolute translate-y-[-50%] cursor-pointer rounded-full
bg-blue-600 p-1 text-xs text-white
data-[active='true']:bg-white
data-[active='true']:text-blue-600
hover:bg-blue-800
`}
style={{
top: `${(divRect.bottom + divRect.top) / 2}px`,
left: `${divRect.right - 10}px`,
}}
onClick={() => {
activeSource !== sources[idx] ? setActiveSource(sources[idx]) : setActiveSource(null);
}}
>
<FaPlus></FaPlus>
</div>
);
}
})}
@ -318,28 +314,25 @@ export function AudioMenu(props: { open: boolean; onClose: () => void; children?
right: (div as HTMLDivElement).offsetLeft + (div as HTMLDivElement).clientWidth,
};
return (
<>
<div>
<div
className={`
absolute translate-y-[-50%] cursor-pointer
rounded-full bg-blue-600 p-1 text-xs text-white
hover:bg-blue-800
`}
style={{
top: `${(divRect.bottom + divRect.top) / 2}px`,
left: `${divRect.right - 10}px`,
}}
onClick={() => {
if (activeSource.getConnectedTo().includes(sinks[idx])) activeSource.disconnect(sinks[idx]);
else activeSource.connect(sinks[idx]);
}}
>
{" "}
{activeSource.getConnectedTo().includes(sinks[idx]) ? <FaMinus></FaMinus> : <FaPlus></FaPlus>}
</div>
</div>
</>
<div
key={idx}
className={`
absolute translate-y-[-50%] cursor-pointer rounded-full
bg-blue-600 p-1 text-xs text-white
hover:bg-blue-800
`}
style={{
top: `${(divRect.bottom + divRect.top) / 2}px`,
left: `${divRect.right - 10}px`,
}}
onClick={() => {
if (activeSource.getConnectedTo().includes(sinks[idx])) activeSource.disconnect(sinks[idx]);
else activeSource.connect(sinks[idx]);
}}
>
{" "}
{activeSource.getConnectedTo().includes(sinks[idx]) ? <FaMinus></FaMinus> : <FaPlus></FaPlus>}
</div>
);
}
})}

View File

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

View File

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

View File

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

View File

@ -15,12 +15,11 @@ export function InfoBar(props: {}) {
}, []);
return (
<div
className={`absolute left-[50%] top-16`}
>
<div className={`absolute left-[50%] top-16`}>
{messages.slice(Math.max(0, messages.length - 4), Math.max(0, messages.length)).map((message, idx) => {
return (
<div
key={idx}
className={`
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

View File

@ -38,7 +38,7 @@ export function OptionsMenu(props: { open: boolean; onClose: () => void; childre
`}
>
<OlAccordion
onClick={() => setOpenAccordion(openAccordion === Accordion.NONE ? Accordion.BINDINGS : Accordion.NONE)}
onClick={() => setOpenAccordion(openAccordion !== Accordion.BINDINGS ? Accordion.BINDINGS : Accordion.NONE)}
open={openAccordion === Accordion.BINDINGS}
title="Key bindings"
>
@ -53,9 +53,10 @@ export function OptionsMenu(props: { open: boolean; onClose: () => void; childre
.map(([id, shortcut]) => {
return (
<div
key={id}
className={`
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
`}
onClick={() => {
@ -65,39 +66,9 @@ export function OptionsMenu(props: { open: boolean; onClose: () => void; childre
>
<span>{shortcut.getOptions().label}</span>
<span className="flex gap-1">
{shortcut.getOptions().altKey ? (
<div className="flex gap-1">
<div className={`text-green-500`}>Alt</div> +{" "}
</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().ctrlKey && "Ctrl + "}
{shortcut.getOptions().altKey && "Alt + "}
{shortcut.getOptions().shiftKey && "Shift + "}
{shortcut.getOptions().code}
</span>
</div>
@ -107,7 +78,7 @@ export function OptionsMenu(props: { open: boolean; onClose: () => void; childre
</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}
title="Map options"
>
@ -158,7 +129,7 @@ export function OptionsMenu(props: { open: boolean; onClose: () => void; childre
<div
className={`
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
`}
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>
<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
cursor-pointer p-2
dark:hover:bg-olympus-400
`}
onClick={() => getApp().getMap().setOption("hideGroupMembers", !mapOptions.hideGroupMembers)}
@ -181,7 +152,7 @@ export function OptionsMenu(props: { open: boolean; onClose: () => void; childre
<div
className={`
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
`}
onClick={() => getApp().getMap().setOption("showMinimap", !mapOptions.showMinimap)}
@ -192,7 +163,7 @@ export function OptionsMenu(props: { open: boolean; onClose: () => void; childre
</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}
title="Camera plugin options"
>

View File

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

View File

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

View File

@ -10,7 +10,7 @@ import { OptionsMenu } from "./panels/optionsmenu";
import { MapHiddenTypes, MapOptions } from "../types/types";
import { NO_SUBSTATE, OlympusState, OlympusSubState, OptionsSubstate, SpawnSubState, UnitControlSubState } from "../constants/constants";
import { getApp, setupApp } from "../olympusapp";
import { LoginModal } from "./modals/login";
import { LoginModal } from "./modals/loginmodal";
import { MiniMapPanel } from "./panels/minimappanel";
import { UnitControlBar } from "./panels/unitcontrolbar";
@ -20,7 +20,7 @@ import { MapContextMenu } from "./contextmenus/mapcontextmenu";
import { AirbaseMenu } from "./panels/airbasemenu";
import { AudioMenu } from "./panels/audiomenu";
import { FormationMenu } from "./panels/formationmenu";
import { ProtectionPrompt } from "./modals/protectionprompt";
import { ProtectionPromptModal } from "./modals/protectionpromptmodal";
import { KeybindModal } from "./modals/keybindmodal";
import { UnitExplosionMenu } from "./panels/unitexplosionmenu";
import { JTACMenu } from "./panels/jtacmenu";
@ -65,16 +65,8 @@ export function UI() {
>
<Header />
<div className="flex h-full w-full flex-row-reverse">
{appState === OlympusState.LOGIN && (
<>
<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} />
<LoginModal open={appState === OlympusState.LOGIN} />
<ProtectionPromptModal 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" />