diff --git a/frontend/react/src/audio/audiomanager.ts b/frontend/react/src/audio/audiomanager.ts index 589a3cc9..12b73bb2 100644 --- a/frontend/react/src/audio/audiomanager.ts +++ b/frontend/react/src/audio/audiomanager.ts @@ -42,7 +42,7 @@ export class AudioManager { config.audio.WSPort ? this.setPort(config.audio.WSPort) : this.setEndpoint(config.audio.WSEndpoint); }); - let PTTKeys = ["KeyZ", "KeyX", "KeyC", "KeyV", "KeyB", "KeyN", "KeyM", "KeyK", "KeyL"]; + let PTTKeys = ["KeyZ", "KeyX", "KeyC", "KeyV", "KeyB", "KeyN", "KeyM", "KeyComma", "KeyDot"]; PTTKeys.forEach((key, idx) => { getApp() .getShortcutManager() @@ -50,7 +50,8 @@ export class AudioManager { label: `PTT ${idx} active`, keyDownCallback: () => this.getSinks()[idx]?.setPtt(true), keyUpCallback: () => this.getSinks()[idx]?.setPtt(false), - code: key + code: key, + shiftKey: true }); }); } diff --git a/frontend/react/src/constants/constants.ts b/frontend/react/src/constants/constants.ts index 0ee4da18..284171db 100644 --- a/frontend/react/src/constants/constants.ts +++ b/frontend/react/src/constants/constants.ts @@ -265,7 +265,7 @@ export enum OlympusState { MAIN_MENU = "Main menu", UNIT_CONTROL = "Unit control", SPAWN = "Spawn", - STARRED_SPAWN = "Starred spawn", + SPAWN_CONTEXT = "Spawn context", DRAW = "Draw", JTAC = "JTAC", OPTIONS = "Options", @@ -336,6 +336,7 @@ export const MAP_OPTIONS_DEFAULTS: MapOptions = { cameraPluginRatio: 1, cameraPluginEnabled: false, cameraPluginMode: "map", + tabletMode: false }; export const MAP_HIDDEN_TYPES_DEFAULTS = { @@ -457,7 +458,7 @@ export namespace ContextActions { { executeImmediately: true, type: ContextActionType.MOVE, - hotkey: "KeyZ", + code: "Space", } ); @@ -474,7 +475,7 @@ export namespace ContextActions { .getUnitsManager() .addDestination(targetPosition, getApp().getMap().getOptions().keepRelativePositions, getApp().getMap().getDestinationRotation(), units); }, - { type: ContextActionType.MOVE, hotkey: "KeyX" } + { type: ContextActionType.MOVE, code: null } ); export const PATH = new ContextAction( @@ -489,7 +490,7 @@ export namespace ContextActions { .getUnitsManager() .addDestination(targetPosition, getApp().getMap().getOptions().keepRelativePositions, getApp().getMap().getDestinationRotation(), units); }, - { type: ContextActionType.MOVE, hotkey: "KeyC" } + { type: ContextActionType.MOVE, code: "ControlLeft" } ); export const DELETE = new ContextAction( @@ -504,6 +505,7 @@ export namespace ContextActions { { executeImmediately: true, type: ContextActionType.DELETE, + code: "Delete" } ); @@ -520,6 +522,8 @@ export namespace ContextActions { { executeImmediately: true, type: ContextActionType.DELETE, + code: "Delete", + ctrlKey: true } ); @@ -532,7 +536,7 @@ export namespace ContextActions { (units: Unit[]) => { getApp().getMap().centerOnUnit(units[0]); }, - { executeImmediately: true, type: ContextActionType.OTHER } + { executeImmediately: true, type: ContextActionType.OTHER, code: "KeyM", altKey: true } ); export const REFUEL = new ContextAction( @@ -544,7 +548,7 @@ export namespace ContextActions { (units: Unit[]) => { getApp().getUnitsManager().refuel(units); }, - { executeImmediately: true, type: ContextActionType.ADMIN } + { executeImmediately: true, type: ContextActionType.ADMIN, code: "KeyV" } ); export const FOLLOW = new ContextAction( @@ -562,7 +566,7 @@ export namespace ContextActions { ); } }, - { type: ContextActionType.ADMIN } + { type: ContextActionType.ADMIN, code: "KeyF" } ); export const BOMB = new ContextAction( @@ -577,7 +581,7 @@ export namespace ContextActions { .getUnitsManager() .bombPoint(targetPosition, getApp().getMap().getOptions().keepRelativePositions, getApp().getMap().getDestinationRotation(), units); }, - { type: ContextActionType.ENGAGE } + { type: ContextActionType.ENGAGE, code: "KeyB" } ); export const CARPET_BOMB = new ContextAction( @@ -592,7 +596,7 @@ export namespace ContextActions { .getUnitsManager() .carpetBomb(targetPosition, getApp().getMap().getOptions().keepRelativePositions, getApp().getMap().getDestinationRotation(), units); }, - { type: ContextActionType.ENGAGE } + { type: ContextActionType.ENGAGE, code: "KeyB", altKey: true } ); export const LAND = new ContextAction( @@ -604,7 +608,7 @@ export namespace ContextActions { (units: Unit[], _, targetPosition: LatLng | null) => { if (targetPosition) getApp().getUnitsManager().landAt(targetPosition, units); }, - { type: ContextActionType.ADMIN } + { type: ContextActionType.ADMIN, code: "KeyL" } ); export const LAND_AT_POINT = new ContextAction( @@ -619,7 +623,7 @@ export namespace ContextActions { .getUnitsManager() .landAtPoint(targetPosition, getApp().getMap().getOptions().keepRelativePositions, getApp().getMap().getDestinationRotation(), units); }, - { type: ContextActionType.ADMIN } + { type: ContextActionType.ADMIN, code: "KeyL", altKey: true } ); export const GROUP = new ContextAction( @@ -631,7 +635,7 @@ export namespace ContextActions { (units: Unit[], _1, _2) => { getApp().getUnitsManager().createGroup(units); }, - { executeImmediately: true, type: ContextActionType.OTHER } + { executeImmediately: true, type: ContextActionType.OTHER, code: "KeyG" } ); export const ATTACK = new ContextAction( @@ -643,7 +647,7 @@ export namespace ContextActions { (units: Unit[], targetUnit: Unit | null, _) => { if (targetUnit) getApp().getUnitsManager().attackUnit(targetUnit.ID, units); }, - { type: ContextActionType.ENGAGE } + { type: ContextActionType.ENGAGE, code: "KeyZ" } ); export const FIRE_AT_AREA = new ContextAction( @@ -658,7 +662,7 @@ export namespace ContextActions { .getUnitsManager() .fireAtArea(targetPosition, getApp().getMap().getOptions().keepRelativePositions, getApp().getMap().getDestinationRotation(), units); }, - { type: ContextActionType.ENGAGE } + { type: ContextActionType.ENGAGE, code: "KeyZ", altKey: true } ); export const SIMULATE_FIRE_FIGHT = new ContextAction( @@ -673,6 +677,6 @@ export namespace ContextActions { .getUnitsManager() .simulateFireFight(targetPosition, getApp().getMap().getOptions().keepRelativePositions, getApp().getMap().getDestinationRotation(), units); }, - { type: ContextActionType.ADMIN } + { type: ContextActionType.ADMIN, code: "KeyX" } ); } diff --git a/frontend/react/src/events.ts b/frontend/react/src/events.ts index 0f8265cd..1df318c4 100644 --- a/frontend/react/src/events.ts +++ b/frontend/react/src/events.ts @@ -98,19 +98,6 @@ export class InfoPopupEvent { } } -export class HideMenuEvent { - static on(callback: (hidden: boolean) => void) { - document.addEventListener(this.name, (ev: CustomEventInit) => { - callback(ev.detail.hidden); - }); - } - - static dispatch(hidden: boolean) { - document.dispatchEvent(new CustomEvent(this.name, { detail: { hidden } })); - console.log(`Event ${this.name} dispatched`); - } -} - export class ShortcutsChangedEvent { static on(callback: (shortcuts: { [key: string]: Shortcut }) => void) { document.addEventListener(this.name, (ev: CustomEventInit) => { @@ -332,7 +319,7 @@ export class UnitContextMenuRequestEvent { } } -export class StarredSpawnContextMenuRequestEvent { +export class SpawnContextMenuRequestEvent { static on(callback: (latlng: L.LatLng) => void) { document.addEventListener(this.name, (ev: CustomEventInit) => { callback(ev.detail.latlng); diff --git a/frontend/react/src/map/map.ts b/frontend/react/src/map/map.ts index 7b01e166..0a6efce3 100644 --- a/frontend/react/src/map/map.ts +++ b/frontend/react/src/map/map.ts @@ -53,7 +53,7 @@ import { MapSourceChangedEvent, MouseMovedEvent, SelectionClearedEvent, - StarredSpawnContextMenuRequestEvent, + SpawnContextMenuRequestEvent, StarredSpawnsChangedEvent, UnitDeselectedEvent, UnitSelectedEvent, @@ -79,12 +79,6 @@ export class Map extends L.Map { #mapLayers: any = defaultMapLayers; #mapMirrors: any = defaultMapMirrors; - /* Inputs timers */ - #mouseCooldownTimer: number = 0; - #shortPressTimer: number = 0; - #longPressTimer: number = 0; - #selecting: boolean = false; - /* Camera keyboard panning control */ defaultPanDelta: number = 100; #panInterval: number | null = null; @@ -103,13 +97,13 @@ export class Map extends L.Map { #miniMapPolyline: L.Polyline; /* Other state controls */ - #isMouseOnCooldown: boolean = false; #isZooming: boolean = false; #isDragging: boolean = false; #isMouseDown: boolean = false; #lastMousePosition: L.Point = new L.Point(0, 0); #lastMouseCoordinates: L.LatLng = new L.LatLng(0, 0); #previousZoom: number = 0; + #selecting: boolean = false; /* Camera control plugin */ #slaveDCSCamera: boolean = false; @@ -189,10 +183,10 @@ export class Map extends L.Map { this.on("selectionstart", (e: any) => this.#onSelectionStart(e)); this.on("selectionend", (e: any) => this.#onSelectionEnd(e)); - this.on("dblclick", (e: any) => this.#onDoubleClick(e)); this.on("mouseup", (e: any) => this.#onMouseUp(e)); this.on("mousedown", (e: any) => this.#onMouseDown(e)); - this.on("contextmenu", (e: any) => e.originalEvent.preventDefault()); + this.on("click", (e: any) => this.#onLeftClick(e)); + this.on("contextmenu", (e: any) => this.#onRightClick(e)); this.on("mousemove", (e: any) => this.#onMouseMove(e)); @@ -301,67 +295,78 @@ export class Map extends L.Map { label: "Hide/show labels", keyUpCallback: () => this.setOption("showUnitLabels", !this.getOptions().showUnitLabels), code: "KeyL", + shiftKey: true }) .addShortcut("toggleAcquisitionRings", { label: "Hide/show acquisition rings", keyUpCallback: () => this.setOption("showUnitsAcquisitionRings", !this.getOptions().showUnitsAcquisitionRings), code: "KeyE", + shiftKey: true }) .addShortcut("toggleEngagementRings", { label: "Hide/show engagement rings", keyUpCallback: () => this.setOption("showUnitsEngagementRings", !this.getOptions().showUnitsEngagementRings), code: "KeyQ", + shiftKey: true }) .addShortcut("toggleHideShortEngagementRings", { label: "Hide/show short range rings", keyUpCallback: () => this.setOption("hideUnitsShortRangeRings", !this.getOptions().hideUnitsShortRangeRings), code: "KeyR", + shiftKey: true }) .addShortcut("toggleDetectionLines", { label: "Hide/show detection lines", keyUpCallback: () => this.setOption("showUnitTargets", !this.getOptions().showUnitTargets), code: "KeyF", + shiftKey: true }) .addShortcut("toggleGroupMembers", { label: "Hide/show group members", keyUpCallback: () => this.setOption("hideGroupMembers", !this.getOptions().hideGroupMembers), code: "KeyG", + shiftKey: true }) .addShortcut("toggleRelativePositions", { label: "Toggle group movement mode", keyUpCallback: () => this.setOption("keepRelativePositions", !this.getOptions().keepRelativePositions), code: "KeyP", + shiftKey: true }) .addShortcut("increaseCameraZoom", { label: "Increase camera zoom", - altKey: true, keyUpCallback: () => this.increaseCameraZoom(), code: "Equal", + shiftKey: true }) .addShortcut("decreaseCameraZoom", { label: "Decrease camera zoom", - altKey: true, keyUpCallback: () => this.decreaseCameraZoom(), code: "Minus", + shiftKey: true }); for (let contextActionName in ContextActions) { - if (ContextActions[contextActionName].getOptions().hotkey) { + const contextAction = ContextActions[contextActionName] as ContextAction; + if (contextAction.getOptions().code) { getApp() .getShortcutManager() .addShortcut(`${contextActionName}Hotkey`, { - label: ContextActions[contextActionName].getLabel(), - code: ContextActions[contextActionName].getOptions().hotkey, - shiftKey: true, - keyUpCallback: () => { + label: contextAction.getLabel(), + code: contextAction.getOptions().code as string, + shiftKey: contextAction.getOptions().shiftKey, + altKey: contextAction.getOptions().altKey, + ctrlKey: contextAction.getOptions().ctrlKey, + keyDownCallback: () => { const contextActionSet = this.getContextActionSet(); - if ( - getApp().getState() === OlympusState.UNIT_CONTROL && - contextActionSet && - ContextActions[contextActionName].getId() in contextActionSet.getContextActions() - ) { - if (ContextActions[contextActionName].getOptions().executeImmediately) ContextActions[contextActionName].executeCallback(); - else this.setContextAction(ContextActions[contextActionName]); + if (getApp().getState() === OlympusState.UNIT_CONTROL && contextActionSet && contextAction.getId() in contextActionSet.getContextActions()) { + if (contextAction.getOptions().executeImmediately) contextAction.executeCallback(null, null); + else this.setContextAction(contextAction); + } + }, + keyUpCallback: () => { + if (getApp().getState() === OlympusState.UNIT_CONTROL && !contextAction.getOptions().executeImmediately) { + this.setContextAction(null); } }, }); @@ -517,212 +522,6 @@ export class Map extends L.Map { ContextActionChangedEvent.dispatch(this.#contextAction); } - getCurrentControls() { - //const touch = matchMedia("(hover: none)").matches; - //if (getApp().getState() === IDLE) { - // return [ - // { - // actions: [touch ? faHandPointer : "LMB"], - // target: faJetFighter, - // text: "Select unit", - // }, - // touch - // ? { - // actions: [faHandPointer, "Drag"], - // target: faMap, - // text: "Box selection", - // } - // : { - // actions: ["Shift", "LMB", "Drag"], - // target: faMap, - // text: "Box selection", - // }, - // { - // actions: [touch ? faHandPointer : "LMB", "Drag"], - // target: faMap, - // text: "Move map location", - // }, - // ]; - //} else if (getApp().getState() === OlympusState.SPAWN_UNIT) { - // return [ - // { - // actions: [touch ? faHandPointer : "LMB"], - // target: faMap, - // text: "Spawn unit", - // }, - // { - // actions: [touch ? faHandPointer : "LMB", 2], - // target: faMap, - // text: "Exit spawn mode", - // }, - // { - // actions: [touch ? faHandPointer : "LMB", "Drag"], - // target: faMap, - // text: "Move map location", - // }, - // ]; - //} else if (getApp().getState() === SPAWN_EFFECT) { - // return [ - // { - // actions: [touch ? faHandPointer : "LMB"], - // target: faMap, - // text: "Spawn effect", - // }, - // { - // actions: [touch ? faHandPointer : "LMB", 2], - // target: faMap, - // text: "Exit spawn mode", - // }, - // { - // actions: [touch ? faHandPointer : "LMB", "Drag"], - // target: faMap, - // text: "Move map location", - // }, - // ]; - //} else if (getApp().getState() === CONTEXT_ACTION) { - // let controls = [ - // { - // actions: [touch ? faHandPointer : "LMB"], - // target: faMap, - // text: "Deselect units", - // }, - // { - // actions: [touch ? faHandPointer : "LMB", "Drag"], - // target: faMap, - // text: "Move map location", - // }, - // ]; - // - // if (this.#contextAction) { - // controls.push({ - // actions: [touch ? faHandPointer : "LMB"], - // target: this.#contextAction.getTarget() === "unit" ? faJetFighter : faMap, - // text: this.#contextAction?.getLabel() ?? "", - // }); - // } - // - // if (!touch && this.#defaultContextAction) { - // controls.push({ - // actions: ["RMB"], - // target: faMap, - // text: this.#defaultContextAction?.getLabel() ?? "", - // }); - // controls.push({ - // actions: ["RMB", "hold"], - // target: faMap, - // text: "Open context menu", - // }); - // } - // - // return controls; - //} else if (getApp().getState() === COALITIONAREA_EDIT) { - // return [ - // { - // actions: [touch ? faHandPointer : "LMB"], - // target: faDrawPolygon, - // text: "Select shape", - // }, - // { - // actions: [touch ? faHandPointer : "LMB", 2], - // target: faMap, - // text: "Exit drawing mode", - // }, - // { - // actions: [touch ? faHandPointer : "LMB", "Drag"], - // target: faMap, - // text: "Move map location", - // }, - // ]; - //} else if (getApp().getState() === COALITIONAREA_DRAW_POLYGON) { - // return [ - // { - // actions: [touch ? faHandPointer : "LMB"], - // target: faMap, - // text: "Add vertex to polygon", - // }, - // { - // actions: [touch ? faHandPointer : "LMB", 2], - // target: faMap, - // text: "Finalize polygon", - // }, - // { - // actions: [touch ? faHandPointer : "LMB", "Drag"], - // target: faMap, - // text: "Move map location", - // }, - // ]; - //} else if (getApp().getState() === COALITIONAREA_DRAW_CIRCLE) { - // return [ - // { - // actions: [touch ? faHandPointer : "LMB"], - // target: faMap, - // text: "Add circle", - // }, - // { - // actions: [touch ? faHandPointer : "LMB", "Drag"], - // target: faMap, - // text: "Move map location", - // }, - // ]; - //} else if (getApp().getState() === SELECT_JTAC_TARGET) { - // return [ - // { - // actions: [touch ? faHandPointer : "LMB"], - // target: faMap, - // text: "Set unit/location as target", - // }, - // { - // actions: [touch ? faHandPointer : "LMB", 2], - // target: faMap, - // text: "Exit selection mode", - // }, - // { - // actions: [touch ? faHandPointer : "LMB", "Drag"], - // target: faMap, - // text: "Move map location", - // }, - // ]; - //} else if (getApp().getState() === SELECT_JTAC_ECHO) { - // return [ - // { - // actions: [touch ? faHandPointer : "LMB"], - // target: faMap, - // text: "Set location as ECHO point", - // }, - // { - // actions: [touch ? faHandPointer : "LMB", 2], - // target: faMap, - // text: "Exit selection mode", - // }, - // { - // actions: [touch ? faHandPointer : "LMB", "Drag"], - // target: faMap, - // text: "Move map location", - // }, - // ]; - //} else if (getApp().getState() === SELECT_JTAC_IP) { - // return [ - // { - // actions: [touch ? faHandPointer : "LMB"], - // target: faMap, - // text: "Set location as IP point", - // }, - // { - // actions: [touch ? faHandPointer : "LMB", 2], - // target: faMap, - // text: "Exit selection mode", - // }, - // { - // actions: [touch ? faHandPointer : "LMB", "Drag"], - // target: faMap, - // text: "Move map location", - // }, - // ]; - //} else { - // return []; - //} - } - deselectAllCoalitionAreas() { if (this.getSelectedCoalitionArea() !== null) { CoalitionAreaSelectedEvent.dispatch(null); @@ -905,12 +704,6 @@ export class Map extends L.Map { this.#contextActionSet?.getDefaultContextAction()?.executeCallback(targetUnit, targetPosition, originalEvent); } - preventClicks() { - console.log("Preventing clicks on map"); - window.clearTimeout(this.#shortPressTimer); - window.clearTimeout(this.#longPressTimer); - } - /* Event handlers */ #onStateChanged(state: OlympusState, subState: OlympusSubState) { /* Operations to perform when leaving a state */ @@ -979,71 +772,26 @@ export class Map extends L.Map { #onMouseUp(e: any) { this.#isMouseDown = false; - window.clearTimeout(this.#longPressTimer); - - this.scrollWheelZoom.enable(); - this.dragging.enable(); - this.#isRotatingDestination = false; - this.#isMouseOnCooldown = true; - this.#mouseCooldownTimer = window.setTimeout(() => { - this.#isMouseOnCooldown = false; - }, 200); } #onMouseDown(e: any) { this.#isMouseDown = true; - - if (this.#isMouseOnCooldown) { - return; - } - if (this.#contextAction?.getTarget() === ContextActionTarget.POINT && e.originalEvent?.button === 2) this.#isRotatingDestination = true; - this.scrollWheelZoom.disable(); - - this.#shortPressTimer = window.setTimeout(() => { - /* If the mouse is no longer being pressed, execute the short press action */ - if (!this.#isMouseDown) this.#onShortPress(e); - }, 200); - - this.#longPressTimer = window.setTimeout(() => { - /* If the mouse is still being pressed, execute the long press action */ - if (this.#isMouseDown && !this.#isDragging && !this.#isZooming) this.#onLongPress(e); - }, 350); } - #onDoubleClick(e: any) { - console.log(`Double click at ${e.latlng}`); - - window.clearTimeout(this.#shortPressTimer); - window.clearTimeout(this.#longPressTimer); - - if (getApp().getState() === OlympusState.IDLE) { - StarredSpawnContextMenuRequestEvent.dispatch(e.latlng); - getApp().setState(OlympusState.STARRED_SPAWN); - } else { - if (getApp().getSubState() !== NO_SUBSTATE) { - getApp().setState(getApp().getState(), NO_SUBSTATE); - } else { - getApp().setState(OlympusState.IDLE); - } - } - } - - #onShortPress(e: any) { - let pressLocation: L.LatLng; - if (e.type === "touchstart") pressLocation = this.containerPointToLatLng(this.mouseEventToContainerPoint(e.touches[0])); - else pressLocation = new L.LatLng(e.latlng.lat, e.latlng.lng); - - console.log(`Short press at ${pressLocation}`); + #onLeftClick(e: L.LeafletMouseEvent) { + console.log(`Left click at ${e.latlng}`); /* Execute the short click action */ if (getApp().getState() === OlympusState.IDLE) { /* Do nothing */ + } else if (getApp().getState() === OlympusState.SPAWN_CONTEXT) { + getApp().setState(OlympusState.IDLE); } else if (getApp().getState() === OlympusState.SPAWN) { if (getApp().getSubState() === SpawnSubState.SPAWN_UNIT) { - if (e.originalEvent?.button != 2 && this.#spawnRequestTable !== null) { - this.#spawnRequestTable.unit.location = pressLocation; + if (this.#spawnRequestTable !== null) { + this.#spawnRequestTable.unit.location = e.latlng; getApp() .getUnitsManager() .spawnUnits( @@ -1054,24 +802,23 @@ export class Map extends L.Map { undefined, undefined, (hash) => { - this.addTemporaryMarker(pressLocation, this.#spawnRequestTable?.unit.unitType ?? "unknown", this.#spawnRequestTable?.coalition ?? "blue", hash); + this.addTemporaryMarker(e.latlng, this.#spawnRequestTable?.unit.unitType ?? "unknown", this.#spawnRequestTable?.coalition ?? "blue", hash); } ); } } else if (getApp().getSubState() === SpawnSubState.SPAWN_EFFECT) { - if (e.originalEvent?.button != 2 && this.#effectRequestTable !== null) { + if (this.#effectRequestTable !== null) { if (this.#effectRequestTable.type === "explosion") { - if (this.#effectRequestTable.explosionType === "High explosive") getApp().getServerManager().spawnExplosion(50, "normal", pressLocation); - else if (this.#effectRequestTable.explosionType === "Napalm") getApp().getServerManager().spawnExplosion(50, "napalm", pressLocation); - else if (this.#effectRequestTable.explosionType === "White phosphorous") - getApp().getServerManager().spawnExplosion(50, "phosphorous", pressLocation); + if (this.#effectRequestTable.explosionType === "High explosive") getApp().getServerManager().spawnExplosion(50, "normal", e.latlng); + else if (this.#effectRequestTable.explosionType === "Napalm") getApp().getServerManager().spawnExplosion(50, "napalm", e.latlng); + else if (this.#effectRequestTable.explosionType === "White phosphorous") getApp().getServerManager().spawnExplosion(50, "phosphorous", e.latlng); - this.addExplosionMarker(pressLocation); + this.addExplosionMarker(e.latlng); } else if (this.#effectRequestTable.type === "smoke") { getApp() .getServerManager() - .spawnSmoke(this.#effectRequestTable.smokeColor ?? "white", pressLocation); - this.addSmokeMarker(pressLocation, this.#effectRequestTable.smokeColor ?? "white"); + .spawnSmoke(this.#effectRequestTable.smokeColor ?? "white", e.latlng); + this.addSmokeMarker(e.latlng, this.#effectRequestTable.smokeColor ?? "white"); } } } @@ -1079,18 +826,18 @@ export class Map extends L.Map { if (getApp().getSubState() === DrawSubState.DRAW_POLYGON) { const selectedArea = this.getSelectedCoalitionArea(); if (selectedArea && selectedArea instanceof CoalitionPolygon) { - selectedArea.addTemporaryLatLng(pressLocation); + selectedArea.addTemporaryLatLng(e.latlng); } } else if (getApp().getSubState() === DrawSubState.DRAW_CIRCLE) { const selectedArea = this.getSelectedCoalitionArea(); if (selectedArea && selectedArea instanceof CoalitionCircle) { - if (selectedArea.getLatLng().lat == 0 && selectedArea.getLatLng().lng == 0) selectedArea.setLatLng(pressLocation); + if (selectedArea.getLatLng().lat == 0 && selectedArea.getLatLng().lng == 0) selectedArea.setLatLng(e.latlng); getApp().setState(OlympusState.DRAW, DrawSubState.EDIT); } } else if (getApp().getSubState() == DrawSubState.NO_SUBSTATE) { this.deselectAllCoalitionAreas(); for (let idx = 0; idx < this.#coalitionAreas.length; idx++) { - if (areaContains(pressLocation, this.#coalitionAreas[idx])) { + if (areaContains(e.latlng, this.#coalitionAreas[idx])) { this.#coalitionAreas[idx].setSelected(true); getApp().setState(OlympusState.DRAW, DrawSubState.EDIT); break; @@ -1098,17 +845,13 @@ export class Map extends L.Map { } } } else if (getApp().getState() === OlympusState.UNIT_CONTROL) { - if (e.type === "touchstart" || e.originalEvent?.buttons === 1) { - if (this.#contextAction !== null) this.executeContextAction(null, pressLocation, e.originalEvent); - else getApp().setState(OlympusState.IDLE); - } else if (e.originalEvent?.buttons === 2) { - this.executeDefaultContextAction(null, pressLocation, e.originalEvent); - } + if (this.#contextAction !== null) this.executeContextAction(null, e.latlng, e.originalEvent); + else getApp().setState(OlympusState.IDLE); } else if (getApp().getState() === OlympusState.JTAC) { // TODO less redundant way to do this if (getApp().getSubState() === JTACSubState.SELECT_TARGET) { if (!this.#targetPoint) { - this.#targetPoint = new TextMarker(pressLocation, "BP", "rgb(37 99 235)", { interactive: true, draggable: true }); + this.#targetPoint = new TextMarker(e.latlng, "BP", "rgb(37 99 235)", { interactive: true, draggable: true }); this.#targetPoint.addTo(this); this.#targetPoint.on("dragstart", (event) => { event.target.options["freeze"] = true; @@ -1120,10 +863,10 @@ export class Map extends L.Map { this.#targetPoint.on("click", (event) => { getApp().setState(OlympusState.JTAC); }); - } else this.#targetPoint.setLatLng(pressLocation); + } else this.#targetPoint.setLatLng(e.latlng); } else if (getApp().getSubState() === JTACSubState.SELECT_ECHO_POINT) { if (!this.#ECHOPoint) { - this.#ECHOPoint = new TextMarker(pressLocation, "BP", "rgb(37 99 235)", { interactive: true, draggable: true }); + this.#ECHOPoint = new TextMarker(e.latlng, "BP", "rgb(37 99 235)", { interactive: true, draggable: true }); this.#ECHOPoint.addTo(this); this.#ECHOPoint.on("dragstart", (event) => { event.target.options["freeze"] = true; @@ -1135,10 +878,10 @@ export class Map extends L.Map { this.#ECHOPoint.on("click", (event) => { getApp().setState(OlympusState.JTAC); }); - } else this.#ECHOPoint.setLatLng(pressLocation); + } else this.#ECHOPoint.setLatLng(e.latlng); } else if (getApp().getSubState() === JTACSubState.SELECT_IP) { if (!this.#IPPoint) { - this.#IPPoint = new TextMarker(pressLocation, "BP", "rgb(37 99 235)", { interactive: true, draggable: true }); + this.#IPPoint = new TextMarker(e.latlng, "BP", "rgb(37 99 235)", { interactive: true, draggable: true }); this.#IPPoint.addTo(this); this.#IPPoint.on("dragstart", (event) => { event.target.options["freeze"] = true; @@ -1150,7 +893,7 @@ export class Map extends L.Map { this.#IPPoint.on("click", (event) => { getApp().setState(OlympusState.JTAC); }); - } else this.#IPPoint.setLatLng(pressLocation); + } else this.#IPPoint.setLatLng(e.latlng); } getApp().setState(OlympusState.JTAC); this.#drawIPToTargetLine(); @@ -1158,40 +901,26 @@ export class Map extends L.Map { } } - #onLongPress(e: any) { - let pressLocation: L.LatLng; - if (e.type === "touchstart") pressLocation = this.containerPointToLatLng(this.mouseEventToContainerPoint(e.touches[0])); - else pressLocation = new L.LatLng(e.latlng.lat, e.latlng.lng); + #onRightClick(e: L.LeafletMouseEvent) { + e.originalEvent.preventDefault(); - console.log(`Long press at ${pressLocation}`); + console.log(`Right click at ${e.latlng}`); if (!this.#isDragging && !this.#isZooming) { this.deselectAllCoalitionAreas(); - if (getApp().getState() === OlympusState.IDLE) { - if (e.type === "touchstart") document.dispatchEvent(new CustomEvent("forceboxselect", { detail: e })); - else document.dispatchEvent(new CustomEvent("forceboxselect", { detail: e.originalEvent })); + if (getApp().getState() === OlympusState.IDLE || getApp().getState() === OlympusState.SPAWN_CONTEXT) { + SpawnContextMenuRequestEvent.dispatch(e.latlng); + getApp().setState(OlympusState.SPAWN_CONTEXT); } else if (getApp().getState() === OlympusState.UNIT_CONTROL) { - if (e.originalEvent?.button === 2) { - if (!this.getContextAction()) { - getApp().setState(OlympusState.UNIT_CONTROL, UnitControlSubState.MAP_CONTEXT_MENU); - MapContextMenuRequestEvent.dispatch(pressLocation); - } - } else { - if (this.#contextAction?.getTarget() === ContextActionTarget.POINT) { - this.dragging.disable(); - this.#isRotatingDestination = true; - } else { - if (e.type === "touchstart") document.dispatchEvent(new CustomEvent("forceboxselect", { detail: e })); - else document.dispatchEvent(new CustomEvent("forceboxselect", { detail: e.originalEvent })); - } + if (!this.getContextAction()) { + getApp().setState(OlympusState.UNIT_CONTROL, UnitControlSubState.MAP_CONTEXT_MENU); + MapContextMenuRequestEvent.dispatch(e.latlng); } } } } #onMouseMove(e: any) { - window.clearTimeout(this.#longPressTimer); - if (!this.#isRotatingDestination) { this.#lastMousePosition.x = e.originalEvent.x; this.#lastMousePosition.y = e.originalEvent.y; @@ -1200,8 +929,8 @@ export class Map extends L.Map { MouseMovedEvent.dispatch(e.latlng); getGroundElevation(e.latlng, (elevation) => { MouseMovedEvent.dispatch(e.latlng, elevation); - }) - + }); + if (this.#currentSpawnMarker) this.#currentSpawnMarker.setLatLng(e.latlng); if (this.#currentEffectMarker) this.#currentEffectMarker.setLatLng(e.latlng); } else { diff --git a/frontend/react/src/map/markers/smokemarker.ts b/frontend/react/src/map/markers/smokemarker.ts index ed1a2e67..a360708f 100644 --- a/frontend/react/src/map/markers/smokemarker.ts +++ b/frontend/react/src/map/markers/smokemarker.ts @@ -8,6 +8,7 @@ export class SmokeMarker extends CustomMarker { constructor(latlng: LatLngExpression, color: string, options?: MarkerOptions) { super(latlng, options); + this.options.interactive = false; this.setZIndexOffset(9999); this.#color = color; window.setTimeout(() => { diff --git a/frontend/react/src/map/markers/targetmarker.ts b/frontend/react/src/map/markers/targetmarker.ts index 94a1de0c..44290d11 100644 --- a/frontend/react/src/map/markers/targetmarker.ts +++ b/frontend/react/src/map/markers/targetmarker.ts @@ -4,6 +4,7 @@ import { CustomMarker } from "./custommarker"; export class TargetMarker extends CustomMarker { constructor(latlng: LatLngExpression, options?: MarkerOptions) { super(latlng, options); + this.options.interactive = false; this.setZIndexOffset(9999); } diff --git a/frontend/react/src/map/stylesheets/map.css b/frontend/react/src/map/stylesheets/map.css index 358845cd..5758cadc 100644 --- a/frontend/react/src/map/stylesheets/map.css +++ b/frontend/react/src/map/stylesheets/map.css @@ -176,4 +176,8 @@ .ol-explosion-icon { fill: red; +} + +path.leaflet-interactive:focus { + outline: none; } \ No newline at end of file diff --git a/frontend/react/src/mission/airbase.ts b/frontend/react/src/mission/airbase.ts index 266fdcc6..dd3d1ebe 100644 --- a/frontend/react/src/mission/airbase.ts +++ b/frontend/react/src/mission/airbase.ts @@ -4,7 +4,7 @@ import { SVGInjector } from "@tanem/svg-injector"; import { AirbaseChartData, AirbaseOptions } from "../interfaces"; import { getApp } from "../olympusapp"; import { OlympusState } from "../constants/constants"; -import { AirbaseSelectedEvent } from "../events"; +import { AirbaseSelectedEvent, AppStateChangedEvent } from "../events"; // TODO add ability to select the marker export class Airbase extends CustomMarker { @@ -27,6 +27,12 @@ export class Airbase extends CustomMarker { this.#name = options.name; this.#img = document.createElement("img"); + AppStateChangedEvent.on((state, subState) => { + const element = this.getElement(); + if (element) + element.style.pointerEvents = (state === OlympusState.IDLE || state === OlympusState.AIRBASE)? "all": "none"; + }) + AirbaseSelectedEvent.on((airbase) => { this.#selected = airbase == this; if (this.getElement()?.querySelector(".airbase-icon")) diff --git a/frontend/react/src/mission/bullseye.ts b/frontend/react/src/mission/bullseye.ts index 59be274c..ea854db6 100644 --- a/frontend/react/src/mission/bullseye.ts +++ b/frontend/react/src/mission/bullseye.ts @@ -1,10 +1,16 @@ -import { DivIcon } from "leaflet"; +import { DivIcon, LatLngExpression, MarkerOptions } from "leaflet"; import { CustomMarker } from "../map/markers/custommarker"; import { SVGInjector } from "@tanem/svg-injector"; export class Bullseye extends CustomMarker { #coalition: string = ""; + constructor(latlng: LatLngExpression, options?: MarkerOptions) { + super(latlng, options); + this.options.interactive = false; + this.setZIndexOffset(9999); + } + createIcon() { var icon = new DivIcon({ className: "leaflet-bullseye-marker", diff --git a/frontend/react/src/olympusapp.ts b/frontend/react/src/olympusapp.ts index 2dd70fe7..e783d61f 100644 --- a/frontend/react/src/olympusapp.ts +++ b/frontend/react/src/olympusapp.ts @@ -93,11 +93,11 @@ export class OlympusApp { */ getExpressAddress() { - return `${window.location.href.split("?")[0].replace("vite/", "").replace("vite", "")}express` + return `${window.location.href.split("?")[0].replace("vite/", "").replace("vite", "")}express`; } getBackendAddress() { - return `${window.location.href.split("?")[0].replace("vite/", "").replace("vite", "")}olympus` + return `${window.location.href.split("?")[0].replace("vite/", "").replace("vite", "")}olympus`; } start() { @@ -150,6 +150,17 @@ export class OlympusApp { ConfigLoadedEvent.dispatch(this.#config as OlympusConfig); this.setState(OlympusState.LOGIN); }); + + this.#shortcutManager?.addShortcut("idle", { + label: "Deselect all", + keyUpCallback: (ev: KeyboardEvent) => { + this.setState(OlympusState.IDLE); + }, + code: "Escape", + }); + + this.#shortcutManager.checkShortcuts(); + } getConfig() { diff --git a/frontend/react/src/other/utils.ts b/frontend/react/src/other/utils.ts index a7c59275..573acef3 100644 --- a/frontend/react/src/other/utils.ts +++ b/frontend/react/src/other/utils.ts @@ -87,29 +87,34 @@ export const zeroPad = function (num: number, places: number) { return string; }; -export function latLngToMGRS(lat: number, lng: number, precision: number = 4): MGRS | false { +export function latLngToMGRS(lat: number, lng: number, precision: number = 4): MGRS | undefined { if (precision < 0 || precision > 6) { console.error("latLngToMGRS: precision must be a number >= 0 and <= 6. Given precision: " + precision); - return false; + return undefined; } + const mgrs = new Converter({}).LLtoMGRS(lat, lng, precision); const match = mgrs.match(new RegExp(`^(\\d{2})([A-Z])([A-Z])([A-Z])(\\d+)$`)); - const easting = match[5].substr(0, match[5].length / 2); - const northing = match[5].substr(match[5].length / 2); + if (match) { + const easting = match[5].substr(0, match[5].length / 2); + const northing = match[5].substr(match[5].length / 2); - let output: MGRS = { - bandLetter: match[2], - columnLetter: match[3], - groups: [match[1] + match[2], match[3] + match[4], easting, northing], - easting: easting, - northing: northing, - precision: precision, - rowLetter: match[4], - string: match[0], - zoneNumber: match[1], - }; + let output: MGRS = { + bandLetter: match[2], + columnLetter: match[3], + groups: [match[1] + match[2], match[3] + match[4], easting, northing], + easting: easting, + northing: northing, + precision: precision, + rowLetter: match[4], + string: match[0], + zoneNumber: match[1], + }; - return output; + return output; + } else { + return undefined; + } } export function latLngToUTM(lat: number, lng: number) { @@ -317,19 +322,20 @@ export function makeID(length) { } export function hash(str, seed = 0) { - let h1 = 0xdeadbeef ^ seed, h2 = 0x41c6ce57 ^ seed; - for(let i = 0, ch; i < str.length; i++) { - ch = str.charCodeAt(i); - h1 = Math.imul(h1 ^ ch, 2654435761); - h2 = Math.imul(h2 ^ ch, 1597334677); + let h1 = 0xdeadbeef ^ seed, + h2 = 0x41c6ce57 ^ seed; + for (let i = 0, ch; i < str.length; i++) { + ch = str.charCodeAt(i); + h1 = Math.imul(h1 ^ ch, 2654435761); + h2 = Math.imul(h2 ^ ch, 1597334677); } - h1 = Math.imul(h1 ^ (h1 >>> 16), 2246822507); + h1 = Math.imul(h1 ^ (h1 >>> 16), 2246822507); h1 ^= Math.imul(h2 ^ (h2 >>> 13), 3266489909); - h2 = Math.imul(h2 ^ (h2 >>> 16), 2246822507); + h2 = Math.imul(h2 ^ (h2 >>> 16), 2246822507); h2 ^= Math.imul(h1 ^ (h1 >>> 13), 3266489909); return `${4294967296 * (2097151 & h2) + (h1 >>> 0)}`; -}; +} export function byteArrayToInteger(array) { let res = 0; diff --git a/frontend/react/src/server/servermanager.ts b/frontend/react/src/server/servermanager.ts index c7fd83bd..77bfc40e 100644 --- a/frontend/react/src/server/servermanager.ts +++ b/frontend/react/src/server/servermanager.ts @@ -42,7 +42,7 @@ export class ServerManager { keyUpCallback: () => { this.setPaused(!this.getPaused()); }, - code: "Space" + code: "Enter" }) } diff --git a/frontend/react/src/shortcut/shortcut.ts b/frontend/react/src/shortcut/shortcut.ts index 23701958..0da40a38 100644 --- a/frontend/react/src/shortcut/shortcut.ts +++ b/frontend/react/src/shortcut/shortcut.ts @@ -1,22 +1,26 @@ -import { ShortcutChangedEvent, ShortcutsChangedEvent } from "../events"; +import { AppStateChangedEvent, ShortcutChangedEvent, ShortcutsChangedEvent } from "../events"; import { ShortcutOptions } from "../interfaces"; import { keyEventWasInInput } from "../other/utils"; export class Shortcut { #id: string; #options: ShortcutOptions; + #keydown: boolean = false; constructor(id, options: ShortcutOptions) { this.#id = id; this.#options = options; + AppStateChangedEvent.on(() => this.#keydown = false) + /* Key up event is mandatory */ document.addEventListener("keyup", (ev: any) => { + this.#keydown = false; if (keyEventWasInInput(ev) || options.code !== ev.code) return; if ( - (typeof options.altKey !== "boolean" || (typeof options.altKey === "boolean" && ev.altKey === options.altKey)) && - (typeof options.ctrlKey !== "boolean" || (typeof options.ctrlKey === "boolean" && ev.ctrlKey === options.ctrlKey)) && - (typeof options.shiftKey !== "boolean" || (typeof options.shiftKey === "boolean" && ev.shiftKey === options.shiftKey)) + ev.altKey === (options.altKey ?? ev.code.indexOf("Alt") >= 0) && + ev.ctrlKey === (options.ctrlKey ?? ev.code.indexOf("Ctrl") >= 0) && + ev.shiftKey === (options.shiftKey ?? ev.code.indexOf("Shift") >= 0) ) options.keyUpCallback(ev); }); @@ -24,11 +28,12 @@ export class Shortcut { /* Key down event is optional */ if (options.keyDownCallback) { document.addEventListener("keydown", (ev: any) => { - if (keyEventWasInInput(ev) || options.code !== ev.code) return; + if (this.#keydown || keyEventWasInInput(ev) || options.code !== ev.code) return; + this.#keydown = true; if ( - (typeof options.altKey !== "boolean" || (typeof options.altKey === "boolean" && ev.altKey === options.altKey)) && - (typeof options.ctrlKey !== "boolean" || (typeof options.ctrlKey === "boolean" && ev.ctrlKey === options.ctrlKey)) && - (typeof options.shiftKey !== "boolean" || (typeof options.shiftKey === "boolean" && ev.shiftKey === options.shiftKey)) + ev.altKey === (options.altKey ?? ev.code.indexOf("Alt") >= 0) && + ev.ctrlKey === (options.ctrlKey ?? ev.code.indexOf("Control") >= 0) && + ev.shiftKey === (options.shiftKey ?? ev.code.indexOf("Shift") >= 0) ) if (options.keyDownCallback) options.keyDownCallback(ev); }); diff --git a/frontend/react/src/shortcut/shortcutmanager.ts b/frontend/react/src/shortcut/shortcutmanager.ts index 74659ea4..05807800 100644 --- a/frontend/react/src/shortcut/shortcutmanager.ts +++ b/frontend/react/src/shortcut/shortcutmanager.ts @@ -39,4 +39,24 @@ export class ShortcutManager { this.#shortcuts[id].setOptions(shortcutOptions); ShortcutsChangedEvent.dispatch(this.#shortcuts); } + + checkShortcuts() { + for (let id in this.#shortcuts) { + const shortcut = this.#shortcuts[id]; + for (let otherid in this.#shortcuts) { + if (id != otherid) { + const otherShortcut = this.#shortcuts[otherid]; + if (shortcut.getOptions().code === otherShortcut.getOptions().code) { + if ( + (shortcut.getOptions().altKey ?? false) === (otherShortcut.getOptions().altKey ?? false) && + (shortcut.getOptions().ctrlKey ?? false) === (otherShortcut.getOptions().ctrlKey ?? false) && + (shortcut.getOptions().shiftKey ?? false) === (otherShortcut.getOptions().shiftKey ?? false) + ) { + console.error("Duplicate shortcut: " + shortcut.getOptions().label + " and " + otherShortcut.getOptions().label) + } + } + } + } + } + } } diff --git a/frontend/react/src/types/types.ts b/frontend/react/src/types/types.ts index 313aedaf..cd55151d 100644 --- a/frontend/react/src/types/types.ts +++ b/frontend/react/src/types/types.ts @@ -26,6 +26,7 @@ export type MapOptions = { cameraPluginRatio: number; cameraPluginEnabled: boolean; cameraPluginMode: string; + tabletMode: boolean; }; export type MapHiddenTypes = { diff --git a/frontend/react/src/ui/components/olcoalitiontoggle.tsx b/frontend/react/src/ui/components/olcoalitiontoggle.tsx index 03bb13b7..5ff0b4c8 100644 --- a/frontend/react/src/ui/components/olcoalitiontoggle.tsx +++ b/frontend/react/src/ui/components/olcoalitiontoggle.tsx @@ -1,7 +1,7 @@ import React from "react"; import { Coalition } from "../../types/types"; -export function OlCoalitionToggle(props: { coalition: Coalition | undefined; onClick: () => void }) { +export function OlCoalitionToggle(props: { coalition: Coalition | undefined; onClick: () => void; showLabel?: boolean }) { return (
- - {props.coalition ? `${props.coalition[0].toLocaleUpperCase() + props.coalition.substring(1)}` : "Diff. values"} - + {props.showLabel && ( + + {props.coalition ? `${props.coalition[0].toLocaleUpperCase() + props.coalition.substring(1)}` : "Diff. values"} + + )} ); } diff --git a/frontend/react/src/ui/contextmenus/mapcontextmenu.tsx b/frontend/react/src/ui/contextmenus/mapcontextmenu.tsx index 407b8b37..dbeb4914 100644 --- a/frontend/react/src/ui/contextmenus/mapcontextmenu.tsx +++ b/frontend/react/src/ui/contextmenus/mapcontextmenu.tsx @@ -113,7 +113,6 @@ export function MapContextMenu(props: {}) { contextActionIt.executeCallback(unit, null); } } - getApp().setState(OlympusState.UNIT_CONTROL); }} > diff --git a/frontend/react/src/ui/contextmenus/spawncontextmenu.tsx b/frontend/react/src/ui/contextmenus/spawncontextmenu.tsx new file mode 100644 index 00000000..d73e3464 --- /dev/null +++ b/frontend/react/src/ui/contextmenus/spawncontextmenu.tsx @@ -0,0 +1,606 @@ +import React, { useEffect, useRef, useState } from "react"; +import { BLUE_COMMANDER, COMMAND_MODE_OPTIONS_DEFAULTS, GAME_MASTER, NO_SUBSTATE, OlympusState, OlympusSubState } from "../../constants/constants"; +import { LatLng } from "leaflet"; +import { + AppStateChangedEvent, + CommandModeOptionsChangedEvent, + SpawnContextMenuRequestEvent, + StarredSpawnsChangedEvent, + UnitDatabaseLoadedEvent, +} from "../../events"; +import { getApp } from "../../olympusapp"; +import { SpawnRequestTable, UnitBlueprint } from "../../interfaces"; +import { faArrowLeft, faEllipsisVertical, faExplosion, faListDots, faSearch, faSmog, faStar } from "@fortawesome/free-solid-svg-icons"; +import { EffectSpawnMenu } from "../panels/effectspawnmenu"; +import { UnitSpawnMenu } from "../panels/unitspawnmenu"; +import { OlEffectListEntry } from "../components/oleffectlistentry"; +import { + olButtonsVisibilityAircraft, + olButtonsVisibilityGroundunit, + olButtonsVisibilityGroundunitSam, + olButtonsVisibilityHelicopter, + olButtonsVisibilityNavyunit, +} from "../components/olicons"; +import { OlUnitListEntry } from "../components/olunitlistentry"; +import { OlSearchBar } from "../components/olsearchbar"; +import { OlStateButton } from "../components/olstatebutton"; +import { OlDropdownItem } from "../components/oldropdown"; +import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; +import { OlCoalitionToggle } from "../components/olcoalitiontoggle"; +import { Coalition } from "../../types/types"; +import { CompactUnitSpawnMenu } from "../panels/compactunitspawnmenu"; +import { CompactEffectSpawnMenu } from "../panels/compacteffectspawnmenu"; + +enum CategoryGroup { + NONE, + AIRCRAFT, + HELICOPTER, + AIR_DEFENCE, + GROUND_UNIT, + NAVY_UNIT, + EFFECT, + SEARCH, + STARRED, +} + +export function SpawnContextMenu(props: {}) { + const [appState, setAppState] = useState(OlympusState.NOT_INITIALIZED); + const [appSubState, setAppSubState] = useState(NO_SUBSTATE as OlympusSubState); + const [xPosition, setXPosition] = useState(0); + const [yPosition, setYPosition] = useState(0); + const [latlng, setLatLng] = useState(null as null | LatLng); + const [starredSpawns, setStarredSpawns] = useState({} as { [key: string]: SpawnRequestTable }); + const [openAccordion, setOpenAccordion] = useState(CategoryGroup.NONE); + const [blueprint, setBlueprint] = useState(null as null | UnitBlueprint); + const [effect, setEffect] = useState(null as null | string); + const [filterString, setFilterString] = useState(""); + const [selectedRole, setSelectedRole] = useState(null as null | string); + const [selectedType, setSelectedType] = useState(null as null | string); + const [blueprints, setBlueprints] = useState([] as UnitBlueprint[]); + const [roles, setRoles] = useState({ aircraft: [] as string[], helicopter: [] as string[] }); + const [types, setTypes] = useState({ groundunit: [] as string[], navyunit: [] as string[] }); + const [commandModeOptions, setCommandModeOptions] = useState(COMMAND_MODE_OPTIONS_DEFAULTS); + const [showCost, setShowCost] = useState(false); + const [spawnCoalition, setSpawnCoalition] = useState("blue" as Coalition); + const [showMore, setShowMore] = useState(false); + + useEffect(() => { + if (selectedRole) setBlueprints(getApp()?.getUnitsManager().getDatabase().getByRole(selectedRole)); + else if (selectedType) setBlueprints(getApp()?.getUnitsManager().getDatabase().getByType(selectedType)); + else setBlueprints(getApp()?.getUnitsManager().getDatabase().getBlueprints()); + }, [selectedRole, selectedType, openAccordion]); + + useEffect(() => { + UnitDatabaseLoadedEvent.on(() => { + setRoles({ + aircraft: getApp() + ?.getUnitsManager() + .getDatabase() + .getRoles((unit) => unit.category === "aircraft"), + helicopter: getApp() + ?.getUnitsManager() + .getDatabase() + .getRoles((unit) => unit.category === "helicopter"), + }); + + setTypes({ + groundunit: getApp() + ?.getUnitsManager() + .getDatabase() + .getTypes((unit) => unit.category === "groundunit"), + navyunit: getApp() + ?.getUnitsManager() + .getDatabase() + .getTypes((unit) => unit.category === "navyunit"), + }); + }); + + CommandModeOptionsChangedEvent.on((commandModeOptions) => { + setCommandModeOptions(commandModeOptions); + setShowCost(!(commandModeOptions.commandMode == GAME_MASTER || !commandModeOptions.restrictSpawns)); + setOpenAccordion(CategoryGroup.NONE); + }); + + StarredSpawnsChangedEvent.on((starredSpawns) => setStarredSpawns({ ...starredSpawns })); + }, []); + + useEffect(() => { + setBlueprint(null); + setEffect(null); + setSelectedType(null); + setSelectedRole(null); + }, [openAccordion]); + + /* Filter the blueprints according to the label */ + const filteredBlueprints: UnitBlueprint[] = []; + if (blueprints && filterString !== "") { + blueprints.forEach((blueprint) => { + if (blueprint.enabled && (filterString === "" || blueprint.label.toLowerCase().includes(filterString.toLowerCase()))) filteredBlueprints.push(blueprint); + }); + } + + var contentRef = useRef(null); + + useEffect(() => { + AppStateChangedEvent.on((state, subState) => { + setAppState(state); + setAppSubState(subState); + }); + StarredSpawnsChangedEvent.on((starredSpawns) => setStarredSpawns({ ...starredSpawns })); + SpawnContextMenuRequestEvent.on((latlng) => { + setLatLng(latlng); + const containerPoint = getApp().getMap().latLngToContainerPoint(latlng); + setXPosition(getApp().getMap().getContainer().offsetLeft + containerPoint.x); + setYPosition(getApp().getMap().getContainer().offsetTop + containerPoint.y); + }); + }, []); + + useEffect(() => { + if (contentRef.current) { + const content = contentRef.current as HTMLDivElement; + + content.style.left = `${xPosition}px`; + content.style.top = `${yPosition}px`; + + let newXPosition = xPosition; + let newYposition = yPosition; + + let [cxr, cyb] = [content.getBoundingClientRect().x + content.clientWidth, content.getBoundingClientRect().y + content.clientHeight]; + + /* Try and move the content so it is inside the screen */ + if (cxr > window.innerWidth) newXPosition -= cxr - window.innerWidth; + if (cyb > window.innerHeight) newYposition -= cyb - window.innerHeight; + + content.style.left = `${newXPosition}px`; + content.style.top = `${newYposition}px`; + } + }); + + return ( + <> + {appState === OlympusState.SPAWN_CONTEXT && ( + <> +
+
+
+ { + spawnCoalition === "blue" && setSpawnCoalition("neutral"); + spawnCoalition === "neutral" && setSpawnCoalition("red"); + spawnCoalition === "red" && setSpawnCoalition("blue"); + }} + /> + (openAccordion !== CategoryGroup.AIRCRAFT ? setOpenAccordion(CategoryGroup.AIRCRAFT) : setOpenAccordion(CategoryGroup.NONE))} + icon={olButtonsVisibilityAircraft} + tooltip="Show aircraft units" + buttonColor={spawnCoalition === "blue" ? "#2563eb" : spawnCoalition === "neutral" ? "#9ca3af" : "#ef4444"} + /> + + openAccordion !== CategoryGroup.HELICOPTER ? setOpenAccordion(CategoryGroup.HELICOPTER) : setOpenAccordion(CategoryGroup.NONE) + } + icon={olButtonsVisibilityHelicopter} + tooltip="Show helicopter units" + buttonColor={spawnCoalition === "blue" ? "#2563eb" : spawnCoalition === "neutral" ? "#9ca3af" : "#ef4444"} + /> + + openAccordion !== CategoryGroup.AIR_DEFENCE ? setOpenAccordion(CategoryGroup.AIR_DEFENCE) : setOpenAccordion(CategoryGroup.NONE) + } + icon={olButtonsVisibilityGroundunitSam} + tooltip="Show air defence units" + buttonColor={spawnCoalition === "blue" ? "#2563eb" : spawnCoalition === "neutral" ? "#9ca3af" : "#ef4444"} + /> + + openAccordion !== CategoryGroup.GROUND_UNIT ? setOpenAccordion(CategoryGroup.GROUND_UNIT) : setOpenAccordion(CategoryGroup.NONE) + } + icon={olButtonsVisibilityGroundunit} + tooltip="Show ground units" + buttonColor={spawnCoalition === "blue" ? "#2563eb" : spawnCoalition === "neutral" ? "#9ca3af" : "#ef4444"} + /> + (openAccordion !== CategoryGroup.NAVY_UNIT ? setOpenAccordion(CategoryGroup.NAVY_UNIT) : setOpenAccordion(CategoryGroup.NONE))} + icon={olButtonsVisibilityNavyunit} + tooltip="Show navy units" + buttonColor={spawnCoalition === "blue" ? "#2563eb" : spawnCoalition === "neutral" ? "#9ca3af" : "#ef4444"} + /> + setShowMore(!showMore)} icon={faEllipsisVertical} tooltip="Show more options" /> + {showMore && ( + <> + (openAccordion !== CategoryGroup.EFFECT ? setOpenAccordion(CategoryGroup.EFFECT) : setOpenAccordion(CategoryGroup.NONE))} + icon={faExplosion} + tooltip="Show effects" + className="ml-auto" + /> + (openAccordion !== CategoryGroup.SEARCH ? setOpenAccordion(CategoryGroup.SEARCH) : setOpenAccordion(CategoryGroup.NONE))} + icon={faSearch} + tooltip="Search unit" + /> + (openAccordion !== CategoryGroup.STARRED ? setOpenAccordion(CategoryGroup.STARRED) : setOpenAccordion(CategoryGroup.NONE))} + icon={faStar} + tooltip="Show starred spanws" + /> + + )} +
+ {blueprint === null && effect === null && openAccordion !== CategoryGroup.NONE && ( +
+ <> + <> + {openAccordion === CategoryGroup.AIRCRAFT && ( + <> +
+ {roles.aircraft.sort().map((role) => { + return ( +
{ + selectedRole === role ? setSelectedRole(null) : setSelectedRole(role); + }} + > + {role} +
+ ); + })} +
+
+ {blueprints + ?.sort((a, b) => (a.label > b.label ? 1 : -1)) + .filter((blueprint) => blueprint.category === "aircraft") + .map((blueprint) => { + return ( + setBlueprint(blueprint)} + showCost={showCost} + cost={getApp().getUnitsManager().getDatabase().getSpawnPointsByName(blueprint.name)} + /> + ); + })} +
+ + )} + {openAccordion === CategoryGroup.HELICOPTER && ( + <> +
+ {roles.helicopter.sort().map((role) => { + return ( +
{ + selectedRole === role ? setSelectedRole(null) : setSelectedRole(role); + }} + > + {role} +
+ ); + })} +
+
+ {blueprints + ?.sort((a, b) => (a.label > b.label ? 1 : -1)) + .filter((blueprint) => blueprint.category === "helicopter") + .map((blueprint) => { + return ( + setBlueprint(blueprint)} + showCost={showCost} + cost={getApp().getUnitsManager().getDatabase().getSpawnPointsByName(blueprint.name)} + /> + ); + })} +
+ + )} + {openAccordion === CategoryGroup.AIR_DEFENCE && ( + <> +
+ {types.groundunit + .sort() + ?.filter((type) => type === "SAM Site" || type === "AAA") + .map((type) => { + return ( +
{ + selectedType === type ? setSelectedType(null) : setSelectedType(type); + }} + > + {type} +
+ ); + })} +
+
+ {blueprints + ?.sort((a, b) => (a.label > b.label ? 1 : -1)) + .filter((blueprint) => blueprint.category === "groundunit" && (blueprint.type === "SAM Site" || blueprint.type === "AAA")) + .map((blueprint) => { + return ( + setBlueprint(blueprint)} + showCost={showCost} + cost={getApp().getUnitsManager().getDatabase().getSpawnPointsByName(blueprint.name)} + /> + ); + })} +
+ + )} + {openAccordion === CategoryGroup.GROUND_UNIT && ( + <> +
+ {types.groundunit + .sort() + ?.filter((type) => type !== "SAM Site" && type !== "AAA") + .map((type) => { + return ( +
{ + selectedType === type ? setSelectedType(null) : setSelectedType(type); + }} + > + {type} +
+ ); + })} +
+
+ {blueprints + ?.sort((a, b) => (a.label > b.label ? 1 : -1)) + .filter((blueprint) => blueprint.category === "groundunit" && blueprint.type !== "SAM Site" && blueprint.type !== "AAA") + .map((blueprint) => { + return ( + setBlueprint(blueprint)} + showCost={showCost} + cost={getApp().getUnitsManager().getDatabase().getSpawnPointsByName(blueprint.name)} + /> + ); + })} +
+ + )} + {openAccordion === CategoryGroup.NAVY_UNIT && ( + <> +
+ {types.navyunit.sort().map((type) => { + return ( +
{ + selectedType === type ? setSelectedType(null) : setSelectedType(type); + }} + > + {type} +
+ ); + })} +
+
+ {blueprints + ?.sort((a, b) => (a.label > b.label ? 1 : -1)) + .filter((blueprint) => blueprint.category === "navyunit") + .map((blueprint) => { + return ( + setBlueprint(blueprint)} + showCost={showCost} + cost={getApp().getUnitsManager().getDatabase().getSpawnPointsByName(blueprint.name)} + /> + ); + })} +
+ + )} + {openAccordion === CategoryGroup.EFFECT && ( + <> +
+ { + setEffect("explosion"); + }} + /> + { + setEffect("smoke"); + }} + /> +
+ + )} + {openAccordion === CategoryGroup.SEARCH && ( +
+ setFilterString(value)} text={filterString} /> +
+ {filteredBlueprints.length > 0 ? ( + filteredBlueprints.map((blueprint) => { + return ( + setBlueprint(blueprint)} + showCost={showCost} + cost={getApp().getUnitsManager().getDatabase().getSpawnPointsByName(blueprint.name)} + /> + ); + }) + ) : filterString === "" ? ( + Type to search + ) : ( + No results + )} +
+
+ )} + {openAccordion === CategoryGroup.STARRED && ( +
+ {Object.values(starredSpawns).length > 0 ? ( + Object.values(starredSpawns).map((spawnRequestTable) => { + return ( + { + if (latlng) { + spawnRequestTable.unit.location = latlng; + getApp() + .getUnitsManager() + .spawnUnits(spawnRequestTable.category, Array(spawnRequestTable.amount).fill(spawnRequestTable.unit), spawnRequestTable.coalition, false); + getApp().setState(OlympusState.IDLE); + } + }} + > + +
+ {getApp().getUnitsManager().getDatabase().getByName(spawnRequestTable.unit.unitType)?.label} ( + {spawnRequestTable.quickAccessName}) +
+
+ ); + }) + ) : ( +
No starred spawns, use the spawn menu to create a quick access spawn
+ )} +
+ )} + + +
+ )} + {!(blueprint === null) && setBlueprint(null)}/>} + {!(effect === null) && latlng && setEffect(null)} />} +
+
+ + )} + + ); +} diff --git a/frontend/react/src/ui/contextmenus/starredspawncontextmenu.tsx b/frontend/react/src/ui/contextmenus/starredspawncontextmenu.tsx deleted file mode 100644 index 939f0c32..00000000 --- a/frontend/react/src/ui/contextmenus/starredspawncontextmenu.tsx +++ /dev/null @@ -1,108 +0,0 @@ -import React, { useEffect, useRef, useState } from "react"; -import { NO_SUBSTATE, OlympusState, OlympusSubState } from "../../constants/constants"; -import { OlDropdownItem } from "../components/oldropdown"; -import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; -import { LatLng } from "leaflet"; -import { AppStateChangedEvent, StarredSpawnContextMenuRequestEvent, StarredSpawnsChangedEvent } from "../../events"; -import { getApp } from "../../olympusapp"; -import { SpawnRequestTable } from "../../interfaces"; -import { faStar } from "@fortawesome/free-solid-svg-icons"; - -export function StarredSpawnContextMenu(props: {}) { - const [appState, setAppState] = useState(OlympusState.NOT_INITIALIZED); - const [appSubState, setAppSubState] = useState(NO_SUBSTATE as OlympusSubState); - const [xPosition, setXPosition] = useState(0); - const [yPosition, setYPosition] = useState(0); - const [latlng, setLatLng] = useState(null as null | LatLng); - const [starredSpawns, setStarredSpawns] = useState({} as { [key: string]: SpawnRequestTable }); - - var contentRef = useRef(null); - - useEffect(() => { - AppStateChangedEvent.on((state, subState) => { - setAppState(state); - setAppSubState(subState); - }); - StarredSpawnsChangedEvent.on((starredSpawns) => setStarredSpawns({ ...starredSpawns })); - StarredSpawnContextMenuRequestEvent.on((latlng) => { - setLatLng(latlng); - const containerPoint = getApp().getMap().latLngToContainerPoint(latlng); - setXPosition(getApp().getMap().getContainer().offsetLeft + containerPoint.x); - setYPosition(getApp().getMap().getContainer().offsetTop + containerPoint.y); - }); - }, []); - - useEffect(() => { - if (contentRef.current) { - const content = contentRef.current as HTMLDivElement; - - content.style.left = `${xPosition}px`; - content.style.top = `${yPosition}px`; - - let newXPosition = xPosition; - let newYposition = yPosition; - - let [cxr, cyb] = [content.getBoundingClientRect().x + content.clientWidth, content.getBoundingClientRect().y + content.clientHeight]; - - /* Try and move the content so it is inside the screen */ - if (cxr > window.innerWidth) newXPosition -= cxr - window.innerWidth; - if (cyb > window.innerHeight) newYposition -= cyb - window.innerHeight; - - content.style.left = `${newXPosition}px`; - content.style.top = `${newYposition}px`; - } - }); - - return ( - <> - {appState === OlympusState.STARRED_SPAWN && ( - <> -
-
- {Object.values(starredSpawns).length > 0? Object.values(starredSpawns).map((spawnRequestTable) => { - return ( - { - if (latlng) { - spawnRequestTable.unit.location = latlng; - getApp().getUnitsManager().spawnUnits(spawnRequestTable.category, [spawnRequestTable.unit], spawnRequestTable.coalition, false); - getApp().setState(OlympusState.IDLE) - } - }} - > - -
- {getApp().getUnitsManager().getDatabase().getByName(spawnRequestTable.unit.unitType)?.label} ({spawnRequestTable.quickAccessName}) -
-
- ); - }): -
No starred spawns, use the spawn menu to create a quick access spawn
} -
-
- - )} - - ); -} diff --git a/frontend/react/src/ui/panels/airbasemenu.tsx b/frontend/react/src/ui/panels/airbasemenu.tsx index 28432bde..70d93de3 100644 --- a/frontend/react/src/ui/panels/airbasemenu.tsx +++ b/frontend/react/src/ui/panels/airbasemenu.tsx @@ -272,7 +272,6 @@ export function AirbaseMenu(props: { open: boolean; onClose: () => void; childre diff --git a/frontend/react/src/ui/panels/compacteffectspawnmenu.tsx b/frontend/react/src/ui/panels/compacteffectspawnmenu.tsx new file mode 100644 index 00000000..b8387ca5 --- /dev/null +++ b/frontend/react/src/ui/panels/compacteffectspawnmenu.tsx @@ -0,0 +1,101 @@ +import React, { useEffect, useState } from "react"; +import { OlDropdown, OlDropdownItem } from "../components/oldropdown"; +import { getApp } from "../../olympusapp"; +import { OlympusState, SpawnSubState } from "../../constants/constants"; +import { OlStateButton } from "../components/olstatebutton"; +import { faArrowLeft, faSmog } from "@fortawesome/free-solid-svg-icons"; +import { LatLng } from "leaflet"; +import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; + +export function CompactEffectSpawnMenu(props: { effect: string; latlng: LatLng; onBack: () => void }) { + const [explosionType, setExplosionType] = useState("High explosive"); + const [smokeColor, setSmokeColor] = useState("white"); + + return ( +
+ {props.effect === "explosion" && ( + <> +
+ + Explosion type +
+ + {["High explosive", "Napalm", "White phosphorous"].map((optionExplosionType) => { + return ( + { + setExplosionType(optionExplosionType); + }} + > + {optionExplosionType} + + ); + })} + + + )} + {props.effect === "smoke" && ( + <> +
+ + Smoke color +
+
+ {["white", "blue", "red", "green", "orange"].map((optionSmokeColor) => { + return ( + { + setSmokeColor(optionSmokeColor); + }} + tooltip="" + buttonColor={optionSmokeColor} + /> + ); + })} +
+ + )} + +
+ ); +} diff --git a/frontend/react/src/ui/panels/compactunitspawnmenu.tsx b/frontend/react/src/ui/panels/compactunitspawnmenu.tsx new file mode 100644 index 00000000..522c1039 --- /dev/null +++ b/frontend/react/src/ui/panels/compactunitspawnmenu.tsx @@ -0,0 +1,445 @@ +import React, { useState, useEffect, useCallback } from "react"; +import { OlUnitSummary } from "../components/olunitsummary"; +import { OlCoalitionToggle } from "../components/olcoalitiontoggle"; +import { OlNumberInput } from "../components/olnumberinput"; +import { OlLabelToggle } from "../components/ollabeltoggle"; +import { OlRangeSlider } from "../components/olrangeslider"; +import { OlDropdownItem, OlDropdown } from "../components/oldropdown"; +import { LoadoutBlueprint, SpawnRequestTable, UnitBlueprint } from "../../interfaces"; +import { OlStateButton } from "../components/olstatebutton"; +import { Coalition } from "../../types/types"; +import { getApp } from "../../olympusapp"; +import { ftToM, hash } from "../../other/utils"; +import { LatLng } from "leaflet"; +import { Airbase } from "../../mission/airbase"; +import { altitudeIncrements, groupUnitCount, maxAltitudeValues, minAltitudeValues, OlympusState, SpawnSubState } from "../../constants/constants"; +import { faArrowLeft, faStar } from "@fortawesome/free-solid-svg-icons"; +import { OlStringInput } from "../components/olstringinput"; +import { countryCodes } from "../data/codes"; +import { OlAccordion } from "../components/olaccordion"; +import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; + +export function CompactUnitSpawnMenu(props: { + starredSpawns: { [key: string]: SpawnRequestTable }; + blueprint: UnitBlueprint; + onBack: () => void; + latlng?: LatLng | null; + airbase?: Airbase | null; + coalition?: Coalition; +}) { + /* Compute the min and max values depending on the unit type */ + const minNumber = 1; + const maxNumber = groupUnitCount[props.blueprint.category]; + const minAltitude = minAltitudeValues[props.blueprint.category]; + const maxAltitude = maxAltitudeValues[props.blueprint.category]; + const altitudeStep = altitudeIncrements[props.blueprint.category]; + + /* State initialization */ + const [spawnCoalition, setSpawnCoalition] = useState("blue" as Coalition); + const [spawnNumber, setSpawnNumber] = useState(1); + const [spawnRole, setSpawnRole] = useState(""); + const [spawnLoadoutName, setSpawnLoadout] = useState(""); + const [spawnAltitude, setSpawnAltitude] = useState((maxAltitude - minAltitude) / 2); + const [spawnAltitudeType, setSpawnAltitudeType] = useState(false); + const [spawnLiveryID, setSpawnLiveryID] = useState(""); + const [spawnSkill, setSpawnSkill] = useState("High"); + + const [showAdvancedOptions, setShowAdvancedOptions] = useState(false); + const [showLoadout, setShowLoadout] = useState(false); + const [showUnitSummary, setShowUnitSummary] = useState(false); + + const [quickAccessName, setQuickAccessName] = useState("No name"); + 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 && !props.latlng && spawnRequestTable) { + /* Refresh the unique key identified */ + const newKey = hash(JSON.stringify(spawnRequestTable)); + setKey(newKey); + + getApp()?.getMap()?.setSpawnRequestTable(spawnRequestTable); + getApp().setState(OlympusState.SPAWN, SpawnSubState.SPAWN_UNIT); + } + }, [spawnRequestTable]); + + /* Callback and effect to update the quick access name of the starredSpawn */ + const updateStarredSpawnQuickAccessNameS = useCallback(() => { + if (key in props.starredSpawns) props.starredSpawns[key].quickAccessName = quickAccessName; + }, [props.starredSpawns, key, quickAccessName]); + useEffect(updateStarredSpawnQuickAccessNameS, [quickAccessName]); + + /* Callback and effect to update the quick access name in the input field */ + const updateQuickAccessName = useCallback(() => { + /* If the spawn is starred, set the quick access name */ + if (key in props.starredSpawns && props.starredSpawns[key].quickAccessName) setQuickAccessName(props.starredSpawns[key].quickAccessName); + else setQuickAccessName("No name"); + }, [props.starredSpawns, key]); + useEffect(updateQuickAccessName, [key]); + + /* Callback and effect to update the spawn request table */ + const updateSpawnRequestTable = useCallback(() => { + if (props.blueprint !== null) { + setSpawnRequestTable({ + category: props.blueprint.category, + unit: { + unitType: props.blueprint.name, + location: props.latlng ?? new LatLng(0, 0), // This will be filled when the user clicks on the map to spawn the unit + skill: spawnSkill, + liveryID: spawnLiveryID, + altitude: ftToM(spawnAltitude), + loadout: props.blueprint.loadouts?.find((loadout) => loadout.name === spawnLoadoutName)?.code ?? "", + }, + amount: spawnNumber, + coalition: spawnCoalition, + }); + } + }, [props.blueprint, spawnAltitude, spawnLoadoutName, spawnCoalition, spawnNumber, spawnLiveryID, spawnSkill]); + useEffect(updateSpawnRequestTable, [props.blueprint, spawnAltitude, spawnLoadoutName, spawnCoalition, spawnNumber, spawnLiveryID, spawnSkill]); + + /* Effect to update the coalition if it is force externally */ + useEffect(() => { + if (props.coalition) setSpawnCoalition(props.coalition); + }, [props.coalition]); + + /* Get a list of all the roles */ + const roles: string[] = []; + (props.blueprint as UnitBlueprint).loadouts?.forEach((loadout) => { + loadout.roles.forEach((role) => { + !roles.includes(role) && roles.push(role); + }); + }); + + /* Initialize the role */ + spawnRole === "" && roles.length > 0 && setSpawnRole(roles[0]); + + /* Get a list of all the loadouts */ + const loadouts: LoadoutBlueprint[] = []; + (props.blueprint as UnitBlueprint).loadouts?.forEach((loadout) => { + loadout.roles.includes(spawnRole) && loadouts.push(loadout); + }); + + /* Initialize the loadout */ + spawnLoadoutName === "" && loadouts.length > 0 && setSpawnLoadout(loadouts[0].name); + const spawnLoadout = props.blueprint.loadouts?.find((loadout) => { + return loadout.name === spawnLoadoutName; + }); + + return ( +
+
+
+ +
{props.blueprint.label}
+ { + setSpawnNumber(Math.max(minNumber, spawnNumber - 1)); + }} + onIncrease={() => { + setSpawnNumber(Math.min(maxNumber, spawnNumber + 1)); + }} + onChange={(ev) => { + !isNaN(Number(ev.target.value)) && setSpawnNumber(Math.max(minNumber, Math.min(maxNumber, Number(ev.target.value)))); + }} + /> +
+
+
Quick access:
+ { + setQuickAccessName(e.target.value); + }} + value={quickAccessName} + /> + { + if (spawnRequestTable) + key in props.starredSpawns + ? getApp().getMap().removeStarredSpawnRequestTable(key) + : getApp().getMap().addStarredSpawnRequestTable(key, spawnRequestTable); + }} + tooltip="Save this spawn for quick access" + checked={key in props.starredSpawns} + icon={faStar} + > +
+ {["aircraft", "helicopter"].includes(props.blueprint.category) && ( + <> + {!props.airbase && ( +
+
+
+ + Altitude + + {`${Intl.NumberFormat("en-US").format(spawnAltitude)} FT`} +
+ setSpawnAltitudeType(!spawnAltitudeType)} /> +
+ setSpawnAltitude(Number(ev.target.value))} + value={spawnAltitude} + min={minAltitude} + max={maxAltitude} + step={altitudeStep} + /> +
+ )} +
+ + Role + + + {roles.map((role) => { + return ( + { + setSpawnRole(role); + setSpawnLoadout(""); + }} + className={`w-full`} + > + {role} + + ); + })} + +
+
+ + Weapons + + + {loadouts.map((loadout) => { + return ( + { + setSpawnLoadout(loadout.name); + }} + className={`w-full`} + > + + {loadout.name} + + + ); + })} + +
+ + )} + { + setShowAdvancedOptions(!showAdvancedOptions); + }} + open={showAdvancedOptions} + title="Advanced options" + > +
+
+ + Livery + + + {props.blueprint.liveries && + Object.keys(props.blueprint.liveries) + .sort((ida, idb) => { + if (props.blueprint.liveries) { + if (props.blueprint.liveries[ida].countries.length > 1) return 1; + return props.blueprint.liveries[ida].countries[0] > props.blueprint.liveries[idb].countries[0] ? 1 : -1; + } else return -1; + }) + .map((id) => { + let country = Object.values(countryCodes).find((countryCode) => { + if (props.blueprint.liveries && countryCode.liveryCodes?.includes(props.blueprint.liveries[id].countries[0])) return true; + }); + return ( + { + setSpawnLiveryID(id); + }} + className={`w-full`} + > + + {props.blueprint.liveries && props.blueprint.liveries[id].countries.length == 1 && ( + + )} + +
+ + {props.blueprint.liveries ? props.blueprint.liveries[id].name : ""} + +
+
+
+ ); + })} +
+
+
+ + Skill + + + {["Average", "Good", "High", "Excellent"].map((skill) => { + return ( + { + setSpawnSkill(skill); + }} + className={`w-full`} + > + +
{skill}
+
+
+ ); + })} +
+
+
+
+
+ { + setShowUnitSummary(!showUnitSummary); + }} + open={showUnitSummary} + title="Unit summary" + > + + + {spawnLoadout && spawnLoadout.items.length > 0 && ( + { + setShowLoadout(!showLoadout); + }} + open={showLoadout} + title="Loadout" + > + {spawnLoadout.items.map((item) => { + return ( +
+
+ {item.quantity} +
+
+ {item.name} +
+
+ ); + })} +
+ )} + {(props.latlng || props.airbase) && ( + + )} +
+ ); +} diff --git a/frontend/react/src/ui/panels/components/menu.tsx b/frontend/react/src/ui/panels/components/menu.tsx index eadeb49e..6f4d6271 100644 --- a/frontend/react/src/ui/panels/components/menu.tsx +++ b/frontend/react/src/ui/panels/components/menu.tsx @@ -2,7 +2,6 @@ import { faArrowLeft, faClose } from "@fortawesome/free-solid-svg-icons"; import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; import React, { useEffect, useState } from "react"; import { FaChevronDown, FaChevronUp } from "react-icons/fa"; -import { HideMenuEvent } from "../../../events"; export function Menu(props: { title: string; @@ -16,11 +15,7 @@ export function Menu(props: { const [hide, setHide] = useState(true); if (!props.open && hide) setHide(false); - - useEffect(() => { - HideMenuEvent.dispatch(hide) - }, [hide]) - + return (
setMapOptions({ ...mapOptions })); }, []); - useEffect(() => { + const callback = useCallback(() => { const touch = matchMedia("(hover: none)").matches; let controls: { actions: (string | number | IconDefinition)[]; - target: IconDefinition; + target: IconDefinition | null; text: string; }[] = []; @@ -42,7 +44,7 @@ export function ControlsPanel(props: {}) { text: "Select unit", }, { - actions: [touch ? faHandPointer : "LMB", 2], + actions: touch ? [faHandPointer, "Hold"] : ["RMB"], target: faMap, text: "Quick spawn menu", }, @@ -57,6 +59,58 @@ export function ControlsPanel(props: {}) { text: "Move map location", }, ]; + } else if (appState === OlympusState.SPAWN_CONTEXT) { + controls = [ + { + actions: [touch ? faHandPointer : "LMB"], + target: faJetFighter, + text: "Close context menu", + }, + { + actions: touch ? [faHandPointer, "Hold"] : ["RMB"], + target: faMap, + text: "Move context menu", + }, + { + actions: touch ? [faHandPointer, "Drag"] : ["Shift", "LMB", "Drag"], + target: faMap, + text: "Box selection", + }, + { + actions: [touch ? faHandPointer : "LMB", "Drag"], + target: faMap, + text: "Move map location", + }, + ]; + } else if (appState === OlympusState.UNIT_CONTROL) { + if (!mapOptions.tabletMode) { + controls = Object.values(getApp().getMap().getContextActionSet()?.getContextActions() ?? {}) + .sort((a: ContextAction, b: ContextAction) => (a.getLabel() > b.getLabel() ? 1 : -1)) + .filter((contextAction: ContextAction) => contextAction.getOptions().code) + .map((contextAction: ContextAction) => { + let actions: (string | IconDefinition)[] = []; + contextAction.getOptions().shiftKey && actions.push("Shift"); + contextAction.getOptions().altKey && actions.push("Alt"); + contextAction.getOptions().ctrlKey && actions.push("Ctrl"); + actions.push( + (contextAction.getOptions().code as string) + .replace("Key", "") + .replace("ControlLeft", "Ctrl LH") + .replace("AltLeft", "Alt LH") + .replace("ShiftLeft", "Shift LH") + .replace("ControlRight", "Ctrl RH") + .replace("AltRight", "Alt RH") + .replace("ShiftRight", "Shift RH") + ); + contextAction.getTarget() !== ContextActionTarget.NONE && actions.push(touch ? faHandPointer : "LMB"); + return { + actions: actions, + target: + contextAction.getTarget() === ContextActionTarget.NONE ? null : contextAction.getTarget() === ContextActionTarget.POINT ? faMap : faJetFighter, + text: contextAction.getLabel(), + }; + }); + } } else if (appState === OlympusState.SPAWN) { controls = [ { @@ -91,7 +145,9 @@ export function ControlsPanel(props: {}) { } setControls(controls); - }, [appState, appSubState]); + }, [appState, appSubState, mapOptions]); + + useEffect(callback, [appState, appSubState, mapOptions]); return (
- {typeof action === "string" || typeof action === "number" ? action : } + {typeof action === "string" || typeof action === "number" ? ( + action + ) : ( + + )}
{idx < control.actions.length - 1 && typeof control.actions[idx + 1] === "string" &&
+
} {idx < control.actions.length - 1 && typeof control.actions[idx + 1] === "number" &&
x
} diff --git a/frontend/react/src/ui/panels/header.tsx b/frontend/react/src/ui/panels/header.tsx index 74c3591a..e2b389d6 100644 --- a/frontend/react/src/ui/panels/header.tsx +++ b/frontend/react/src/ui/panels/header.tsx @@ -15,7 +15,7 @@ import { olButtonsVisibilityNavyunit, olButtonsVisibilityOlympus, } from "../components/olicons"; -import { FaChevronLeft, FaChevronRight } from "react-icons/fa6"; +import { FaChevronLeft, FaChevronRight, FaComputer, FaTabletScreenButton } from "react-icons/fa6"; import { CommandModeOptionsChangedEvent, ConfigLoadedEvent, HiddenTypesChangedEvent, MapOptionsChangedEvent, MapSourceChangedEvent } from "../../events"; import { BLUE_COMMANDER, COMMAND_MODE_OPTIONS_DEFAULTS, MAP_HIDDEN_TYPES_DEFAULTS, MAP_OPTIONS_DEFAULTS } from "../../constants/constants"; import { OlympusConfig } from "../../interfaces"; @@ -111,12 +111,18 @@ export function Header() {
{commandModeOptions.commandMode === BLUE_COMMANDER && ( -
+
BLUE Commander ({commandModeOptions.spawnPoints.blue} points)
)} +
{ + getApp().getMap().setOption("tabletMode", !mapOptions.tabletMode); + }} + > + {mapOptions.tabletMode ? : } +
+ /> { AppStateChangedEvent.on((state, subState) => setAppState(state)); - HideMenuEvent.on((hidden) => setMenuHidden(hidden)); HotgroupsChangedEvent.on((hotgroups) => setHotgroups({ ...hotgroups })); }, []); diff --git a/frontend/react/src/ui/panels/infobar.tsx b/frontend/react/src/ui/panels/infobar.tsx index 3683ae37..98d40f68 100644 --- a/frontend/react/src/ui/panels/infobar.tsx +++ b/frontend/react/src/ui/panels/infobar.tsx @@ -1,5 +1,5 @@ import React, { useEffect, useState } from "react"; -import { AppStateChangedEvent, ContextActionChangedEvent, HideMenuEvent, InfoPopupEvent } from "../../events"; +import { AppStateChangedEvent, ContextActionChangedEvent, InfoPopupEvent } from "../../events"; import { OlympusState } from "../../constants/constants"; import { ContextAction } from "../../unit/contextaction"; @@ -7,35 +7,16 @@ export function InfoBar(props: {}) { const [messages, setMessages] = useState([] as string[]); const [appState, setAppState] = useState(OlympusState.NOT_INITIALIZED); const [contextAction, setContextAction] = useState(null as ContextAction | null); - const [menuHidden, setMenuHidden] = useState(false); useEffect(() => { InfoPopupEvent.on((messages) => setMessages([...messages])); AppStateChangedEvent.on((state, subState) => setAppState(state)); ContextActionChangedEvent.on((contextAction) => setContextAction(contextAction)); - HideMenuEvent.on((hidden) => setMenuHidden(hidden)); }, []); - let topString = ""; - if (appState === OlympusState.UNIT_CONTROL) { - if (contextAction === null) { - topString = "top-36"; - } else { - topString = "top-48"; - } - } else { - topString = "top-16"; - } - return (
{messages.slice(Math.max(0, messages.length - 4), Math.max(0, messages.length)).map((message, idx) => { return ( diff --git a/frontend/react/src/ui/panels/spawnmenu.tsx b/frontend/react/src/ui/panels/spawnmenu.tsx index 95a47017..54ffd5bd 100644 --- a/frontend/react/src/ui/panels/spawnmenu.tsx +++ b/frontend/react/src/ui/panels/spawnmenu.tsx @@ -436,7 +436,6 @@ export function SpawnMenu(props: { open: boolean; onClose: () => void; children? {!(blueprint === null) && ( diff --git a/frontend/react/src/ui/panels/unitcontrolbar.tsx b/frontend/react/src/ui/panels/unitcontrolbar.tsx index 4c9552ea..8c203e93 100644 --- a/frontend/react/src/ui/panels/unitcontrolbar.tsx +++ b/frontend/react/src/ui/panels/unitcontrolbar.tsx @@ -3,19 +3,20 @@ import { ContextActionSet } from "../../unit/contextactionset"; import { OlStateButton } from "../components/olstatebutton"; import { getApp } from "../../olympusapp"; import { ContextAction } from "../../unit/contextaction"; -import { CONTEXT_ACTION_COLORS } from "../../constants/constants"; +import { CONTEXT_ACTION_COLORS, MAP_OPTIONS_DEFAULTS } from "../../constants/constants"; import { FaInfoCircle } from "react-icons/fa"; -import { FaChevronLeft, FaChevronRight } from "react-icons/fa6"; +import { FaChevronDown, FaChevronLeft, FaChevronRight, FaChevronUp } from "react-icons/fa6"; import { OlympusState } from "../../constants/constants"; -import { AppStateChangedEvent, ContextActionChangedEvent, ContextActionSetChangedEvent, HideMenuEvent } from "../../events"; +import { AppStateChangedEvent, ContextActionChangedEvent, ContextActionSetChangedEvent, MapOptionsChangedEvent } from "../../events"; export function UnitControlBar(props: {}) { const [appState, setAppState] = useState(OlympusState.NOT_INITIALIZED); const [contextActionSet, setcontextActionSet] = useState(null as ContextActionSet | null); const [contextAction, setContextAction] = useState(null as ContextAction | null); - const [scrolledLeft, setScrolledLeft] = useState(true); - const [scrolledRight, setScrolledRight] = useState(false); + const [scrolledTop, setScrolledTop] = useState(true); + const [scrolledBottom, setScrolledBottom] = useState(false); const [menuHidden, setMenuHidden] = useState(false); + const [mapOptions, setMapOptions] = useState(MAP_OPTIONS_DEFAULTS); /* Initialize the "scroll" position of the element */ var scrollRef = useRef(null); @@ -27,18 +28,18 @@ export function UnitControlBar(props: {}) { AppStateChangedEvent.on((state, subState) => setAppState(state)); ContextActionSetChangedEvent.on((contextActionSet) => setcontextActionSet(contextActionSet)); ContextActionChangedEvent.on((contextAction) => setContextAction(contextAction)); - HideMenuEvent.on((hidden) => setMenuHidden(hidden)); + MapOptionsChangedEvent.on((mapOptions) => setMapOptions({ ...mapOptions })); }, []); function onScroll(el) { - const sl = el.scrollLeft; - const sr = el.scrollWidth - el.scrollLeft - el.clientWidth; + const sl = el.scrollTop; + const sr = el.scrollHeight - el.scrollTop - el.clientHeight; - sl < 1 && !scrolledLeft && setScrolledLeft(true); - sl > 1 && scrolledLeft && setScrolledLeft(false); + sl < 1 && !scrolledTop && setScrolledTop(true); + sl > 1 && scrolledTop && setScrolledTop(false); - sr < 1 && !scrolledRight && setScrolledRight(true); - sr > 1 && scrolledRight && setScrolledRight(false); + sr < 1 && !scrolledBottom && setScrolledBottom(true); + sr > 1 && scrolledBottom && setScrolledBottom(false); } let reorderedActions: ContextAction[] = contextActionSet @@ -49,70 +50,68 @@ export function UnitControlBar(props: {}) { <> {appState === OlympusState.UNIT_CONTROL && contextActionSet && Object.keys(contextActionSet.getContextActions()).length > 0 && ( <> -
- {!scrolledLeft && ( - +
- )} -
onScroll(ev.target)} ref={scrollRef}> - {reorderedActions.map((contextActionIt: ContextAction) => { - return ( -
- { - if (contextActionIt.getOptions().executeImmediately) { - contextActionIt.executeCallback(null, null); - } else { - contextActionIt !== contextAction ? getApp().getMap().setContextAction(contextActionIt) : getApp().getMap().setContextAction(null); - } - }} - /> -
- {(contextActionIt.getOptions().hotkey ?? "").replace("Key", "")} -
-
- ); - })} -
- {!scrolledRight && ( - - )} -
- {/*} + > + {!scrolledTop && ( + + )} +
onScroll(ev.target)} ref={scrollRef}> + {reorderedActions.map((contextActionIt: ContextAction) => { + return ( +
+ { + if (contextActionIt.getOptions().executeImmediately) { + contextActionIt.executeCallback(null, null); + } else { + contextActionIt !== contextAction + ? getApp().getMap().setContextAction(contextActionIt) + : getApp().getMap().setContextAction(null); + } + }} + /> +
+ ); + })} +
+ {!scrolledBottom && ( + + )} +
+ + )} + {contextAction && (
)} - {*/} )} diff --git a/frontend/react/src/ui/panels/unitspawnmenu.tsx b/frontend/react/src/ui/panels/unitspawnmenu.tsx index 9770b1aa..979e0a13 100644 --- a/frontend/react/src/ui/panels/unitspawnmenu.tsx +++ b/frontend/react/src/ui/panels/unitspawnmenu.tsx @@ -21,7 +21,6 @@ import { OlAccordion } from "../components/olaccordion"; export function UnitSpawnMenu(props: { starredSpawns: { [key: string]: SpawnRequestTable }; blueprint: UnitBlueprint; - spawnAtLocation: boolean; airbase?: Airbase | null; coalition?: Coalition; }) { @@ -49,7 +48,7 @@ export function UnitSpawnMenu(props: { /* When the menu is opened show the unit preview on the map as a cursor */ useEffect(() => { - if (props.spawnAtLocation && spawnRequestTable) { + if (!props.airbase && spawnRequestTable) { /* Refresh the unique key identified */ const newKey = hash(JSON.stringify(spawnRequestTable)); setKey(newKey); @@ -67,7 +66,7 @@ export function UnitSpawnMenu(props: { /* Callback and effect to update the quick access name in the input field */ const updateQuickAccessName = useCallback(() => { - if (props.spawnAtLocation) { + if (!props.airbase) { /* If the spawn is starred, set the quick access name */ if (key in props.starredSpawns && props.starredSpawns[key].quickAccessName) setQuickAccessName(props.starredSpawns[key].quickAccessName); else setQuickAccessName("No name"); @@ -408,7 +407,7 @@ export function UnitSpawnMenu(props: {
)} - {!props.spawnAtLocation && ( + {props.airbase && (
); diff --git a/frontend/react/src/unit/contextaction.ts b/frontend/react/src/unit/contextaction.ts index 53ba0823..56742ce5 100644 --- a/frontend/react/src/unit/contextaction.ts +++ b/frontend/react/src/unit/contextaction.ts @@ -6,7 +6,10 @@ import { ContextActionTarget, ContextActionType } from "../constants/constants"; export interface ContextActionOptions { executeImmediately?: boolean; type: ContextActionType; - hotkey?: string; + code: string | null; + shiftKey?: boolean; + altKey?: boolean; + ctrlKey?: boolean; } export type ContextActionCallback = (units: Unit[], targetUnit: Unit | null, targetPosition: LatLng | null, originalEvent?: MouseEvent) => void; diff --git a/frontend/react/src/unit/unit.ts b/frontend/react/src/unit/unit.ts index a935dd71..c0158d74 100644 --- a/frontend/react/src/unit/unit.ts +++ b/frontend/react/src/unit/unit.ts @@ -159,7 +159,6 @@ export abstract class Unit extends CustomMarker { /* Inputs timers */ #mouseCooldownTimer: number = 0; #shortPressTimer: number = 0; - #longPressTimer: number = 0; #isMouseOnCooldown: boolean = false; #isMouseDown: boolean = false; @@ -345,10 +344,11 @@ export abstract class Unit extends CustomMarker { /* Leaflet events listeners */ this.on("mousedown", (e) => this.#onMouseDown(e)); this.on("mouseup", (e) => this.#onMouseUp(e)); + this.on("contextmenu", (e) => this.#onRightClick(e)); this.on("dblclick", (e) => this.#onDoubleClick(e)); + this.on("mouseover", () => { - if (this.belongsToCommandedCoalition() && (getApp().getState() === OlympusState.IDLE || getApp().getState() === OlympusState.UNIT_CONTROL)) - this.setHighlighted(true); + if (this.belongsToCommandedCoalition()) this.setHighlighted(true); }); this.on("mouseout", () => this.setHighlighted(false)); getApp() @@ -1281,96 +1281,83 @@ export abstract class Unit extends CustomMarker { } /***********************************************/ - #onMouseUp(e: any) { - if (getApp().getState() === OlympusState.IDLE || getApp().getState() === OlympusState.UNIT_CONTROL) { - this.#isMouseDown = false; - - if (getApp().getMap().isSelecting()) return; - - DomEvent.stop(e); - DomEvent.preventDefault(e); - e.originalEvent.stopImmediatePropagation(); - - e.originalEvent.stopPropagation(); - - window.clearTimeout(this.#longPressTimer); - - this.#isMouseOnCooldown = true; - this.#mouseCooldownTimer = window.setTimeout(() => { - this.#isMouseOnCooldown = false; - }, 200); - } - } - #onMouseDown(e: any) { - if (getApp().getState() === OlympusState.IDLE || getApp().getState() === OlympusState.UNIT_CONTROL) { - this.#isMouseDown = true; + if (e.originalEvent.button === 2) return; - DomEvent.stop(e); - DomEvent.preventDefault(e); - e.originalEvent.stopImmediatePropagation(); + this.#isMouseDown = true; - if (this.#isMouseOnCooldown) { - return; - } + DomEvent.stop(e); + DomEvent.preventDefault(e); + e.originalEvent.stopImmediatePropagation(); - this.#shortPressTimer = window.setTimeout(() => { - /* If the mouse is no longer being pressed, execute the short press action */ - if (!this.#isMouseDown) this.#onShortPress(e); - }, 200); + if (this.#isMouseOnCooldown) return; - this.#longPressTimer = window.setTimeout(() => { - /* If the mouse is still being pressed, execute the long press action */ - if (this.#isMouseDown) this.#onLongPress(e); - }, 350); - } + this.#shortPressTimer = window.setTimeout(() => { + /* If the mouse is no longer being pressed, execute the short press action */ + if (!this.#isMouseDown) this.#onLeftClick(e); + }, 200); } - #onShortPress(e: LeafletMouseEvent) { - console.log(`Short press on ${this.getUnitName()}`); + #onMouseUp(e: any) { + if (e.originalEvent.button === 2) return; - if (getApp().getState() === OlympusState.IDLE) { + this.#isMouseDown = false; + + if (getApp().getMap().isSelecting()) return; + + DomEvent.stop(e); + DomEvent.preventDefault(e); + e.originalEvent.stopImmediatePropagation(); + + this.#isMouseOnCooldown = true; + this.#mouseCooldownTimer = window.setTimeout(() => { + this.#isMouseOnCooldown = false; + }, 200); + } + + #onLeftClick(e: any) { + console.log(`Left click on ${this.getUnitName()}`); + + if (getApp().getMap().getContextAction() === null) { if (!e.originalEvent.ctrlKey) getApp().getUnitsManager().deselectAllUnits(); this.setSelected(!this.getSelected()); } else if (getApp().getState() === OlympusState.UNIT_CONTROL) { - if (getApp().getMap().getContextAction() && getApp().getMap().getContextAction()?.getTarget() === ContextActionTarget.UNIT) { + if (getApp().getMap().getContextAction()?.getTarget() === ContextActionTarget.UNIT) { getApp().getMap().executeContextAction(this, null, e.originalEvent); } else { if (!e.originalEvent.ctrlKey) getApp().getUnitsManager().deselectAllUnits(); this.setSelected(!this.getSelected()); } - } else if (getApp().getState() === OlympusState.JTAC && getApp().getSubState() === JTACSubState.SELECT_TARGET) { - // TODO document.dispatchEvent(new CustomEvent("selectJTACTarget", { detail: { unit: this } })); - getApp().setState(OlympusState.IDLE); } } - #onLongPress(e: any) { - console.log(`Long press on ${this.getUnitName()}`); + #onRightClick(e: any) { + console.log(`Right click on ${this.getUnitName()}`); + + if (getApp().getState() === OlympusState.UNIT_CONTROL && !getApp().getMap().getContextAction()) { + DomEvent.stop(e); + DomEvent.preventDefault(e); + e.originalEvent.stopImmediatePropagation(); - if (getApp().getState() === OlympusState.UNIT_CONTROL && e.originalEvent.button === 2 && !getApp().getMap().getContextAction()) { getApp().setState(OlympusState.UNIT_CONTROL, UnitControlSubState.UNIT_CONTEXT_MENU); UnitContextMenuRequestEvent.dispatch(this); } } #onDoubleClick(e: any) { + DomEvent.stop(e); + DomEvent.preventDefault(e); + + console.log(`Double click on ${this.getUnitName()}`); + + window.clearTimeout(this.#shortPressTimer); + + /* Select all matching units in the viewport */ if (getApp().getState() === OlympusState.IDLE || getApp().getState() === OlympusState.UNIT_CONTROL) { - DomEvent.stop(e); - DomEvent.preventDefault(e); - - console.log(`Double click on ${this.getUnitName()}`); - - window.clearTimeout(this.#shortPressTimer); - window.clearTimeout(this.#longPressTimer); - - /* Select all matching units in the viewport */ - if (getApp().getState() === OlympusState.IDLE || getApp().getState() === OlympusState.UNIT_CONTROL) { - const unitsManager = getApp().getUnitsManager(); - Object.values(unitsManager.getUnits()).forEach((unit: Unit) => { - if (unit.getAlive() === true && unit.getName() === this.getName() && unit.isInViewport()) unitsManager.selectUnit(unit.ID, false); - }); - } + const unitsManager = getApp().getUnitsManager(); + Object.values(unitsManager.getUnits()).forEach((unit: Unit) => { + if (unit.getAlive() === true && unit.getName() === this.getName() && unit.isInViewport()) unitsManager.selectUnit(unit.ID, false); + }); } } diff --git a/frontend/react/src/unit/unitsmanager.ts b/frontend/react/src/unit/unitsmanager.ts index 088093d1..f405d350 100644 --- a/frontend/react/src/unit/unitsmanager.ts +++ b/frontend/react/src/unit/unitsmanager.ts @@ -68,18 +68,6 @@ export class UnitsManager { getApp() .getShortcutManager() - .addShortcut("deselectAll", { - label: "Deselect all units", - keyUpCallback: (ev: KeyboardEvent) => { - getApp().getUnitsManager().deselectAllUnits(); - }, - code: "Escape", - }) - .addShortcut("delete", { - label: "Delete selected units", - keyUpCallback: () => this.delete(), - code: "Delete", - }) .addShortcut("selectAll", { label: "Select all units", keyUpCallback: () => {