diff --git a/frontend/react/src/audio/audiounitpipeline.ts b/frontend/react/src/audio/audiounitpipeline.ts
index 5702b99e..3954a3a0 100644
--- a/frontend/react/src/audio/audiounitpipeline.ts
+++ b/frontend/react/src/audio/audiounitpipeline.ts
@@ -80,7 +80,9 @@ export class AudioUnitPipeline {
/* Don't bother updating parameters if the client is too far away */
if (this.#distance < this.#maxDistance) {
/* Compute a new gain decreasing with distance. */
- let newGain = 1.0 - Math.pow(this.#distance / this.#maxDistance, 1); // Arbitrary
+ //let newGain = 1.0 - Math.pow(this.#distance / this.#maxDistance, 1); // Arbitrary
+
+ let newGain = Math.min( 1, 20 / this.#distance )
/* Set the values of the main gain node and the multitap gain node, used for reverb effect */
this.#gainNode.gain.setValueAtTime(newGain, getApp().getAudioManager().getAudioContext().currentTime);
diff --git a/frontend/react/src/constants/constants.ts b/frontend/react/src/constants/constants.ts
index dac06c3a..6a5bafb5 100644
--- a/frontend/react/src/constants/constants.ts
+++ b/frontend/react/src/constants/constants.ts
@@ -1,6 +1,29 @@
import { LatLng, LatLngBounds } from "leaflet";
import { MapOptions } from "../types/types";
import { CommandModeOptions } from "../interfaces";
+import { ContextAction } from "../unit/contextaction";
+import {
+ faExplosion,
+ faHand,
+ faLocationCrosshairs,
+ faLocationDot,
+ faMapLocation,
+ faPeopleGroup,
+ faPlaneArrival,
+ faRoute,
+ faTrash,
+ faXmarksLines,
+} from "@fortawesome/free-solid-svg-icons";
+import { Unit } from "../unit/unit";
+import { getApp } from "../olympusapp";
+import {
+ olButtonsContextAttack,
+ olButtonsContextFollow,
+ olButtonsContextLandAtPoint,
+ olButtonsContextRefuel,
+ olButtonsContextSimulateFireFight,
+} from "../ui/components/olicons";
+import { FormationCreationRequestEvent, UnitExplosionRequestEvent } from "../events";
export const UNITS_URI = "units";
export const WEAPONS_URI = "weapons";
@@ -308,7 +331,7 @@ export const MAP_OPTIONS_DEFAULTS = {
fillSelectedRing: false,
showMinimap: false,
protectDCSUnits: true,
- keepRelativePositions: true
+ keepRelativePositions: true,
} as MapOptions;
export const MAP_HIDDEN_TYPES_DEFAULTS = {
@@ -400,6 +423,12 @@ export enum AudioMessageType {
settings,
}
+export enum ContextActionTarget {
+ NONE,
+ UNIT,
+ POINT,
+}
+
export enum ContextActionType {
NO_COLOR,
MOVE,
@@ -408,6 +437,238 @@ export enum ContextActionType {
ENGAGE,
DELETE,
}
+
export const CONTEXT_ACTION_COLORS = [null, "white", "green", "purple", "blue", "red"];
+export namespace ContextActions {
+ export const STOP = new ContextAction(
+ "stop",
+ "Stop unit",
+ "Stops the unit",
+ faHand,
+ ContextActionTarget.NONE,
+ (units: Unit[], _1, _2) => {
+ getApp().getUnitsManager().stop(units);
+ },
+ {
+ executeImmediately: true,
+ type: ContextActionType.MOVE,
+ hotkey: "KeyZ",
+ }
+ );
+ export const MOVE = new ContextAction(
+ "move",
+ "Set destination",
+ "Click on the map to move the units there",
+ faLocationDot,
+ ContextActionTarget.POINT,
+ (units: Unit[], _, targetPosition, originalEvent) => {
+ if (!originalEvent?.ctrlKey) getApp().getUnitsManager().clearDestinations(units);
+ if (targetPosition)
+ getApp()
+ .getUnitsManager()
+ .addDestination(targetPosition, getApp().getMap().getOptions().keepRelativePositions, getApp().getMap().getDestinationRotation(), units);
+ },
+ { type: ContextActionType.MOVE, hotkey: "KeyX" }
+ );
+
+ export const PATH = new ContextAction(
+ "path",
+ "Create route",
+ "Click on the map to add a destination to the path",
+ faRoute,
+ ContextActionTarget.POINT,
+ (units: Unit[], _, targetPosition) => {
+ if (targetPosition)
+ getApp()
+ .getUnitsManager()
+ .addDestination(targetPosition, getApp().getMap().getOptions().keepRelativePositions, getApp().getMap().getDestinationRotation(), units);
+ },
+ { type: ContextActionType.MOVE, hotkey: "KeyC" }
+ );
+
+ export const DELETE = new ContextAction(
+ "delete",
+ "Delete unit",
+ "Deletes the unit",
+ faTrash,
+ ContextActionTarget.NONE,
+ (units: Unit[], _1, _2) => {
+ getApp().getUnitsManager().delete(false);
+ },
+ {
+ executeImmediately: true,
+ type: ContextActionType.DELETE,
+ }
+ );
+
+ export const EXPLODE = new ContextAction(
+ "explode",
+ "Explode unit",
+ "Explodes the unit",
+ faExplosion,
+ ContextActionTarget.NONE,
+ (units: Unit[], _1, _2) => {
+ getApp().setState(OlympusState.UNIT_CONTROL, UnitControlSubState.UNIT_EXPLOSION_MENU);
+ UnitExplosionRequestEvent.dispatch(units);
+ },
+ {
+ executeImmediately: true,
+ type: ContextActionType.DELETE,
+ }
+ );
+
+ export const CENTER_MAP = new ContextAction(
+ "center-map",
+ "Center map",
+ "Center the map on the unit and follow it",
+ faMapLocation,
+ ContextActionTarget.NONE,
+ (units: Unit[]) => {
+ getApp().getMap().centerOnUnit(units[0]);
+ },
+ { executeImmediately: true, type: ContextActionType.OTHER }
+ );
+
+ export const REFUEL = new ContextAction(
+ "refuel",
+ "Refuel at tanker",
+ "Refuel units at the nearest AAR Tanker. If no tanker is available the unit will RTB",
+ olButtonsContextRefuel,
+ ContextActionTarget.NONE,
+ (units: Unit[]) => {
+ getApp().getUnitsManager().refuel(units);
+ },
+ { executeImmediately: true, type: ContextActionType.ADMIN }
+ );
+
+ export const FOLLOW = new ContextAction(
+ "follow",
+ "Follow unit",
+ "Click on a unit to follow it in formation",
+ olButtonsContextFollow,
+ ContextActionTarget.UNIT,
+ (units: Unit[], targetUnit: Unit | null, _) => {
+ if (targetUnit) {
+ getApp().setState(OlympusState.UNIT_CONTROL, UnitControlSubState.FORMATION);
+ FormationCreationRequestEvent.dispatch(
+ targetUnit,
+ units.filter((unit) => unit !== targetUnit)
+ );
+ }
+ },
+ { type: ContextActionType.ADMIN }
+ );
+
+ export const BOMB = new ContextAction(
+ "bomb",
+ "Precision bomb location",
+ "Click on a point to execute a precision bombing attack",
+ faLocationCrosshairs,
+ ContextActionTarget.POINT,
+ (units: Unit[], _, targetPosition: LatLng | null) => {
+ if (targetPosition)
+ getApp()
+ .getUnitsManager()
+ .bombPoint(targetPosition, getApp().getMap().getOptions().keepRelativePositions, getApp().getMap().getDestinationRotation(), units);
+ },
+ { type: ContextActionType.ENGAGE }
+ );
+
+ export const CARPET_BOMB = new ContextAction(
+ "carpet-bomb",
+ "Carpet bomb location",
+ "Click on a point to execute a carpet bombing attack",
+ faXmarksLines,
+ ContextActionTarget.POINT,
+ (units: Unit[], _, targetPosition: LatLng | null) => {
+ if (targetPosition)
+ getApp()
+ .getUnitsManager()
+ .carpetBomb(targetPosition, getApp().getMap().getOptions().keepRelativePositions, getApp().getMap().getDestinationRotation(), units);
+ },
+ { type: ContextActionType.ENGAGE }
+ );
+
+ export const LAND = new ContextAction(
+ "land",
+ "Land",
+ "Click on a point to land at the nearest airbase",
+ faPlaneArrival,
+ ContextActionTarget.POINT,
+ (units: Unit[], _, targetPosition: LatLng | null) => {
+ if (targetPosition) getApp().getUnitsManager().landAt(targetPosition, units);
+ },
+ { type: ContextActionType.ADMIN }
+ );
+
+ export const LAND_AT_POINT = new ContextAction(
+ "land-at-point",
+ "Land at location",
+ "Click on a point to land there",
+ olButtonsContextLandAtPoint,
+ ContextActionTarget.POINT,
+ (units: Unit[], _, targetPosition: LatLng | null) => {
+ if (targetPosition)
+ getApp()
+ .getUnitsManager()
+ .landAtPoint(targetPosition, getApp().getMap().getOptions().keepRelativePositions, getApp().getMap().getDestinationRotation(), units);
+ },
+ { type: ContextActionType.ADMIN }
+ );
+
+ export const GROUP = new ContextAction(
+ "group-ground",
+ "Group ground units",
+ "Create a group of ground units",
+ faPeopleGroup,
+ ContextActionTarget.NONE,
+ (units: Unit[], _1, _2) => {
+ getApp().getUnitsManager().createGroup(units);
+ },
+ { executeImmediately: true, type: ContextActionType.OTHER }
+ );
+
+ export const ATTACK = new ContextAction(
+ "attack",
+ "Attack unit",
+ "Click on a unit to attack it",
+ olButtonsContextAttack,
+ ContextActionTarget.UNIT,
+ (units: Unit[], targetUnit: Unit | null, _) => {
+ if (targetUnit) getApp().getUnitsManager().attackUnit(targetUnit.ID, units);
+ },
+ { type: ContextActionType.ENGAGE }
+ );
+
+ export const FIRE_AT_AREA = new ContextAction(
+ "fire-at-area",
+ "Fire at area",
+ "Click on a point to precisely fire at it (if possible)",
+ faLocationCrosshairs,
+ ContextActionTarget.POINT,
+ (units: Unit[], _, targetPosition: LatLng | null) => {
+ if (targetPosition)
+ getApp()
+ .getUnitsManager()
+ .fireAtArea(targetPosition, getApp().getMap().getOptions().keepRelativePositions, getApp().getMap().getDestinationRotation(), units);
+ },
+ { type: ContextActionType.ENGAGE }
+ );
+
+ export const SIMULATE_FIRE_FIGHT = new ContextAction(
+ "simulate-fire-fight",
+ "Simulate fire fight",
+ "Simulate a fire fight by shooting randomly in a certain large area. WARNING: works correctly only on neutral units, blue or red units will aim",
+ olButtonsContextSimulateFireFight,
+ ContextActionTarget.POINT,
+ (units: Unit[], _, targetPosition: LatLng | null) => {
+ if (targetPosition)
+ getApp()
+ .getUnitsManager()
+ .simulateFireFight(targetPosition, getApp().getMap().getOptions().keepRelativePositions, getApp().getMap().getDestinationRotation(), units);
+ },
+ { type: ContextActionType.ADMIN }
+ );
+}
diff --git a/frontend/react/src/map/map.ts b/frontend/react/src/map/map.ts
index 9de812ff..eaca1940 100644
--- a/frontend/react/src/map/map.ts
+++ b/frontend/react/src/map/map.ts
@@ -20,6 +20,8 @@ import {
DrawSubState,
JTACSubState,
UnitControlSubState,
+ ContextActionTarget,
+ ContextActionType,
} from "../constants/constants";
import { CoalitionPolygon } from "./coalitionarea/coalitionpolygon";
import { MapHiddenTypes, MapOptions } from "../types/types";
@@ -931,7 +933,7 @@ export class Map extends L.Map {
return;
}
- if (e.originalEvent.button === 2) this.#isRotatingDestination = true;
+ if (this.#contextAction?.getTarget() === ContextActionTarget.POINT && e.originalEvent.button === 2) this.#isRotatingDestination = true;
this.scrollWheelZoom.disable();
this.#shortPressTimer = window.setTimeout(() => {
@@ -1100,10 +1102,12 @@ export class Map extends L.Map {
else document.dispatchEvent(new CustomEvent("forceboxselect", { detail: e.originalEvent }));
} else if (getApp().getState() === OlympusState.UNIT_CONTROL) {
if (e.originalEvent.button === 2) {
- getApp().setState(OlympusState.UNIT_CONTROL, UnitControlSubState.MAP_CONTEXT_MENU);
- MapContextMenuRequestEvent.dispatch(pressLocation);
+ if (!getApp().getMap().getContextAction()) {
+ getApp().setState(OlympusState.UNIT_CONTROL, UnitControlSubState.MAP_CONTEXT_MENU);
+ MapContextMenuRequestEvent.dispatch(pressLocation);
+ }
} else {
- if (this.#contextAction?.getTarget() === "position") {
+ if (this.#contextAction?.getTarget() === ContextActionTarget.POINT) {
this.dragging.disable();
this.#isRotatingDestination = true;
} else {
@@ -1259,9 +1263,9 @@ export class Map extends L.Map {
});
selectedUnits.forEach((unit) => {
- if (["move", "path", "land-at-point"].includes(this.#contextAction?.getId() ?? "")) {
+ if (this.#contextAction?.getOptions().type === ContextActionType.MOVE) {
this.#destinationPreviewMarkers[unit.ID] = new TemporaryUnitMarker(new L.LatLng(0, 0), unit.getName(), unit.getCoalition());
- } else if (this.#contextAction?.getTarget() === "position" && this.#contextAction?.getId() !== "land") {
+ } else if (this.#contextAction?.getTarget() === ContextActionTarget.POINT) {
this.#destinationPreviewMarkers[unit.ID] = new TargetMarker(new L.LatLng(0, 0));
}
this.#destinationPreviewMarkers[unit.ID]?.addTo(this);
diff --git a/frontend/react/src/shortcut/shortcutmanager.ts b/frontend/react/src/shortcut/shortcutmanager.ts
index 16046c58..07c882fb 100644
--- a/frontend/react/src/shortcut/shortcutmanager.ts
+++ b/frontend/react/src/shortcut/shortcutmanager.ts
@@ -1,3 +1,4 @@
+import { ContextActions, OlympusState } from "../constants/constants";
import { ShortcutKeyboardOptions, ShortcutMouseOptions } from "../interfaces";
import { getApp } from "../olympusapp";
import { ShortcutKeyboard, ShortcutMouse } from "./shortcut";
@@ -117,6 +118,26 @@ export class ShortcutManager {
shiftKey: false,
});
+ for (let contextActionName in ContextActions) {
+ if (ContextActions[contextActionName].getOptions().hotkey) {
+ this.addKeyboardShortcut(`${contextActionName}Hotkey`, {
+ code: ContextActions[contextActionName].getOptions().hotkey,
+ shiftKey: true,
+ callback: () => {
+ const contextActionSet = getApp().getMap().getContextActionSet();
+ if (
+ getApp().getState() === OlympusState.UNIT_CONTROL &&
+ contextActionSet &&
+ ContextActions[contextActionName].getId() in contextActionSet.getContextActions()
+ ) {
+ if (ContextActions[contextActionName].getOptions().executeImmediately) ContextActions[contextActionName].executeCallback();
+ else getApp().getMap().setContextAction(ContextActions[contextActionName]);
+ }
+ },
+ });
+ }
+ }
+
let PTTKeys = ["KeyZ", "KeyX", "KeyC", "KeyV", "KeyB", "KeyN", "KeyM", "KeyK", "KeyL"];
PTTKeys.forEach((key, idx) => {
this.addKeyboardShortcut(`PTT${idx}Active`, {
diff --git a/frontend/react/src/ui/contextmenus/mapcontextmenu.tsx b/frontend/react/src/ui/contextmenus/mapcontextmenu.tsx
index 1be88d06..407b8b37 100644
--- a/frontend/react/src/ui/contextmenus/mapcontextmenu.tsx
+++ b/frontend/react/src/ui/contextmenus/mapcontextmenu.tsx
@@ -1,7 +1,7 @@
import React, { useEffect, useRef, useState } from "react";
import { Unit } from "../../unit/unit";
import { ContextAction } from "../../unit/contextaction";
-import { CONTEXT_ACTION_COLORS, NO_SUBSTATE, OlympusState, OlympusSubState, UnitControlSubState } from "../../constants/constants";
+import { CONTEXT_ACTION_COLORS, ContextActionTarget, NO_SUBSTATE, OlympusState, OlympusSubState, UnitControlSubState } from "../../constants/constants";
import { OlDropdownItem } from "../components/oldropdown";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { LatLng } from "leaflet";
@@ -69,9 +69,9 @@ export function MapContextMenu(props: {}) {
});
let reorderedActions: ContextAction[] = contextActionSet
- ? Object.values(contextActionSet.getContextActions(appSubState === UnitControlSubState.MAP_CONTEXT_MENU ? "position" : "unit")).sort(
- (a: ContextAction, b: ContextAction) => (a.getOptions().type < b.getOptions().type ? -1 : 1)
- )
+ ? Object.values(
+ contextActionSet.getContextActions(appSubState === UnitControlSubState.MAP_CONTEXT_MENU ? ContextActionTarget.POINT : ContextActionTarget.UNIT)
+ ).sort((a: ContextAction, b: ContextAction) => (a.getOptions().type < b.getOptions().type ? -1 : 1))
: [];
return (
@@ -107,13 +107,13 @@ export function MapContextMenu(props: {}) {
if (contextActionIt.getOptions().executeImmediately) {
contextActionIt.executeCallback(null, null);
} else {
- if (appSubState === UnitControlSubState.MAP_CONTEXT_MENU ) {
+ if (appSubState === UnitControlSubState.MAP_CONTEXT_MENU) {
contextActionIt.executeCallback(null, latLng);
} else if (unit !== null) {
contextActionIt.executeCallback(unit, null);
}
}
- getApp().setState(OlympusState.UNIT_CONTROL)
+ getApp().setState(OlympusState.UNIT_CONTROL);
}}
>