diff --git a/frontend/react/src/constants/constants.ts b/frontend/react/src/constants/constants.ts
index 7da5ae1c..93b11d36 100644
--- a/frontend/react/src/constants/constants.ts
+++ b/frontend/react/src/constants/constants.ts
@@ -3,6 +3,7 @@ import { Coalition, MapOptions } from "../types/types";
import { CommandModeOptions } from "../interfaces";
import { ContextAction } from "../unit/contextaction";
import {
+ faClone,
faExplosion,
faHand,
faLocationCrosshairs,
@@ -264,12 +265,12 @@ export const mapBounds = {
export const defaultMapMirrors = {};
export const defaultMapLayers = {
- "AWACS": {
- "urlTemplate": 'https://abcd.basemaps.cartocdn.com/dark_nolabels/{z}/{x}/{y}{r}.png',
- "minZoom": 1,
- "maxZoom": 19,
- "attribution": `© OpenStreetMap contributors © CARTO'`
- },
+ AWACS: {
+ urlTemplate: "https://abcd.basemaps.cartocdn.com/dark_nolabels/{z}/{x}/{y}{r}.png",
+ minZoom: 1,
+ maxZoom: 19,
+ attribution: `© OpenStreetMap contributors © CARTO'`,
+ },
};
export enum OlympusState {
@@ -286,7 +287,7 @@ export enum OlympusState {
OPTIONS = "Options",
AUDIO = "Audio",
AIRBASE = "Airbase",
- GAME_MASTER = "Game master"
+ GAME_MASTER = "Game master",
}
export const NO_SUBSTATE = "No substate";
@@ -304,7 +305,7 @@ export enum LoginSubState {
NO_SUBSTATE = "No substate",
CREDENTIALS = "Credentials",
COMMAND_MODE = "Command mode",
- CONNECT = "Connect"
+ CONNECT = "Connect",
}
export enum DrawSubState {
@@ -359,7 +360,7 @@ export const MAP_OPTIONS_DEFAULTS: MapOptions = {
cameraPluginMode: "map",
tabletMode: false,
AWACSMode: false,
- AWACSCoalition: "blue"
+ AWACSCoalition: "blue",
};
export const MAP_HIDDEN_TYPES_DEFAULTS = {
@@ -602,9 +603,7 @@ export namespace ContextActions {
ContextActionTarget.POINT,
(units: Unit[], _, targetPosition: LatLng | null) => {
if (targetPosition)
- getApp()
- .getUnitsManager()
- .bombPoint(targetPosition, getApp().getMap().getKeepRelativePositions(), getApp().getMap().getDestinationRotation(), units);
+ getApp().getUnitsManager().bombPoint(targetPosition, getApp().getMap().getKeepRelativePositions(), getApp().getMap().getDestinationRotation(), units);
},
{ type: ContextActionType.ENGAGE, code: "KeyB", ctrlKey: false, shiftKey: false }
);
@@ -617,11 +616,9 @@ export namespace ContextActions {
ContextActionTarget.POINT,
(units: Unit[], _, targetPosition: LatLng | null) => {
if (targetPosition)
- getApp()
- .getUnitsManager()
- .carpetBomb(targetPosition, getApp().getMap().getKeepRelativePositions(), getApp().getMap().getDestinationRotation(), units);
+ getApp().getUnitsManager().carpetBomb(targetPosition, getApp().getMap().getKeepRelativePositions(), getApp().getMap().getDestinationRotation(), units);
},
- { type: ContextActionType.ENGAGE, code: "KeyC", ctrlKey: false, shiftKey: false }
+ { type: ContextActionType.ENGAGE, code: "KeyH", ctrlKey: false, shiftKey: false }
);
export const LAND = new ContextAction(
@@ -644,9 +641,7 @@ export namespace ContextActions {
ContextActionTarget.POINT,
(units: Unit[], _, targetPosition: LatLng | null) => {
if (targetPosition)
- getApp()
- .getUnitsManager()
- .landAtPoint(targetPosition, getApp().getMap().getKeepRelativePositions(), getApp().getMap().getDestinationRotation(), units);
+ getApp().getUnitsManager().landAtPoint(targetPosition, getApp().getMap().getKeepRelativePositions(), getApp().getMap().getDestinationRotation(), units);
},
{ type: ContextActionType.ADMIN, code: "KeyK", ctrlKey: false, shiftKey: false }
);
@@ -660,7 +655,7 @@ export namespace ContextActions {
(units: Unit[], _1, _2) => {
getApp().getUnitsManager().createGroup(units);
},
- { type: ContextActionType.OTHER, code: "KeyG", ctrlKey: false, shiftKey: false, altKey: false }
+ { type: ContextActionType.OTHER, code: "KeyG", ctrlKey: false, shiftKey: false, altKey: false }
);
export const ATTACK = new ContextAction(
@@ -683,9 +678,7 @@ export namespace ContextActions {
ContextActionTarget.POINT,
(units: Unit[], _, targetPosition: LatLng | null) => {
if (targetPosition)
- getApp()
- .getUnitsManager()
- .fireAtArea(targetPosition, getApp().getMap().getKeepRelativePositions(), getApp().getMap().getDestinationRotation(), units);
+ getApp().getUnitsManager().fireAtArea(targetPosition, getApp().getMap().getKeepRelativePositions(), getApp().getMap().getDestinationRotation(), units);
},
{ type: ContextActionType.ENGAGE, code: "KeyV", ctrlKey: false, shiftKey: false }
);
@@ -714,6 +707,19 @@ export namespace ContextActions {
(units: Unit[], _1, _2) => {
getApp().getUnitsManager().setAWACSReference(units[0].ID);
},
- { type: ContextActionType.ADMIN, code: "KeyU", ctrlKey: false, shiftKey: false, altKey: false }
+ { type: ContextActionType.ADMIN, code: "KeyU", ctrlKey: false, shiftKey: false, altKey: false }
+ );
+
+ export const CLONE = new ContextAction(
+ "clone",
+ "Clone unit",
+ "Clone the unit at the given location",
+ faClone,
+ ContextActionTarget.POINT,
+ (units: Unit[], _1, targetPosition) => {
+ getApp().getUnitsManager().copy(units);
+ if (targetPosition) getApp().getUnitsManager().paste(targetPosition);
+ },
+ { type: ContextActionType.ADMIN, code: "KeyC", ctrlKey: false, shiftKey: false, altKey: false }
);
}
diff --git a/frontend/react/src/events.ts b/frontend/react/src/events.ts
index 651b06f3..bc29b7ea 100644
--- a/frontend/react/src/events.ts
+++ b/frontend/react/src/events.ts
@@ -1,7 +1,7 @@
import { AudioSink } from "./audio/audiosink";
import { AudioSource } from "./audio/audiosource";
import { OlympusState, OlympusSubState } from "./constants/constants";
-import { CommandModeOptions, OlympusConfig, ServerStatus, SessionData, SpawnRequestTable } from "./interfaces";
+import { CommandModeOptions, OlympusConfig, ServerStatus, SessionData, SpawnRequestTable, UnitData } from "./interfaces";
import { CoalitionCircle } from "./map/coalitionarea/coalitioncircle";
import { CoalitionPolygon } from "./map/coalitionarea/coalitionpolygon";
import { Airbase } from "./mission/airbase";
@@ -242,6 +242,32 @@ export class AirbaseSelectedEvent {
}
}
+export class SelectionEnabledChangedEvent {
+ static on(callback: (enabled: boolean) => void) {
+ document.addEventListener(this.name, (ev: CustomEventInit) => {
+ callback(ev.detail.enabled);
+ });
+ }
+
+ static dispatch(enabled: boolean) {
+ document.dispatchEvent(new CustomEvent(this.name, { detail: { enabled } }));
+ console.log(`Event ${this.name} dispatched`);
+ }
+};
+
+export class PasteEnabledChangedEvent {
+ static on(callback: (enabled: boolean) => void) {
+ document.addEventListener(this.name, (ev: CustomEventInit) => {
+ callback(ev.detail.enabled);
+ });
+ }
+
+ static dispatch(enabled: boolean) {
+ document.dispatchEvent(new CustomEvent(this.name, { detail: { enabled } }));
+ console.log(`Event ${this.name} dispatched`);
+ }
+};
+
export class ContactsUpdatedEvent {
static on(callback: () => void) {
document.addEventListener(this.name, (ev: CustomEventInit) => {
@@ -281,6 +307,19 @@ export class ContextActionChangedEvent {
}
}
+export class CopiedUnitsEvents {
+ static on(callback: (unitsData: UnitData[]) => void) {
+ document.addEventListener(this.name, (ev: CustomEventInit) => {
+ callback(ev.detail.unitsData);
+ });
+ }
+
+ static dispatch(unitsData: UnitData[]) {
+ document.dispatchEvent(new CustomEvent(this.name, { detail: { unitsData } }));
+ console.log(`Event ${this.name} dispatched`);
+ }
+}
+
export class UnitUpdatedEvent extends BaseUnitEvent {
static dispatch(unit: Unit) {
document.dispatchEvent(new CustomEvent(this.name, { detail: { unit } }));
diff --git a/frontend/react/src/map/boxselect.ts b/frontend/react/src/map/boxselect.ts
index b8cb922e..d4db058c 100644
--- a/frontend/react/src/map/boxselect.ts
+++ b/frontend/react/src/map/boxselect.ts
@@ -46,7 +46,7 @@ export var BoxSelect = Handler.extend({
},
_onMouseDown: function (e: any) {
- if (this._map.getEnableSelection() && e.button == 0) {
+ if (this._map.getSelectionEnabled() && 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();
diff --git a/frontend/react/src/map/map.ts b/frontend/react/src/map/map.ts
index 92d07e2e..3c9411da 100644
--- a/frontend/react/src/map/map.ts
+++ b/frontend/react/src/map/map.ts
@@ -53,7 +53,9 @@ import {
MapOptionsChangedEvent,
MapSourceChangedEvent,
MouseMovedEvent,
+ PasteEnabledChangedEvent,
SelectionClearedEvent,
+ SelectionEnabledChangedEvent,
SpawnContextMenuRequestEvent,
StarredSpawnsChangedEvent,
UnitDeselectedEvent,
@@ -113,7 +115,8 @@ export class Map extends L.Map {
#lastMouseCoordinates: L.LatLng = new L.LatLng(0, 0);
#previousZoom: number = 0;
#keepRelativePositions: boolean = false;
- #enableSelection: boolean = false;
+ #selectionEnabled: boolean = false;
+ #pasteEnabled: boolean = false;
/* Camera control plugin */
#slaveDCSCamera: boolean = false;
@@ -363,10 +366,10 @@ export class Map extends L.Map {
shiftKey: false,
ctrlKey: false,
})
- .addShortcut("toggleEnableSelection", {
+ .addShortcut("toggleSelectionEnabled", {
label: "Toggle box selection",
- keyUpCallback: () => this.setEnableSelection(false),
- keyDownCallback: () => this.setEnableSelection(true),
+ keyUpCallback: () => this.setSelectionEnabled(false),
+ keyDownCallback: () => this.setSelectionEnabled(true),
code: "ShiftLeft",
altKey: false,
ctrlKey: false,
@@ -755,12 +758,22 @@ export class Map extends L.Map {
return this.#keepRelativePositions;
}
- setEnableSelection(enableSelection: boolean) {
- this.#enableSelection = enableSelection;
+ setSelectionEnabled(selectionEnabled: boolean) {
+ this.#selectionEnabled = selectionEnabled;
+ SelectionEnabledChangedEvent.dispatch(selectionEnabled)
}
- getEnableSelection() {
- return this.#enableSelection;
+ getSelectionEnabled() {
+ return this.#selectionEnabled;
+ }
+
+ setPasteEnabled(pasteEnabled: boolean) {
+ this.#pasteEnabled = pasteEnabled;
+ PasteEnabledChangedEvent.dispatch(pasteEnabled)
+ }
+
+ getPasteEnabled() {
+ return this.#pasteEnabled;
}
increaseCameraZoom() {
@@ -805,8 +818,12 @@ export class Map extends L.Map {
this.#currentSpawnMarker = null;
this.#currentEffectMarker?.removeFrom(this);
this.#currentEffectMarker = null;
- this.setContextAction(null);
- if (state !== OlympusState.UNIT_CONTROL) getApp().getUnitsManager().deselectAllUnits();
+
+ if (state !== OlympusState.UNIT_CONTROL) {
+ getApp().getUnitsManager().deselectAllUnits();
+ this.setContextAction(null);
+ this.setContextActionSet(null);
+ }
if (state !== OlympusState.DRAW || (state === OlympusState.DRAW && subState !== DrawSubState.EDIT)) this.deselectAllCoalitionAreas();
this.getContainer().classList.remove(`explosion-cursor`);
["white", "blue", "red", "green", "orange"].forEach((color) => this.getContainer().classList.remove(`smoke-${color}-cursor`));
@@ -908,6 +925,10 @@ export class Map extends L.Map {
if (!this.#isSelecting) {
console.log(`Left short click at ${e.latlng}`);
+ if (this.#pasteEnabled) {
+ getApp().getUnitsManager().paste(e.latlng)
+ }
+
/* Execute the short click action */
if (getApp().getState() === OlympusState.IDLE) {
/* Do nothing */
@@ -1023,6 +1044,10 @@ export class Map extends L.Map {
}
getApp().setState(OlympusState.JTAC);
this.#drawIPToTargetLine();
+ } else if (getApp().getState() === OlympusState.UNIT_CONTROL) {
+ if (this.#contextAction !== null) this.executeContextAction(null, e.latlng, e.originalEvent);
+ else if (getApp().getSubState() === NO_SUBSTATE) getApp().setState(OlympusState.IDLE);
+ else getApp().setState(OlympusState.UNIT_CONTROL);
} else {
if (getApp().getSubState() === NO_SUBSTATE) getApp().setState(OlympusState.IDLE);
else getApp().setState(OlympusState.UNIT_CONTROL);
@@ -1041,8 +1066,7 @@ export class Map extends L.Map {
SpawnContextMenuRequestEvent.dispatch(e.latlng);
getApp().setState(OlympusState.SPAWN_CONTEXT);
} else if (getApp().getState() === OlympusState.UNIT_CONTROL) {
- if (this.#contextAction !== null) this.executeContextAction(null, e.latlng, e.originalEvent);
- else this.executeDefaultContextAction(null, e.latlng, e.originalEvent);
+ this.executeDefaultContextAction(null, e.latlng, e.originalEvent);
}
}
@@ -1062,6 +1086,8 @@ export class Map extends L.Map {
if (this.#debounceTimeout) window.clearTimeout(this.#debounceTimeout);
+ this.setPasteEnabled(false);
+
if (getApp().getSubState() === NO_SUBSTATE) getApp().setState(OlympusState.IDLE);
else getApp().setState(getApp().getState());
}
diff --git a/frontend/react/src/map/markers/stylesheets/units.css b/frontend/react/src/map/markers/stylesheets/units.css
index ad15e2f6..41a659be 100644
--- a/frontend/react/src/map/markers/stylesheets/units.css
+++ b/frontend/react/src/map/markers/stylesheets/units.css
@@ -172,9 +172,10 @@
/*** Cursors ***/
[data-is-dead],
-[data-object|="unit-missile"],
-[data-object|="unit-bomb"] {
- cursor: default;
+[data-object|="unit-missile"] *,
+[data-object|="unit-bomb"] *{
+ pointer-events: none;
+
}
/*** Labels ***/
diff --git a/frontend/react/src/ui/components/olstatebutton.tsx b/frontend/react/src/ui/components/olstatebutton.tsx
index ed158141..42bd329a 100644
--- a/frontend/react/src/ui/components/olstatebutton.tsx
+++ b/frontend/react/src/ui/components/olstatebutton.tsx
@@ -9,7 +9,8 @@ export function OlStateButton(props: {
buttonColor?: string | null;
checked: boolean;
icon?: IconProp;
- tooltip: string;
+ tooltip?: string | JSX.Element | JSX.Element[];
+ tooltipPosition?: string;
onClick: () => void;
onMouseUp?: () => void;
onMouseDown?: () => void;
@@ -21,7 +22,8 @@ export function OlStateButton(props: {
const className =
(props.className ?? "") +
`
- h-[40px] w-[40px] flex-none rounded-md text-lg font-medium
+ pointer-events-auto h-[40px] w-[40px] flex-none rounded-md text-lg
+ font-medium
dark:bg-olympus-600 dark:text-gray-300
`;
@@ -57,12 +59,12 @@ export function OlStateButton(props: {
setHover(false);
}}
>
-
+
{props.icon && }
{props.children}
- {hover &&
}
+ {hover && props.tooltip &&
}
>
);
}
diff --git a/frontend/react/src/ui/components/oltooltip.tsx b/frontend/react/src/ui/components/oltooltip.tsx
index 628f6bd1..1038065d 100644
--- a/frontend/react/src/ui/components/oltooltip.tsx
+++ b/frontend/react/src/ui/components/oltooltip.tsx
@@ -1,6 +1,6 @@
import React, { useEffect, useRef, useState } from "react";
-export function OlTooltip(props: { content: string; buttonRef: React.MutableRefObject }) {
+export function OlTooltip(props: { content: string | JSX.Element | JSX.Element[]; buttonRef: React.MutableRefObject; position?: string }) {
var contentRef = useRef(null);
function setPosition(content: HTMLDivElement, button: HTMLButtonElement) {
@@ -13,18 +13,18 @@ export function OlTooltip(props: { content: string; buttonRef: React.MutableRefO
let [cxl, cyt, cxr, cyb, cw, ch] = [
content.getBoundingClientRect().x,
content.getBoundingClientRect().y,
- content.getBoundingClientRect().x + content.clientWidth,
- content.getBoundingClientRect().y + content.clientHeight,
- content.clientWidth,
- content.clientHeight,
+ content.getBoundingClientRect().x + content.offsetWidth,
+ content.getBoundingClientRect().y + content.offsetHeight,
+ content.offsetWidth,
+ content.offsetHeight,
];
let [bxl, byt, bxr, byb, bbw, bh] = [
button.getBoundingClientRect().x,
button.getBoundingClientRect().y,
- button.getBoundingClientRect().x + button.clientWidth,
- button.getBoundingClientRect().y + button.clientHeight,
- button.clientWidth,
- button.clientHeight,
+ button.getBoundingClientRect().x + button.offsetWidth,
+ button.getBoundingClientRect().y + button.offsetHeight,
+ button.offsetWidth,
+ button.offsetHeight,
];
/* Limit the maximum height */
@@ -37,19 +37,29 @@ export function OlTooltip(props: { content: string; buttonRef: React.MutableRefO
var cxc = (cxl + cxr) / 2;
var bxc = (bxl + bxr) / 2;
- /* Compute the x and y offsets needed to align the button and element horizontally, and to put the content below the button */
- var offsetX = bxc - cxc;
- var offsetY = byb - cyt + 8;
+ /* Compute the x and y offsets needed to align the button and element horizontally, and to put the content depending on the requested position */
+ var offsetX = 0;
+ var offsetY = 0;
+
+ if (props.position === undefined || props.position === "below") {
+ offsetX = bxc - cxc;
+ offsetY = byb - cyt + 8;
+ } else if (props.position === "side") {
+ offsetX = bxr + 8;
+ offsetY = byt - cyt + (bh - ch) / 2;
+ }
/* Compute the new position of the left and right margins of the content */
- cxl += offsetX;
- cxr += offsetX;
- cyb += offsetY;
+ let ncxl = cxl + offsetX;
+ let ncxr = cxr + offsetX;
+ let ncyb = cyb + offsetY;
/* Try and move the content so it is inside the screen */
- if (cxl < 0) offsetX -= cxl;
- if (cxr > window.innerWidth) offsetX -= cxr - window.innerWidth;
- if (cyb > window.innerHeight) offsetY -= bh + ch + 16;
+ if (ncxl < 0) offsetX -= cxl;
+ if (ncxr > window.innerWidth) {
+ offsetX = bxl - cxl - cw - 12;
+ }
+ if (ncyb > window.innerHeight) offsetY -= bh + ch + 16;
/* Apply the offset */
content.style.left = `${offsetX}px`;
diff --git a/frontend/react/src/ui/panels/controlspanel.tsx b/frontend/react/src/ui/panels/controlspanel.tsx
index fda86a97..efa44d8a 100644
--- a/frontend/react/src/ui/panels/controlspanel.tsx
+++ b/frontend/react/src/ui/panels/controlspanel.tsx
@@ -5,6 +5,7 @@ import { MAP_OPTIONS_DEFAULTS, NO_SUBSTATE, OlympusState, OlympusSubState, Spawn
import { AppStateChangedEvent, ContextActionSetChangedEvent, MapOptionsChangedEvent, ShortcutsChangedEvent } from "../../events";
import { ContextAction } from "../../unit/contextaction";
import { ContextActionSet } from "../../unit/contextactionset";
+import { MapToolBar } from "./maptoolbar";
export function ControlsPanel(props: {}) {
const [controls, setControls] = useState(
@@ -19,8 +20,8 @@ export function ControlsPanel(props: {}) {
const [appState, setAppState] = useState(OlympusState.NOT_INITIALIZED);
const [appSubState, setAppSubState] = useState(NO_SUBSTATE as OlympusSubState);
const [mapOptions, setMapOptions] = useState(MAP_OPTIONS_DEFAULTS);
- const [shortcuts, setShortcuts] = useState({})
- const [contextActionSet, setContextActionSet] = useState(null as null | ContextActionSet)
+ const [shortcuts, setShortcuts] = useState({});
+ const [contextActionSet, setContextActionSet] = useState(null as null | ContextActionSet);
useEffect(() => {
AppStateChangedEvent.on((state, subState) => {
@@ -74,53 +75,28 @@ export function ControlsPanel(props: {}) {
}
);
} else if (appState === OlympusState.UNIT_CONTROL) {
- if (!mapOptions.tabletMode) {
- controls = Object.values(contextActionSet?.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", "Left Ctrl")
- .replace("AltLeft", "Left Alt")
- .replace("ShiftLeft", "Left Shift")
- .replace("ControlRight", "Right Ctrl")
- .replace("AltRight", "Right Alt")
- .replace("ShiftRight", "Right Shift")
- );
- return {
- actions: actions,
- text: contextAction.getLabel(),
- };
- });
- controls.unshift({
- actions: ["RMB"],
- text: "Move",
- });
- controls.push({
- actions: ["RMB", "Hold"],
- target: faMap,
- text: "Show point actions",
- });
- controls.push({
- actions: ["RMB", "Hold"],
- target: faFighterJet,
- text: "Show unit actions",
- });
- controls.push({
- actions: shortcuts["toggleRelativePositions"]?.toActions(),
- text: "Activate group movement",
- });
- controls.push({
- actions: [...shortcuts["toggleRelativePositions"]?.toActions(), "Wheel"],
- text: "Rotate formation",
- });
- }
+ controls.unshift({
+ actions: ["RMB"],
+ text: "Move",
+ });
+ controls.push({
+ actions: ["RMB", "Hold"],
+ target: faMap,
+ text: "Show point actions",
+ });
+ controls.push({
+ actions: ["RMB", "Hold"],
+ target: faFighterJet,
+ text: "Show unit actions",
+ });
+ controls.push({
+ actions: shortcuts["toggleRelativePositions"]?.toActions(),
+ text: "Activate group movement",
+ });
+ controls.push({
+ actions: [...shortcuts["toggleRelativePositions"]?.toActions(), "Wheel"],
+ text: "Rotate formation",
+ });
} else if (appState === OlympusState.SPAWN) {
controls = [
{
@@ -151,8 +127,8 @@ export function ControlsPanel(props: {}) {
controls = baseControls;
controls.push({
actions: ["LMB"],
- text: "Return to idle state"
- })
+ text: "Return to idle state",
+ });
}
setControls(controls);
@@ -163,11 +139,13 @@ export function ControlsPanel(props: {}) {
return (
+
{controls?.map((control) => {
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/maptoolbar.tsx b/frontend/react/src/ui/panels/maptoolbar.tsx
new file mode 100644
index 00000000..5061b0ed
--- /dev/null
+++ b/frontend/react/src/ui/panels/maptoolbar.tsx
@@ -0,0 +1,275 @@
+import React, { useEffect, useRef, useState } from "react";
+import { ContextActionSet } from "../../unit/contextactionset";
+import { OlStateButton } from "../components/olstatebutton";
+import { getApp } from "../../olympusapp";
+import { ContextAction, ContextActionOptions } from "../../unit/contextaction";
+import { CONTEXT_ACTION_COLORS, ContextActionTarget, MAP_OPTIONS_DEFAULTS } from "../../constants/constants";
+import { FaChevronDown, FaChevronUp } from "react-icons/fa6";
+import { OlympusState } from "../../constants/constants";
+import {
+ AppStateChangedEvent,
+ ContextActionChangedEvent,
+ ContextActionSetChangedEvent,
+ CopiedUnitsEvents,
+ MapOptionsChangedEvent,
+ PasteEnabledChangedEvent,
+ SelectedUnitsChangedEvent,
+ SelectionClearedEvent,
+ SelectionEnabledChangedEvent,
+ ShortcutsChangedEvent,
+} from "../../events";
+import { faCopy, faObjectGroup, faPaste } from "@fortawesome/free-solid-svg-icons";
+import { Shortcut } from "../../shortcut/shortcut";
+import { ShortcutOptions, UnitData } from "../../interfaces";
+import { Unit } from "../../unit/unit";
+
+export function MapToolBar(props: {}) {
+ const [appState, setAppState] = useState(OlympusState.IDLE);
+ const [contextActionSet, setcontextActionSet] = useState(null as ContextActionSet | null);
+ const [contextAction, setContextAction] = useState(null as ContextAction | null);
+ const [scrolledTop, setScrolledTop] = useState(true);
+ const [scrolledBottom, setScrolledBottom] = useState(false);
+ const [mapOptions, setMapOptions] = useState(MAP_OPTIONS_DEFAULTS);
+ const [selectionEnabled, setSelectionEnabled] = useState(false);
+ const [pasteEnabled, setPasteEnabled] = useState(false);
+ const [controller, setController] = useState(new AbortController());
+ const [shortcuts, setShortcuts] = useState(
+ {} as {
+ [key: string]: Shortcut;
+ }
+ );
+ const [selectedUnits, setSelectedUnits] = useState([] as Unit[]);
+ const [copiedUnitsData, setCopiedUnitsData] = useState([] as UnitData[]);
+
+ /* Initialize the "scroll" position of the element */
+ var scrollRef = useRef(null);
+ useEffect(() => {
+ if (scrollRef.current) onScroll(scrollRef.current);
+ });
+
+ useEffect(() => {
+ AppStateChangedEvent.on((state, subState) => setAppState(state));
+ ContextActionSetChangedEvent.on((contextActionSet) => setcontextActionSet(contextActionSet));
+ ContextActionChangedEvent.on((contextAction) => setContextAction(contextAction));
+ MapOptionsChangedEvent.on((mapOptions) => setMapOptions(mapOptions));
+ SelectionEnabledChangedEvent.on((selectionEnabled) => setSelectionEnabled(selectionEnabled));
+ PasteEnabledChangedEvent.on((pasteEnabled) => setPasteEnabled(pasteEnabled));
+ ShortcutsChangedEvent.on((shortcuts) => setShortcuts({ ...shortcuts }));
+ SelectedUnitsChangedEvent.on((selectedUnits) => setSelectedUnits(selectedUnits));
+ SelectionClearedEvent.on(() => setSelectedUnits([]));
+ CopiedUnitsEvents.on((unitsData) => setCopiedUnitsData(unitsData));
+ }, []);
+
+ function onScroll(el) {
+ const sl = el.scrollTop;
+ const sr = el.scrollHeight - el.scrollTop - el.clientHeight;
+
+ sl < 1 && !scrolledTop && setScrolledTop(true);
+ sl > 1 && scrolledTop && setScrolledTop(false);
+
+ sr < 1 && !scrolledBottom && setScrolledBottom(true);
+ sr > 1 && scrolledBottom && setScrolledBottom(false);
+ }
+
+ function shortcutCombination(options: ShortcutOptions | ContextActionOptions) {
+ if (options === undefined) return <>>;
+ return (
+ <>
+ {options.ctrlKey && (
+
+ Ctrl
+
+ )}
+ {options.altKey && (
+
+ Alt
+
+ )}
+ {options.shiftKey && (
+
+ Shift
+
+ )}
+
+ {options.code && (
+
+ {options.code?.replace("Key", "")}
+
+ )}
+ >
+ );
+ }
+
+ let reorderedActions: ContextAction[] = contextActionSet
+ ? Object.values(contextActionSet.getContextActions()).sort((a: ContextAction, b: ContextAction) => (a.getOptions().type < b.getOptions().type ? -1 : 1))
+ : [];
+
+ return (
+ <>
+ <>
+
+ {!scrolledTop && (
+
+ )}
+
onScroll(ev.target)} ref={scrollRef}>
+ <>
+
+
+ {shortcutCombination(shortcuts["toggleSelectionEnabled"]?.getOptions())}
+ Box selection
+
+ }
+ tooltipPosition="side"
+ onClick={() => {
+ getApp().getMap().setSelectionEnabled(!selectionEnabled);
+ if (!selectionEnabled) {
+ getApp()
+ .getMap()
+ .getContainer()
+ .addEventListener(
+ "mouseup",
+ () => {
+ getApp().getMap().setSelectionEnabled(false);
+ },
+ { once: true, signal: controller.signal }
+ );
+ } else {
+ controller.abort();
+ }
+ }}
+ />
+
+ {selectedUnits.length > 0 && (
+
+
+ {shortcutCombination(shortcuts["copyUnits"]?.getOptions())}
+ Copy selected units
+
+ }
+ tooltipPosition="side"
+ onClick={() => {
+ getApp().getUnitsManager().copy(selectedUnits);
+ }}
+ />
+
+ )}
+ {copiedUnitsData.length > 0 && (
+
+
+ {shortcutCombination(shortcuts["pasteUnits"]?.getOptions())}
+ Paste copied units
+
+ } tooltipPosition="side" onClick={() => {
+ getApp().getMap().setPasteEnabled(!pasteEnabled)
+ }} />
+
+ )}
+ >
+
+ {reorderedActions.map((contextActionIt: ContextAction) => {
+ return (
+
+
+ {shortcutCombination(contextActionIt.getOptions())}
+ {contextActionIt.getLabel()}
+
+ }
+ tooltipPosition="side"
+ buttonColor={CONTEXT_ACTION_COLORS[contextActionIt.getOptions().type ?? 0]}
+ onClick={() => {
+ if (contextActionIt.getTarget() === ContextActionTarget.NONE) {
+ contextActionIt.executeCallback(null, null);
+ } else {
+ contextActionIt !== contextAction ? getApp().getMap().setContextAction(contextActionIt) : getApp().getMap().setContextAction(null);
+ }
+ }}
+ />
+
+ );
+ })}
+
+ {!scrolledBottom && (
+
+ )}
+
+ >
+
+ {/*}
+ {contextAction && (
+
+
+
{contextAction.getDescription()}
+
+ )}
+ {*/}
+ >
+ );
+}
diff --git a/frontend/react/src/ui/panels/minimappanel.tsx b/frontend/react/src/ui/panels/minimappanel.tsx
index cb694c43..d0a201f9 100644
--- a/frontend/react/src/ui/panels/minimappanel.tsx
+++ b/frontend/react/src/ui/panels/minimappanel.tsx
@@ -20,7 +20,7 @@ export function MiniMapPanel(props: {}) {
useEffect(() => {
let miniMap = document.querySelector(".leaflet-control-minimap");
if (miniMap) {
- miniMap.classList.add("rounded-t-lg");
+ miniMap.classList.add("rounded-b-lg");
}
});
diff --git a/frontend/react/src/ui/panels/sidebar.tsx b/frontend/react/src/ui/panels/sidebar.tsx
index 0e87c1a0..b4b95783 100644
--- a/frontend/react/src/ui/panels/sidebar.tsx
+++ b/frontend/react/src/ui/panels/sidebar.tsx
@@ -32,6 +32,7 @@ export function SideBar() {
checked={appState === OlympusState.MAIN_MENU}
icon={faEllipsisV}
tooltip="Hide/show main menu"
+ tooltipPosition="side"
>
{
@@ -40,6 +41,7 @@ export function SideBar() {
checked={appState === OlympusState.SPAWN}
icon={faPlusSquare}
tooltip="Hide/show unit spawn menu"
+ tooltipPosition="side"
>
{
@@ -48,6 +50,7 @@ export function SideBar() {
checked={appState === OlympusState.UNIT_CONTROL}
icon={faGamepad}
tooltip="Hide/show selection tool and unit control menu"
+ tooltipPosition="side"
>
{
@@ -56,6 +59,7 @@ export function SideBar() {
checked={appState === OlympusState.DRAW}
icon={faPencil}
tooltip="Hide/show drawing menu"
+ tooltipPosition="side"
>
{
@@ -64,6 +68,7 @@ export function SideBar() {
checked={appState === OlympusState.AUDIO}
icon={faVolumeHigh}
tooltip="Hide/show audio menu"
+ tooltipPosition="side"
>
{/*} {
@@ -80,6 +85,7 @@ export function SideBar() {
checked={appState === OlympusState.AWACS}
icon={faA}
tooltip="Hide/show AWACS menu"
+ tooltipPosition="side"
>
{
@@ -88,6 +94,7 @@ export function SideBar() {
checked={appState === OlympusState.GAME_MASTER}
icon={faCrown}
tooltip="Hide/show Game Master menu"
+ tooltipPosition="side"
>
@@ -98,6 +105,7 @@ export function SideBar() {
checked={false}
icon={faQuestionCircle}
tooltip="Open user guide on separate window"
+ tooltipPosition="side"
>
{
@@ -106,6 +114,7 @@ export function SideBar() {
checked={appState === OlympusState.OPTIONS}
icon={faCog}
tooltip="Hide/show settings menu"
+ tooltipPosition="side"
>
diff --git a/frontend/react/src/ui/panels/unitcontrolbar.tsx b/frontend/react/src/ui/panels/unitcontrolbar.tsx
deleted file mode 100644
index 43bd83fa..00000000
--- a/frontend/react/src/ui/panels/unitcontrolbar.tsx
+++ /dev/null
@@ -1,137 +0,0 @@
-import React, { useEffect, useRef, useState } from "react";
-import { ContextActionSet } from "../../unit/contextactionset";
-import { OlStateButton } from "../components/olstatebutton";
-import { getApp } from "../../olympusapp";
-import { ContextAction } from "../../unit/contextaction";
-import { CONTEXT_ACTION_COLORS, ContextActionTarget, MAP_OPTIONS_DEFAULTS } from "../../constants/constants";
-import { FaChevronDown,FaChevronUp } from "react-icons/fa6";
-import { OlympusState } from "../../constants/constants";
-import { AppStateChangedEvent, ContextActionChangedEvent, ContextActionSetChangedEvent, MapOptionsChangedEvent } from "../../events";
-import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
-
-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 [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);
- useEffect(() => {
- if (scrollRef.current) onScroll(scrollRef.current);
- });
-
- useEffect(() => {
- AppStateChangedEvent.on((state, subState) => setAppState(state));
- ContextActionSetChangedEvent.on((contextActionSet) => setcontextActionSet(contextActionSet));
- ContextActionChangedEvent.on((contextAction) => setContextAction(contextAction));
- MapOptionsChangedEvent.on((mapOptions) => setMapOptions({ ...mapOptions }));
- }, []);
-
- function onScroll(el) {
- const sl = el.scrollTop;
- const sr = el.scrollHeight - el.scrollTop - el.clientHeight;
-
- sl < 1 && !scrolledTop && setScrolledTop(true);
- sl > 1 && scrolledTop && setScrolledTop(false);
-
- sr < 1 && !scrolledBottom && setScrolledBottom(true);
- sr > 1 && scrolledBottom && setScrolledBottom(false);
- }
-
- let reorderedActions: ContextAction[] = contextActionSet
- ? Object.values(contextActionSet.getContextActions()).sort((a: ContextAction, b: ContextAction) => (a.getOptions().type < b.getOptions().type ? -1 : 1))
- : [];
-
- return (
- <>
- {appState === OlympusState.UNIT_CONTROL && contextActionSet && Object.keys(contextActionSet.getContextActions()).length > 0 && (
- <>
- {mapOptions.tabletMode && (
- <>
-
- {!scrolledTop && (
-
- )}
-
onScroll(ev.target)} ref={scrollRef}>
- {reorderedActions.map((contextActionIt: ContextAction) => {
- return (
-
-
{
- if (contextActionIt.getTarget() === ContextActionTarget.NONE) {
- contextActionIt.executeCallback(null, null);
- } else {
- contextActionIt !== contextAction
- ? getApp().getMap().setContextAction(contextActionIt)
- : getApp().getMap().setContextAction(null);
- }
- }}
- />
-
- );
- })}
-
- {!scrolledBottom && (
-
- )}
-
- >
- )}
-
- {contextAction && (
-
-
-
- {contextAction.getDescription()}
-
-
- )}
- >
- )}
- >
- );
-}
diff --git a/frontend/react/src/ui/ui.tsx b/frontend/react/src/ui/ui.tsx
index c8b28985..7f382287 100644
--- a/frontend/react/src/ui/ui.tsx
+++ b/frontend/react/src/ui/ui.tsx
@@ -13,7 +13,7 @@ import { getApp, setupApp } from "../olympusapp";
import { LoginModal } from "./modals/loginmodal";
import { MiniMapPanel } from "./panels/minimappanel";
-import { UnitControlBar } from "./panels/unitcontrolbar";
+import { MapToolBar } from "./panels/maptoolbar";
import { DrawingMenu } from "./panels/drawingmenu";
import { ControlsPanel } from "./panels/controlspanel";
import { MapContextMenu } from "./contextmenus/mapcontextmenu";
@@ -105,7 +105,6 @@ export function UI() {
-
diff --git a/frontend/react/src/unit/unit.ts b/frontend/react/src/unit/unit.ts
index 0f30d1f6..60fa5731 100644
--- a/frontend/react/src/unit/unit.ts
+++ b/frontend/react/src/unit/unit.ts
@@ -836,6 +836,9 @@ export abstract class Unit extends CustomMarker {
contextActionSet.addContextAction(this, ContextActions.PATH);
contextActionSet.addContextAction(this, ContextActions.DELETE);
contextActionSet.addContextAction(this, ContextActions.EXPLODE);
+ contextActionSet.addContextAction(this, ContextActions.CENTER_MAP);
+ contextActionSet.addContextAction(this, ContextActions.CLONE);
+ contextActionSet.addContextAction(this, ContextActions.ATTACK);
contextActionSet.addDefaultContextAction(this, ContextActions.MOVE);
}
@@ -1357,16 +1360,18 @@ export abstract class Unit extends CustomMarker {
this.#debounceTimeout = window.setTimeout(() => {
console.log(`Left short click on ${this.getUnitName()}`);
- if (!e.originalEvent.ctrlKey) getApp().getUnitsManager().deselectAllUnits();
- this.setSelected(!this.getSelected());
+ if (getApp().getState() === OlympusState.UNIT_CONTROL && getApp().getMap().getContextAction()) {
+ if (getApp().getMap().getContextAction()?.getTarget() === ContextActionTarget.UNIT) getApp().getMap().executeContextAction(this, null, e.originalEvent);
+ else getApp().getMap().executeContextAction(null, this.getPosition(), e.originalEvent);
+ } else {
+ if (!e.originalEvent.ctrlKey) getApp().getUnitsManager().deselectAllUnits();
+ this.setSelected(!this.getSelected());
+ }
}, SHORT_PRESS_MILLISECONDS);
}
#onRightShortClick(e: any) {
console.log(`Right short click on ${this.getUnitName()}`);
-
- if (getApp().getState() === OlympusState.UNIT_CONTROL && getApp().getMap().getContextAction()?.getTarget() === ContextActionTarget.UNIT)
- getApp().getMap().executeContextAction(this, null, e.originalEvent);
}
#onRightLongClick(e: any) {
@@ -1848,7 +1853,7 @@ export abstract class AirUnit extends Unit {
showAmmo: belongsToCommandedCoalition,
showSummary: belongsToCommandedCoalition || this.getDetectionMethods().some((value) => [VISUAL, OPTIC, RADAR, IRST, DLINK].includes(value)),
showCallsign: belongsToCommandedCoalition && (!getApp().getMap().getOptions().AWACSMode || this.getHuman()),
- rotateToHeading: false
+ rotateToHeading: false,
} as ObjectIconOptions;
}
@@ -1857,10 +1862,8 @@ export abstract class AirUnit extends Unit {
/* Context actions to be executed immediately */
contextActionSet.addContextAction(this, ContextActions.REFUEL);
- contextActionSet.addContextAction(this, ContextActions.CENTER_MAP);
/* Context actions that require a target unit */
- contextActionSet.addContextAction(this, ContextActions.ATTACK);
contextActionSet.addContextAction(this, ContextActions.FOLLOW);
contextActionSet.addContextAction(this, ContextActions.SET_AWACS_REFERENCE);
@@ -1946,10 +1949,6 @@ export class GroundUnit extends Unit {
/* Context actions to be executed immediately */
contextActionSet.addContextAction(this, ContextActions.GROUP);
- contextActionSet.addContextAction(this, ContextActions.CENTER_MAP);
-
- /* Context actions that require a target unit */
- contextActionSet.addContextAction(this, ContextActions.ATTACK);
/* Context actions that require a target position */
if (this.canTargetPoint()) {
@@ -2015,10 +2014,6 @@ export class NavyUnit extends Unit {
/* Context actions to be executed immediately */
contextActionSet.addContextAction(this, ContextActions.GROUP);
- contextActionSet.addContextAction(this, ContextActions.CENTER_MAP);
-
- /* Context actions that require a target unit */
- contextActionSet.addContextAction(this, ContextActions.ATTACK);
/* Context actions that require a target position */
contextActionSet.addContextAction(this, ContextActions.FIRE_AT_AREA);
diff --git a/frontend/react/src/unit/unitsmanager.ts b/frontend/react/src/unit/unitsmanager.ts
index 3d797309..bba9d958 100644
--- a/frontend/react/src/unit/unitsmanager.ts
+++ b/frontend/react/src/unit/unitsmanager.ts
@@ -17,6 +17,7 @@ import {
AWACSReferenceChangedEvent,
CommandModeOptionsChangedEvent,
ContactsUpdatedEvent,
+ CopiedUnitsEvents,
HotgroupsChangedEvent,
SelectedUnitsChangedEvent,
SelectionClearedEvent,
@@ -1234,6 +1235,8 @@ export class UnitsManager {
)
); /* Can be applied to humans too */
getApp().addInfoMessage(`${this.#copiedUnits.length} units copied`);
+
+ CopiedUnitsEvents.dispatch(this.#copiedUnits)
}
/*********************** Unit manipulation functions ************************/
@@ -1241,7 +1244,7 @@ export class UnitsManager {
*
* @returns True if units were pasted successfully
*/
- paste() {
+ paste(location?: LatLng) {
let spawnPoints = 0;
/* If spawns are restricted, check that the user has the necessary spawn points */
@@ -1285,7 +1288,10 @@ export class UnitsManager {
var units: { ID: number; location: LatLng }[] = [];
let markers: TemporaryUnitMarker[] = [];
groups[groupName].forEach((unit: UnitData) => {
- var position = new LatLng(
+ var position = location ? new LatLng(
+ location.lat + unit.position.lat - avgLat,
+ location.lng + unit.position.lng - avgLng
+ ) : new LatLng(
getApp().getMap().getMouseCoordinates().lat + unit.position.lat - avgLat,
getApp().getMap().getMouseCoordinates().lng + unit.position.lng - avgLng
);