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 (
@@ -26,15 +26,17 @@ export function OlCoalitionToggle(props: { coalition: Coalition | undefined; onC
rtl:data-[coalition='red']:after:-translate-x-full
`}
>
-
- {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}
+ />
+ );
+ })}
+
+ >
+ )}
+
{
+ if (props.effect === "explosion") {
+ if (explosionType === "High explosive") getApp().getServerManager().spawnExplosion(50, "normal", props.latlng);
+ else if (explosionType === "Napalm") getApp().getServerManager().spawnExplosion(50, "napalm", props.latlng);
+ else if (explosionType === "White phosphorous") getApp().getServerManager().spawnExplosion(50, "phosphorous", props.latlng);
+ getApp().getMap().addExplosionMarker(props.latlng);
+ } else if (props.effect === "smoke") {
+ getApp().getServerManager().spawnSmoke(smokeColor, props.latlng);
+ getApp()
+ .getMap()
+ .addSmokeMarker(props.latlng, smokeColor ?? "white");
+ }
+
+ getApp().setState(OlympusState.IDLE);
+ }}
+ >
+ Spawn
+
+
+ );
+}
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) && (
+
{
+ if (spawnRequestTable)
+ getApp()
+ .getUnitsManager()
+ .spawnUnits(spawnRequestTable.category, Array(spawnRequestTable.amount).fill(spawnRequestTable.unit), spawnRequestTable.coalition, false, props.airbase?.getName() ?? undefined);
+
+ getApp().setState(OlympusState.IDLE)
+ }}
+ >
+ Spawn
+
+ )}
+
+ );
+}
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 && (
Spawn
diff --git a/frontend/react/src/ui/ui.tsx b/frontend/react/src/ui/ui.tsx
index 968e3f2d..0665481b 100644
--- a/frontend/react/src/ui/ui.tsx
+++ b/frontend/react/src/ui/ui.tsx
@@ -28,7 +28,7 @@ import { AppStateChangedEvent, MapOptionsChangedEvent } from "../events";
import { GameMasterMenu } from "./panels/gamemastermenu";
import { InfoBar } from "./panels/infobar";
import { HotGroupBar } from "./panels/hotgroupsbar";
-import { StarredSpawnContextMenu } from "./contextmenus/starredspawncontextmenu";
+import { SpawnContextMenu } from "./contextmenus/SpawnContextmenu";
import { CoordinatesPanel } from "./panels/coordinatespanel";
export type OlympusUIState = {
@@ -112,7 +112,7 @@ export function UI() {
-
+
);
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: () => {