Major controls rework

This commit is contained in:
Davide Passoni 2024-11-21 17:35:51 +01:00
parent 73c8ce2fe7
commit 402a1457aa
25 changed files with 1001 additions and 506 deletions

View File

@ -51,7 +51,9 @@ export class AudioManager {
keyDownCallback: () => this.getSinks()[idx]?.setPtt(true),
keyUpCallback: () => this.getSinks()[idx]?.setPtt(false),
code: key,
shiftKey: true
shiftKey: true,
ctrlKey: false,
altKey: false
});
});
}

View File

@ -25,6 +25,10 @@ import {
} from "../ui/components/olicons";
import { FormationCreationRequestEvent, UnitExplosionRequestEvent } from "../events";
export const SELECT_TOLERANCE_PX = 5;
export const SHORT_PRESS_MILLISECONDS = 200;
export const DEBOUNCE_MILLISECONDS = 200;
export const UNITS_URI = "units";
export const WEAPONS_URI = "weapons";
export const LOGS_URI = "logs";
@ -73,8 +77,8 @@ export enum ERAS_ORDER {
"Early Cold War",
"Mid Cold War",
"Late Cold War",
"Modern"
};
"Modern",
}
export const ROEDescriptions: string[] = [
"Free (Attack anyone)",
@ -331,12 +335,11 @@ export const MAP_OPTIONS_DEFAULTS: MapOptions = {
fillSelectedRing: false,
showMinimap: false,
protectDCSUnits: true,
keepRelativePositions: true,
cameraPluginPort: 3003,
cameraPluginRatio: 1,
cameraPluginEnabled: false,
cameraPluginMode: "map",
tabletMode: false
tabletMode: false,
};
export const MAP_HIDDEN_TYPES_DEFAULTS = {
@ -473,7 +476,7 @@ export namespace ContextActions {
if (targetPosition)
getApp()
.getUnitsManager()
.addDestination(targetPosition, getApp().getMap().getOptions().keepRelativePositions, getApp().getMap().getDestinationRotation(), units);
.addDestination(targetPosition, getApp().getMap().getKeepRelativePositions(), getApp().getMap().getDestinationRotation(), units);
},
{ type: ContextActionType.MOVE, code: null }
);
@ -488,9 +491,9 @@ export namespace ContextActions {
if (targetPosition)
getApp()
.getUnitsManager()
.addDestination(targetPosition, getApp().getMap().getOptions().keepRelativePositions, getApp().getMap().getDestinationRotation(), units);
.addDestination(targetPosition, getApp().getMap().getKeepRelativePositions(), getApp().getMap().getDestinationRotation(), units);
},
{ type: ContextActionType.MOVE, code: "ControlLeft" }
{ type: ContextActionType.MOVE, code: "ControlLeft", shiftKey: false }
);
export const DELETE = new ContextAction(
@ -505,7 +508,10 @@ export namespace ContextActions {
{
executeImmediately: true,
type: ContextActionType.DELETE,
code: "Delete"
code: "Delete",
ctrlKey: false,
shiftKey: false,
altKey: false,
}
);
@ -523,7 +529,9 @@ export namespace ContextActions {
executeImmediately: true,
type: ContextActionType.DELETE,
code: "Delete",
ctrlKey: true
ctrlKey: true,
shiftKey: false,
altKey: false,
}
);
@ -536,7 +544,7 @@ export namespace ContextActions {
(units: Unit[]) => {
getApp().getMap().centerOnUnit(units[0]);
},
{ executeImmediately: true, type: ContextActionType.OTHER, code: "KeyM", altKey: true }
{ executeImmediately: true, type: ContextActionType.OTHER, code: "KeyM", ctrlKey: false, shiftKey: false, altKey: false }
);
export const REFUEL = new ContextAction(
@ -548,7 +556,7 @@ export namespace ContextActions {
(units: Unit[]) => {
getApp().getUnitsManager().refuel(units);
},
{ executeImmediately: true, type: ContextActionType.ADMIN, code: "KeyV" }
{ executeImmediately: true, type: ContextActionType.ADMIN, code: "KeyR", ctrlKey: false, shiftKey: false, altKey: false }
);
export const FOLLOW = new ContextAction(
@ -566,7 +574,7 @@ export namespace ContextActions {
);
}
},
{ type: ContextActionType.ADMIN, code: "KeyF" }
{ type: ContextActionType.ADMIN, code: "KeyF", ctrlKey: false, shiftKey: false, altKey: false }
);
export const BOMB = new ContextAction(
@ -579,9 +587,9 @@ export namespace ContextActions {
if (targetPosition)
getApp()
.getUnitsManager()
.bombPoint(targetPosition, getApp().getMap().getOptions().keepRelativePositions, getApp().getMap().getDestinationRotation(), units);
.bombPoint(targetPosition, getApp().getMap().getKeepRelativePositions(), getApp().getMap().getDestinationRotation(), units);
},
{ type: ContextActionType.ENGAGE, code: "KeyB" }
{ type: ContextActionType.ENGAGE, code: "KeyB", ctrlKey: false, shiftKey: false }
);
export const CARPET_BOMB = new ContextAction(
@ -594,9 +602,9 @@ export namespace ContextActions {
if (targetPosition)
getApp()
.getUnitsManager()
.carpetBomb(targetPosition, getApp().getMap().getOptions().keepRelativePositions, getApp().getMap().getDestinationRotation(), units);
.carpetBomb(targetPosition, getApp().getMap().getKeepRelativePositions(), getApp().getMap().getDestinationRotation(), units);
},
{ type: ContextActionType.ENGAGE, code: "KeyB", altKey: true }
{ type: ContextActionType.ENGAGE, code: "KeyC", ctrlKey: false, shiftKey: false }
);
export const LAND = new ContextAction(
@ -608,7 +616,7 @@ export namespace ContextActions {
(units: Unit[], _, targetPosition: LatLng | null) => {
if (targetPosition) getApp().getUnitsManager().landAt(targetPosition, units);
},
{ type: ContextActionType.ADMIN, code: "KeyL" }
{ type: ContextActionType.ADMIN, code: "KeyL", ctrlKey: false, shiftKey: false }
);
export const LAND_AT_POINT = new ContextAction(
@ -621,9 +629,9 @@ export namespace ContextActions {
if (targetPosition)
getApp()
.getUnitsManager()
.landAtPoint(targetPosition, getApp().getMap().getOptions().keepRelativePositions, getApp().getMap().getDestinationRotation(), units);
.landAtPoint(targetPosition, getApp().getMap().getKeepRelativePositions(), getApp().getMap().getDestinationRotation(), units);
},
{ type: ContextActionType.ADMIN, code: "KeyL", altKey: true }
{ type: ContextActionType.ADMIN, code: "KeyK", ctrlKey: false, shiftKey: false }
);
export const GROUP = new ContextAction(
@ -635,7 +643,7 @@ export namespace ContextActions {
(units: Unit[], _1, _2) => {
getApp().getUnitsManager().createGroup(units);
},
{ executeImmediately: true, type: ContextActionType.OTHER, code: "KeyG" }
{ executeImmediately: true, type: ContextActionType.OTHER, code: "KeyG", ctrlKey: false, shiftKey: false, altKey: false }
);
export const ATTACK = new ContextAction(
@ -647,7 +655,7 @@ export namespace ContextActions {
(units: Unit[], targetUnit: Unit | null, _) => {
if (targetUnit) getApp().getUnitsManager().attackUnit(targetUnit.ID, units);
},
{ type: ContextActionType.ENGAGE, code: "KeyZ" }
{ type: ContextActionType.ENGAGE, code: "KeyZ", ctrlKey: false, shiftKey: false, altKey: false }
);
export const FIRE_AT_AREA = new ContextAction(
@ -660,9 +668,9 @@ export namespace ContextActions {
if (targetPosition)
getApp()
.getUnitsManager()
.fireAtArea(targetPosition, getApp().getMap().getOptions().keepRelativePositions, getApp().getMap().getDestinationRotation(), units);
.fireAtArea(targetPosition, getApp().getMap().getKeepRelativePositions(), getApp().getMap().getDestinationRotation(), units);
},
{ type: ContextActionType.ENGAGE, code: "KeyZ", altKey: true }
{ type: ContextActionType.ENGAGE, code: "KeyV", ctrlKey: false, shiftKey: false }
);
export const SIMULATE_FIRE_FIGHT = new ContextAction(
@ -675,8 +683,8 @@ export namespace ContextActions {
if (targetPosition)
getApp()
.getUnitsManager()
.simulateFireFight(targetPosition, getApp().getMap().getOptions().keepRelativePositions, getApp().getMap().getDestinationRotation(), units);
.simulateFireFight(targetPosition, getApp().getMap().getKeepRelativePositions(), getApp().getMap().getDestinationRotation(), units);
},
{ type: ContextActionType.ADMIN, code: "KeyX" }
{ type: ContextActionType.ADMIN, code: "KeyX", ctrlKey: false, shiftKey: false }
);
}

View File

@ -389,7 +389,7 @@ export class CommandModeOptionsChangedEvent {
export class AudioSourcesChangedEvent {
static on(callback: (audioSources: AudioSource[]) => void) {
document.addEventListener(this.name, (ev: CustomEventInit) => {
callback(ev.detail);
callback(ev.detail.audioSources);
});
}
@ -403,7 +403,7 @@ export class AudioSourcesChangedEvent {
export class AudioSinksChangedEvent {
static on(callback: (audioSinks: AudioSink[]) => void) {
document.addEventListener(this.name, (ev: CustomEventInit) => {
callback(ev.detail);
callback(ev.detail.audioSinks);
});
}

View File

@ -4,6 +4,7 @@ import { DomUtil } from "leaflet";
import { DomEvent } from "leaflet";
import { LatLngBounds } from "leaflet";
import { Bounds } from "leaflet";
import { SELECT_TOLERANCE_PX } from "../constants/constants";
export var BoxSelect = Handler.extend({
initialize: function (map) {
@ -11,24 +12,15 @@ export var BoxSelect = Handler.extend({
this._container = map.getContainer();
this._pane = map.getPanes().overlayPane;
this._resetStateTimeout = 0;
this._forceBoxSelect = false;
map.on("unload", this._destroy, this);
document.addEventListener("forceboxselect", (e) => {
this._forceBoxSelect = true;
const originalEvent = (e as CustomEvent).detail;
this._onMouseDown(originalEvent);
});
},
addHooks: function () {
DomEvent.on(this._container, "mousedown", this._onMouseDown, this);
DomEvent.on(this._container, "forceboxselect", this._onMouseDown, this);
},
removeHooks: function () {
DomEvent.off(this._container, "mousedown", this._onMouseDown, this);
DomEvent.off(this._container, "forceboxselect", this._onMouseDown, this);
},
moved: function () {
@ -53,8 +45,7 @@ export var BoxSelect = Handler.extend({
},
_onMouseDown: function (e: any) {
if ((e.which == 1 && e.button == 0 && (e.shiftKey || this._forceBoxSelect)) || (e.type === "touchstart" && this._forceBoxSelect)) {
this._map.fire("selectionstart");
if (e.which == 1 && e.button == 0) {
// Clear the deferred resetState if it hasn't executed yet, otherwise it
// will interrupt the interaction and orphan a box element in the container.
this._clearDeferredResetState();
@ -87,15 +78,23 @@ export var BoxSelect = Handler.extend({
},
_onMouseMove: function (e: any) {
if (e.type === "touchmove") this._point = this._map.mouseEventToContainerPoint(e.touches[0]);
else this._point = this._map.mouseEventToContainerPoint(e);
if (
Math.abs(this._startPoint.x - this._point.x) < SELECT_TOLERANCE_PX &&
Math.abs(this._startPoint.y - this._point.y) < SELECT_TOLERANCE_PX &&
!this._moved
)
return;
if (!this._moved) {
this._map.fire("selectionstart");
this._moved = true;
this._box = DomUtil.create("div", "leaflet-zoom-box", this._container);
DomUtil.addClass(this._container, "leaflet-crosshair");
}
if (e.type === "touchmove") this._point = this._map.mouseEventToContainerPoint(e.touches[0]);
else this._point = this._map.mouseEventToContainerPoint(e);
var bounds = new Bounds(this._point, this._startPoint),
size = bounds.getSize();
@ -114,7 +113,6 @@ export var BoxSelect = Handler.extend({
DomUtil.enableTextSelection();
DomUtil.enableImageDrag();
this._map.dragging.enable();
this._forceBoxSelect = false;
DomEvent.off(
//@ts-ignore

View File

@ -23,6 +23,8 @@ import {
ContextActionTarget,
ContextActionType,
ContextActions,
SHORT_PRESS_MILLISECONDS,
DEBOUNCE_MILLISECONDS,
} from "../constants/constants";
import { CoalitionPolygon } from "./coalitionarea/coalitionpolygon";
import { MapHiddenTypes, MapOptions } from "../types/types";
@ -99,11 +101,19 @@ export class Map extends L.Map {
/* Other state controls */
#isZooming: boolean = false;
#isDragging: boolean = false;
#isMouseDown: boolean = false;
#isSelecting: boolean = false;
#debounceTimeout: number | null = null;
#isLeftMouseDown: boolean = false;
#isRightMouseDown: boolean = false;
#leftMouseDownEpoch: number = 0;
#rightMouseDownEpoch: number = 0;
#leftMouseDownTimeout: number = 0;
#rightMouseDownTimeout: number = 0;
#lastMousePosition: L.Point = new L.Point(0, 0);
#lastMouseCoordinates: L.LatLng = new L.LatLng(0, 0);
#previousZoom: number = 0;
#selecting: boolean = false;
#keepRelativePositions: boolean = false;
/* Camera control plugin */
#slaveDCSCamera: boolean = false;
@ -122,6 +132,7 @@ export class Map extends L.Map {
#destinationPreviewMarkers: { [key: number]: TemporaryUnitMarker | TargetMarker } = {};
#destinationRotation: number = 0;
#isRotatingDestination: boolean = false;
#destionationWasRotated: boolean = false;
/* Unit context actions */
#contextActionSet: null | ContextActionSet = null;
@ -185,8 +196,9 @@ export class Map extends L.Map {
this.on("mouseup", (e: any) => this.#onMouseUp(e));
this.on("mousedown", (e: any) => this.#onMouseDown(e));
this.on("click", (e: any) => this.#onLeftClick(e));
this.on("contextmenu", (e: any) => this.#onRightClick(e));
this.on("dblclick", (e: any) => this.#onDoubleClick(e));
this.on("click", (e: any) => e.originalEvent.preventDefault());
this.on("contextmenu", (e: any) => e.originalEvent.preventDefault());
this.on("mousemove", (e: any) => this.#onMouseMove(e));
@ -195,6 +207,7 @@ export class Map extends L.Map {
/* Custom touch events for touchscreen support */
L.DomEvent.on(this.getContainer(), "touchstart", this.#onMouseDown, this);
L.DomEvent.on(this.getContainer(), "touchend", this.#onMouseUp, this);
L.DomEvent.on(this.getContainer(), 'wheel', this.#onMouseWheel, this);
/* Event listeners */
AppStateChangedEvent.on((state, subState) => this.#onStateChanged(state, subState));
@ -295,55 +308,73 @@ export class Map extends L.Map {
label: "Hide/show labels",
keyUpCallback: () => this.setOption("showUnitLabels", !this.getOptions().showUnitLabels),
code: "KeyL",
shiftKey: true
shiftKey: true,
altKey: false,
ctrlKey: false,
})
.addShortcut("toggleAcquisitionRings", {
label: "Hide/show acquisition rings",
keyUpCallback: () => this.setOption("showUnitsAcquisitionRings", !this.getOptions().showUnitsAcquisitionRings),
code: "KeyE",
shiftKey: true
shiftKey: true,
altKey: false,
ctrlKey: false,
})
.addShortcut("toggleEngagementRings", {
label: "Hide/show engagement rings",
keyUpCallback: () => this.setOption("showUnitsEngagementRings", !this.getOptions().showUnitsEngagementRings),
code: "KeyQ",
shiftKey: true
shiftKey: true,
altKey: false,
ctrlKey: false,
})
.addShortcut("toggleHideShortEngagementRings", {
label: "Hide/show short range rings",
keyUpCallback: () => this.setOption("hideUnitsShortRangeRings", !this.getOptions().hideUnitsShortRangeRings),
code: "KeyR",
shiftKey: true
shiftKey: true,
altKey: false,
ctrlKey: false,
})
.addShortcut("toggleDetectionLines", {
label: "Hide/show detection lines",
keyUpCallback: () => this.setOption("showUnitTargets", !this.getOptions().showUnitTargets),
code: "KeyF",
shiftKey: true
shiftKey: true,
altKey: false,
ctrlKey: false,
})
.addShortcut("toggleGroupMembers", {
label: "Hide/show group members",
keyUpCallback: () => this.setOption("hideGroupMembers", !this.getOptions().hideGroupMembers),
code: "KeyG",
shiftKey: true
shiftKey: true,
altKey: false,
ctrlKey: false,
})
.addShortcut("toggleRelativePositions", {
label: "Toggle group movement mode",
keyUpCallback: () => this.setOption("keepRelativePositions", !this.getOptions().keepRelativePositions),
code: "KeyP",
shiftKey: true
keyUpCallback: () => this.setKeepRelativePositions(false),
keyDownCallback: () => this.setKeepRelativePositions(true),
code: "AltLeft",
shiftKey: false,
ctrlKey: false,
})
.addShortcut("increaseCameraZoom", {
label: "Increase camera zoom",
keyUpCallback: () => this.increaseCameraZoom(),
code: "Equal",
shiftKey: true
shiftKey: true,
altKey: false,
ctrlKey: false,
})
.addShortcut("decreaseCameraZoom", {
label: "Decrease camera zoom",
keyUpCallback: () => this.decreaseCameraZoom(),
code: "Minus",
shiftKey: true
shiftKey: true,
altKey: false,
ctrlKey: false,
});
for (let contextActionName in ContextActions) {
@ -357,15 +388,14 @@ export class Map extends L.Map {
shiftKey: contextAction.getOptions().shiftKey,
altKey: contextAction.getOptions().altKey,
ctrlKey: contextAction.getOptions().ctrlKey,
keyDownCallback: () => {
const contextActionSet = this.getContextActionSet();
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) {
const contextActionSet = this.getContextActionSet();
if (this.getContextAction() === null || contextAction !== this.getContextAction()) {
if (getApp().getState() === OlympusState.UNIT_CONTROL && contextActionSet && contextAction.getId() in contextActionSet.getContextActions()) {
if (contextAction.getOptions().executeImmediately) contextAction.executeCallback(null, null);
else this.setContextAction(contextAction);
}
} else {
this.setContextAction(null);
}
},
@ -381,30 +411,44 @@ export class Map extends L.Map {
keyUpCallback: (ev: KeyboardEvent) => (this.#panUp = false),
keyDownCallback: (ev: KeyboardEvent) => (this.#panUp = true),
code: "KeyW",
shiftKey: false,
altKey: false,
ctrlKey: false,
})
.addShortcut(`panDown`, {
label: "Pan map down",
keyUpCallback: (ev: KeyboardEvent) => (this.#panDown = false),
keyDownCallback: (ev: KeyboardEvent) => (this.#panDown = true),
code: "KeyS",
shiftKey: false,
altKey: false,
ctrlKey: false,
})
.addShortcut(`panLeft`, {
label: "Pan map left",
keyUpCallback: (ev: KeyboardEvent) => (this.#panLeft = false),
keyDownCallback: (ev: KeyboardEvent) => (this.#panLeft = true),
code: "KeyA",
shiftKey: false,
altKey: false,
ctrlKey: false,
})
.addShortcut(`panRight`, {
label: "Pan map right",
keyUpCallback: (ev: KeyboardEvent) => (this.#panRight = false),
keyDownCallback: (ev: KeyboardEvent) => (this.#panRight = true),
code: "KeyD",
shiftKey: false,
altKey: false,
ctrlKey: false,
})
.addShortcut(`panFast`, {
label: "Pan map fast",
keyUpCallback: (ev: KeyboardEvent) => (this.#panFast = false),
keyDownCallback: (ev: KeyboardEvent) => (this.#panFast = true),
code: "ShiftLeft",
altKey: false,
ctrlKey: false,
});
/* Periodically check if the camera control endpoint is available */
@ -580,7 +624,7 @@ export class Map extends L.Map {
}
isSelecting() {
return this.#selecting;
return this.#isSelecting;
}
setTheatre(theatre: string) {
@ -670,6 +714,17 @@ export class Map extends L.Map {
return this.#previousZoom;
}
setKeepRelativePositions(keepRelativePositions: boolean) {
this.#keepRelativePositions = keepRelativePositions;
this.#updateDestinationPreviewMarkers();
if (keepRelativePositions) this.scrollWheelZoom.disable();
else this.scrollWheelZoom.enable();
}
getKeepRelativePositions() {
return this.#keepRelativePositions;
}
increaseCameraZoom() {
//const slider = document.querySelector(`label[title="${DCS_LINK_RATIO}"] input`);
//if (slider instanceof HTMLInputElement) {
@ -762,166 +817,217 @@ export class Map extends L.Map {
}
#onSelectionStart(e: any) {
this.#selecting = true;
this.#isSelecting = true;
}
#onSelectionEnd(e: any) {
getApp().getUnitsManager().selectFromBounds(e.selectionBounds);
this.#selecting = false;
/* Delay the event so that any other event in the queue still sees the map in selection mode */
window.setTimeout(() => {
this.#isSelecting = false;
}, 300);
}
#onMouseUp(e: any) {
this.#isMouseDown = false;
this.#isRotatingDestination = false;
if (e.originalEvent?.button === 0) {
if (Date.now() - this.#leftMouseDownEpoch < SHORT_PRESS_MILLISECONDS) this.#onLeftShortClick(e);
this.#isLeftMouseDown = false;
} else if (e.originalEvent?.button === 2) {
if (Date.now() - this.#rightMouseDownEpoch < SHORT_PRESS_MILLISECONDS) this.#onRightShortClick(e);
this.#isRightMouseDown = false;
}
}
#onMouseDown(e: any) {
this.#isMouseDown = true;
if (this.#contextAction?.getTarget() === ContextActionTarget.POINT && e.originalEvent?.button === 2) this.#isRotatingDestination = true;
if (e.originalEvent?.button === 0) {
this.#isLeftMouseDown = true;
this.#leftMouseDownEpoch = Date.now();
} else if (e.originalEvent?.button === 2) {
this.#isRightMouseDown = true;
this.#rightMouseDownEpoch = Date.now();
this.#rightMouseDownTimeout = window.setTimeout(() => {
this.#onRightLongClick(e);
}, SHORT_PRESS_MILLISECONDS);
}
}
#onLeftClick(e: L.LeafletMouseEvent) {
console.log(`Left click at ${e.latlng}`);
#onMouseWheel(e: any) {
if (this.#keepRelativePositions) {
this.#destinationRotation += e.deltaY / 20;
this.#moveDestinationPreviewMarkers();
}
}
/* 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 (this.#spawnRequestTable !== null) {
this.#spawnRequestTable.unit.location = e.latlng;
getApp()
.getUnitsManager()
.spawnUnits(
this.#spawnRequestTable.category,
Array(this.#spawnRequestTable.amount).fill(this.#spawnRequestTable.unit),
this.#spawnRequestTable.coalition,
false,
undefined,
undefined,
(hash) => {
this.addTemporaryMarker(e.latlng, this.#spawnRequestTable?.unit.unitType ?? "unknown", this.#spawnRequestTable?.coalition ?? "blue", hash);
#onLeftShortClick(e: L.LeafletMouseEvent) {
if (this.#debounceTimeout) window.clearTimeout(this.#debounceTimeout);
if (Date.now() - this.#leftMouseDownEpoch < SHORT_PRESS_MILLISECONDS) {
this.#debounceTimeout = window.setTimeout(() => {
if (!this.#isSelecting) {
console.log(`Left short click at ${e.latlng}`);
/* Execute the short click action */
if (getApp().getState() === OlympusState.IDLE) {
/* Do nothing */
} else if (getApp().getState() === OlympusState.SPAWN) {
if (getApp().getSubState() === SpawnSubState.SPAWN_UNIT) {
if (this.#spawnRequestTable !== null) {
this.#spawnRequestTable.unit.location = e.latlng;
getApp()
.getUnitsManager()
.spawnUnits(
this.#spawnRequestTable.category,
Array(this.#spawnRequestTable.amount).fill(this.#spawnRequestTable.unit),
this.#spawnRequestTable.coalition,
false,
undefined,
undefined,
(hash) => {
this.addTemporaryMarker(
e.latlng,
this.#spawnRequestTable?.unit.unitType ?? "unknown",
this.#spawnRequestTable?.coalition ?? "blue",
hash
);
}
);
}
);
}
} else if (getApp().getSubState() === SpawnSubState.SPAWN_EFFECT) {
if (this.#effectRequestTable !== null) {
if (this.#effectRequestTable.type === "explosion") {
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);
} else if (getApp().getSubState() === SpawnSubState.SPAWN_EFFECT) {
if (this.#effectRequestTable !== null) {
if (this.#effectRequestTable.type === "explosion") {
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(e.latlng);
} else if (this.#effectRequestTable.type === "smoke") {
getApp()
.getServerManager()
.spawnSmoke(this.#effectRequestTable.smokeColor ?? "white", e.latlng);
this.addSmokeMarker(e.latlng, this.#effectRequestTable.smokeColor ?? "white");
this.addExplosionMarker(e.latlng);
} else if (this.#effectRequestTable.type === "smoke") {
getApp()
.getServerManager()
.spawnSmoke(this.#effectRequestTable.smokeColor ?? "white", e.latlng);
this.addSmokeMarker(e.latlng, this.#effectRequestTable.smokeColor ?? "white");
}
}
}
} else if (getApp().getState() === OlympusState.DRAW) {
if (getApp().getSubState() === DrawSubState.DRAW_POLYGON) {
const selectedArea = this.getSelectedCoalitionArea();
if (selectedArea && selectedArea instanceof CoalitionPolygon) {
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(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(e.latlng, this.#coalitionAreas[idx])) {
this.#coalitionAreas[idx].setSelected(true);
getApp().setState(OlympusState.DRAW, DrawSubState.EDIT);
break;
}
}
}
} 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(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;
});
this.#targetPoint.on("dragend", (event) => {
getApp().setState(OlympusState.JTAC);
event.target.options["freeze"] = false;
});
this.#targetPoint.on("click", (event) => {
getApp().setState(OlympusState.JTAC);
});
} else this.#targetPoint.setLatLng(e.latlng);
} else if (getApp().getSubState() === JTACSubState.SELECT_ECHO_POINT) {
if (!this.#ECHOPoint) {
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;
});
this.#ECHOPoint.on("dragend", (event) => {
getApp().setState(OlympusState.JTAC);
event.target.options["freeze"] = false;
});
this.#ECHOPoint.on("click", (event) => {
getApp().setState(OlympusState.JTAC);
});
} else this.#ECHOPoint.setLatLng(e.latlng);
} else if (getApp().getSubState() === JTACSubState.SELECT_IP) {
if (!this.#IPPoint) {
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;
});
this.#IPPoint.on("dragend", (event) => {
getApp().setState(OlympusState.JTAC);
event.target.options["freeze"] = false;
});
this.#IPPoint.on("click", (event) => {
getApp().setState(OlympusState.JTAC);
});
} else this.#IPPoint.setLatLng(e.latlng);
}
getApp().setState(OlympusState.JTAC);
this.#drawIPToTargetLine();
} else {
if (getApp().getSubState() === NO_SUBSTATE) getApp().setState(OlympusState.IDLE);
else getApp().setState(OlympusState.UNIT_CONTROL);
}
}
}
} else if (getApp().getState() === OlympusState.DRAW) {
if (getApp().getSubState() === DrawSubState.DRAW_POLYGON) {
const selectedArea = this.getSelectedCoalitionArea();
if (selectedArea && selectedArea instanceof CoalitionPolygon) {
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(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(e.latlng, this.#coalitionAreas[idx])) {
this.#coalitionAreas[idx].setSelected(true);
getApp().setState(OlympusState.DRAW, DrawSubState.EDIT);
break;
}
}
}
}, DEBOUNCE_MILLISECONDS);
}
}
#onRightShortClick(e: L.LeafletMouseEvent) {
console.log(`Right short click at ${e.latlng}`);
window.clearTimeout(this.#rightMouseDownTimeout);
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 (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(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;
});
this.#targetPoint.on("dragend", (event) => {
getApp().setState(OlympusState.JTAC);
event.target.options["freeze"] = false;
});
this.#targetPoint.on("click", (event) => {
getApp().setState(OlympusState.JTAC);
});
} else this.#targetPoint.setLatLng(e.latlng);
} else if (getApp().getSubState() === JTACSubState.SELECT_ECHO_POINT) {
if (!this.#ECHOPoint) {
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;
});
this.#ECHOPoint.on("dragend", (event) => {
getApp().setState(OlympusState.JTAC);
event.target.options["freeze"] = false;
});
this.#ECHOPoint.on("click", (event) => {
getApp().setState(OlympusState.JTAC);
});
} else this.#ECHOPoint.setLatLng(e.latlng);
} else if (getApp().getSubState() === JTACSubState.SELECT_IP) {
if (!this.#IPPoint) {
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;
});
this.#IPPoint.on("dragend", (event) => {
getApp().setState(OlympusState.JTAC);
event.target.options["freeze"] = false;
});
this.#IPPoint.on("click", (event) => {
getApp().setState(OlympusState.JTAC);
});
} else this.#IPPoint.setLatLng(e.latlng);
}
getApp().setState(OlympusState.JTAC);
this.#drawIPToTargetLine();
} else {
else this.executeDefaultContextAction(null, e.latlng, e.originalEvent);
}
}
#onRightClick(e: L.LeafletMouseEvent) {
e.originalEvent.preventDefault();
#onRightLongClick(e: L.LeafletMouseEvent) {
console.log(`Right long click at ${e.latlng}`);
console.log(`Right click at ${e.latlng}`);
if (!this.#isDragging && !this.#isZooming) {
this.deselectAllCoalitionAreas();
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 (!this.getContextAction()) {
getApp().setState(OlympusState.UNIT_CONTROL, UnitControlSubState.MAP_CONTEXT_MENU);
MapContextMenuRequestEvent.dispatch(e.latlng);
}
if (getApp().getState() === OlympusState.UNIT_CONTROL) {
if (!this.getContextAction()) {
getApp().setState(OlympusState.UNIT_CONTROL, UnitControlSubState.MAP_CONTEXT_MENU);
MapContextMenuRequestEvent.dispatch(e.latlng);
}
}
}
#onDoubleClick(e: L.LeafletMouseEvent) {
console.log(`Double click at ${e.latlng}`);
if (this.#debounceTimeout) window.clearTimeout(this.#debounceTimeout);
if (getApp().getSubState() === NO_SUBSTATE) getApp().setState(OlympusState.IDLE);
else getApp().setState(getApp().getState());
}
#onMouseMove(e: any) {
if (!this.#isRotatingDestination) {
this.#destionationWasRotated = false;
this.#lastMousePosition.x = e.originalEvent.x;
this.#lastMousePosition.y = e.originalEvent.y;
this.#lastMouseCoordinates = e.latlng;
@ -934,6 +1040,7 @@ export class Map extends L.Map {
if (this.#currentSpawnMarker) this.#currentSpawnMarker.setLatLng(e.latlng);
if (this.#currentEffectMarker) this.#currentEffectMarker.setLatLng(e.latlng);
} else {
this.#destionationWasRotated = true;
this.#destinationRotation -= e.originalEvent.movementX;
}
@ -1047,18 +1154,20 @@ export class Map extends L.Map {
delete this.#destinationPreviewMarkers[ID];
});
selectedUnits.forEach((unit) => {
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() === ContextActionTarget.POINT) {
this.#destinationPreviewMarkers[unit.ID] = new TargetMarker(new L.LatLng(0, 0));
}
this.#destinationPreviewMarkers[unit.ID]?.addTo(this);
});
if (this.#keepRelativePositions) {
selectedUnits.forEach((unit) => {
if (this.#contextAction?.getOptions().type === ContextActionType.MOVE || this.#contextAction === null) {
this.#destinationPreviewMarkers[unit.ID] = new TemporaryUnitMarker(new L.LatLng(0, 0), unit.getName(), unit.getCoalition());
} else if (this.#contextAction?.getTarget() === ContextActionTarget.POINT) {
this.#destinationPreviewMarkers[unit.ID] = new TargetMarker(new L.LatLng(0, 0));
}
this.#destinationPreviewMarkers[unit.ID]?.addTo(this);
});
}
}
#moveDestinationPreviewMarkers() {
if (this.#options.keepRelativePositions) {
if (this.#keepRelativePositions) {
Object.entries(getApp().getUnitsManager().computeGroupDestination(this.#lastMouseCoordinates, this.#destinationRotation)).forEach(([ID, latlng]) => {
this.#destinationPreviewMarkers[ID]?.setLatLng(latlng);
});

View File

@ -160,7 +160,6 @@ export class OlympusApp {
});
this.#shortcutManager.checkShortcuts();
}
getConfig() {
@ -189,13 +188,55 @@ export class OlympusApp {
console.log(`Profile ${this.#profileName} saved correctly`);
} else {
this.addInfoMessage("Error saving profile");
throw new Error("Error saving profile file");
throw new Error("Error saving profile");
}
}) // Parse the response as JSON
.catch((error) => console.error(error)); // Handle errors
}
}
resetProfile() {
if (this.#profileName !== null) {
const requestOptions = {
method: "PUT", // Specify the request method
headers: { "Content-Type": "application/json" }, // Specify the content type
body: "", // Send the data in JSON format
};
fetch(this.getExpressAddress() + `/resources/profile/reset/${this.#profileName}`, requestOptions)
.then((response) => {
if (response.status === 200) {
console.log(`Profile ${this.#profileName} reset correctly`);
location.reload()
} else {
this.addInfoMessage("Error resetting profile");
throw new Error("Error resetting profile");
}
}) // Parse the response as JSON
.catch((error) => console.error(error)); // Handle errors
}
}
resetAllProfiles() {
const requestOptions = {
method: "PUT", // Specify the request method
headers: { "Content-Type": "application/json" }, // Specify the content type
body: "", // Send the data in JSON format
};
fetch(this.getExpressAddress() + `/resources/profile/resetall`, requestOptions)
.then((response) => {
if (response.status === 200) {
console.log(`All profiles reset correctly`);
location.reload()
} else {
this.addInfoMessage("Error resetting profiles");
throw new Error("Error resetting profiles");
}
}) // Parse the response as JSON
.catch((error) => console.error(error)); // Handle errors
}
getProfile() {
if (this.#profileName && this.#config?.profiles && this.#config?.profiles[this.#profileName])
return this.#config?.profiles[this.#profileName] as ProfileOptions;
@ -208,9 +249,9 @@ export class OlympusApp {
this.#map?.setOptions(profile.mapOptions);
this.#shortcutManager?.setShortcutsOptions(profile.shortcuts);
this.addInfoMessage("Profile loaded correctly");
console.log(`Profile ${this.#profileName} saved correctly`);
console.log(`Profile ${this.#profileName} loaded correctly`);
} else {
this.addInfoMessage("Error loading profile");
this.addInfoMessage("Profile not found, creating new profile");
console.log(`Error loading profile`);
}
}

View File

@ -11,33 +11,31 @@ export class Shortcut {
this.#id = id;
this.#options = options;
AppStateChangedEvent.on(() => this.#keydown = false)
/* Key up event is mandatory */
AppStateChangedEvent.on(() => (this.#keydown = false));
/* On keyup, it is enough to check the code only, not the entire combination */
document.addEventListener("keyup", (ev: any) => {
this.#keydown = false;
if (keyEventWasInInput(ev) || options.code !== ev.code) return;
if (
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)
)
if (this.#keydown && options.code === ev.code) {
ev.preventDefault();
options.keyUpCallback(ev);
this.#keydown = false;
}
});
/* Key down event is optional */
if (options.keyDownCallback) {
document.addEventListener("keydown", (ev: any) => {
if (this.#keydown || keyEventWasInInput(ev) || options.code !== ev.code) return;
/* On keydown, check exactly if the requested key combination is being pressed */
document.addEventListener("keydown", (ev: any) => {
if (
!(this.#keydown || keyEventWasInInput(ev) || options.code !== ev.code) &&
(options.altKey === undefined || ev.altKey === (options.altKey ?? ev.code.indexOf("Alt") >= 0)) &&
(options.ctrlKey === undefined || ev.ctrlKey === (options.ctrlKey ?? ev.code.indexOf("Control") >= 0)) &&
(options.shiftKey === undefined || ev.shiftKey === (options.shiftKey ?? ev.code.indexOf("Shift") >= 0))
) {
ev.preventDefault();
this.#keydown = true;
if (
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);
});
}
if (options.keyDownCallback) options.keyDownCallback(ev); /* Key down event is optional */
}
});
}
getOptions() {
@ -51,4 +49,19 @@ export class Shortcut {
getId() {
return this.#id;
}
toActions() {
let actions: string[] = [];
if (this.getOptions().shiftKey) actions.push("Shift");
if (this.getOptions().altKey) actions.push("Alt");
if (this.getOptions().ctrlKey) actions.push("Ctrl")
actions.push(this.getOptions().code.replace("Key", "")
.replace("ControlLeft", "Left Ctrl")
.replace("AltLeft", "Left Alt")
.replace("ShiftLeft", "Left Shift")
.replace("ControlRight", "Right Ctrl")
.replace("AltRight", "Right Alt")
.replace("ShiftRight", "Right Shift"))
return actions
}
}

View File

@ -5,14 +5,7 @@ import { Shortcut } from "./shortcut";
export class ShortcutManager {
#shortcuts: { [key: string]: Shortcut } = {};
constructor() {
// Stop ctrl+digits from sending the browser to another tab
document.addEventListener("keydown", (ev: KeyboardEvent) => {
if (ev.code.indexOf("Digit") >= 0 && ev.ctrlKey === true && ev.altKey === false && ev.shiftKey === false) {
ev.preventDefault();
}
});
}
constructor() {}
addShortcut(id: string, shortcutOptions: ShortcutOptions) {
this.#shortcuts[id] = new Shortcut(id, shortcutOptions);
@ -20,6 +13,14 @@ export class ShortcutManager {
return this;
}
getShortcut(id) {
return this.#shortcuts[id];
}
getShortcuts() {
return this.#shortcuts;
}
getShortcutsOptions() {
let shortcutsOptions = {};
for (let id in this.#shortcuts) {
@ -48,11 +49,18 @@ export class ShortcutManager {
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)
shortcut.getOptions().code === otherShortcut.getOptions().code &&
((shortcut.getOptions().shiftKey === undefined && otherShortcut.getOptions().shiftKey !== undefined) ||
(shortcut.getOptions().shiftKey !== undefined && otherShortcut.getOptions().shiftKey === undefined) ||
shortcut.getOptions().shiftKey === otherShortcut.getOptions().shiftKey) &&
((shortcut.getOptions().altKey === undefined && otherShortcut.getOptions().altKey !== undefined) ||
(shortcut.getOptions().altKey !== undefined && otherShortcut.getOptions().altKey === undefined) ||
shortcut.getOptions().altKey === otherShortcut.getOptions().altKey) &&
((shortcut.getOptions().ctrlKey === undefined && otherShortcut.getOptions().ctrlKey !== undefined) ||
(shortcut.getOptions().ctrlKey !== undefined && otherShortcut.getOptions().ctrlKey === undefined) ||
shortcut.getOptions().ctrlKey === otherShortcut.getOptions().ctrlKey)
) {
console.error("Duplicate shortcut: " + shortcut.getOptions().label + " and " + otherShortcut.getOptions().label)
console.error("Duplicate shortcut: " + shortcut.getOptions().label + " and " + otherShortcut.getOptions().label);
}
}
}

View File

@ -21,7 +21,6 @@ export type MapOptions = {
fillSelectedRing: boolean;
showMinimap: boolean;
protectDCSUnits: boolean;
keepRelativePositions: boolean;
cameraPluginPort: number;
cameraPluginRatio: number;
cameraPluginEnabled: boolean;

View File

@ -11,6 +11,8 @@ export function OlStateButton(props: {
icon?: IconProp;
tooltip: string;
onClick: () => void;
onMouseUp?: () => void;
onMouseDown?: () => void;
children?: JSX.Element | JSX.Element[];
}) {
const [hover, setHover] = useState(false);
@ -36,8 +38,10 @@ export function OlStateButton(props: {
ref={buttonRef}
onClick={() => {
props.onClick();
setHover(false);
props.onClick ?? setHover(false);
}}
onMouseUp={props.onMouseUp ?? (() => {})}
onMouseDown={props.onMouseDown ?? (() => {})}
data-checked={props.checked}
type="button"
className={className}

View File

@ -5,8 +5,7 @@ export function OlToggle(props: { toggled: boolean | undefined; onClick: () => v
<div className="inline-flex cursor-pointer items-center" onClick={props.onClick}>
<button className="peer sr-only" />
<div
data-flash={props.toggled === undefined}
data-toggled={props.toggled ?? false}
data-toggled={props.toggled === true? 'true': props.toggled === undefined? 'undefined': 'false'}
className={`
peer relative h-7 w-14 rounded-full bg-gray-200
after:absolute after:start-[4px] after:top-0.5 after:h-6 after:w-6
@ -14,10 +13,11 @@ export function OlToggle(props: { toggled: boolean | undefined; onClick: () => v
after:transition-all after:content-['']
dark:border-gray-600 dark:peer-focus:ring-blue-800
dark:data-[toggled='true']:bg-blue-500
data-[flash='true']:after:animate-pulse
data-[toggled='false']:bg-gray-500
data-[toggled='true']:after:translate-x-full
data-[toggled='true']:after:border-white
data-[toggled='undefined']:bg-gray-800
data-[toggled='undefined']:after:translate-x-[50%]
peer-focus:outline-none peer-focus:ring-2 peer-focus:ring-blue-300
rtl:data-[toggled='true']:after:-translate-x-full
`}

View File

@ -112,6 +112,11 @@ export function MapContextMenu(props: {}) {
} else if (unit !== null) {
contextActionIt.executeCallback(unit, null);
}
window.setTimeout(() => {
if (getApp().getSubState() === UnitControlSubState.MAP_CONTEXT_MENU || getApp().getSubState() === UnitControlSubState.UNIT_CONTEXT_MENU) {
getApp().setState(OlympusState.UNIT_CONTROL)
}
}, 200)
}
}}
>

View File

@ -6,37 +6,48 @@ import { getApp } from "../../olympusapp";
import { OlympusState } from "../../constants/constants";
import { Shortcut } from "../../shortcut/shortcut";
import { BindShortcutRequestEvent, ShortcutsChangedEvent } from "../../events";
import { OlToggle } from "../components/oltoggle";
export function KeybindModal(props: { open: boolean }) {
const [shortcuts, setShortcuts] = useState({} as { [key: string]: Shortcut });
const [shortcut, setShortcut] = useState(null as null | Shortcut);
const [code, setCode] = useState(null as null | string);
const [shiftKey, setShiftKey] = useState(false);
const [ctrlKey, setCtrlKey] = useState(false);
const [altKey, setAltKey] = useState(false);
const [shiftKey, setShiftKey] = useState(false as boolean | undefined);
const [ctrlKey, setCtrlKey] = useState(false as boolean | undefined);
const [altKey, setAltKey] = useState(false as boolean | undefined);
useEffect(() => {
ShortcutsChangedEvent.on((shortcuts) => setShortcuts({ ...shortcuts }));
BindShortcutRequestEvent.on((shortcut) => setShortcut(shortcut));
document.addEventListener("keydown", (ev) => {
setCode(ev.code);
if (!(ev.code.indexOf("Shift") >= 0 || ev.code.indexOf("Alt") >= 0 || ev.code.indexOf("Control") >= 0)) {
setShiftKey(ev.shiftKey);
setAltKey(ev.altKey);
setCtrlKey(ev.ctrlKey);
if (ev.code) {
setCode(ev.code);
}
});
}, []);
useEffect(() => {
setCode(shortcut?.getOptions().code ?? null)
setShiftKey(shortcut?.getOptions().shiftKey)
setAltKey(shortcut?.getOptions().altKey)
setCtrlKey(shortcut?.getOptions().ctrlKey)
}, [shortcut])
let available: null | boolean = code ? true : null;
let inUseShortcut: null | Shortcut = null;
for (let id in shortcuts) {
if (
if (id !== shortcut?.getId() &&
code === shortcuts[id].getOptions().code &&
shiftKey === (shortcuts[id].getOptions().shiftKey ?? false) &&
altKey === (shortcuts[id].getOptions().altKey ?? false) &&
ctrlKey === (shortcuts[id].getOptions().shiftKey ?? false)
((shiftKey === undefined && shortcuts[id].getOptions().shiftKey !== undefined) ||
(shiftKey !== undefined && shortcuts[id].getOptions().shiftKey === undefined) ||
shiftKey === shortcuts[id].getOptions().shiftKey) && (
(altKey === undefined && shortcuts[id].getOptions().altKey !== undefined) ||
(altKey !== undefined && shortcuts[id].getOptions().altKey === undefined) ||
altKey === shortcuts[id].getOptions().altKey) && (
(ctrlKey === undefined && shortcuts[id].getOptions().ctrlKey !== undefined) ||
(ctrlKey !== undefined && shortcuts[id].getOptions().ctrlKey === undefined) ||
ctrlKey === shortcuts[id].getOptions().ctrlKey)
) {
available = false;
inUseShortcut = shortcuts[id];
@ -75,26 +86,66 @@ export function KeybindModal(props: { open: boolean }) {
Press the key you want to bind to this event
</span>
</div>
<div className="w-full text-center text-white">
{ctrlKey ? "Ctrl + " : ""}
{shiftKey ? "Shift + " : ""}
{altKey ? "Alt + " : ""}
{code}
<div className="w-full text-center text-white">{code}</div>
<div className="flex flex-col gap-2">
<div className="flex gap-2">
<OlToggle
onClick={() => {
if (shiftKey === false) setShiftKey(undefined);
else if (shiftKey === undefined) setShiftKey(true);
else setShiftKey(false);
}}
toggled={shiftKey}
></OlToggle>
<div className="text-white">
{shiftKey === true && "Shift key must be pressed"}
{shiftKey === undefined && "Shift key can be anything"}
{shiftKey === false && "Shift key must NOT be pressed"}
</div>
</div>
<div className="flex gap-2">
<OlToggle
onClick={() => {
if (altKey === false) setAltKey(undefined);
else if (altKey === undefined) setAltKey(true);
else setAltKey(false);
}}
toggled={altKey}
></OlToggle>
<div className="text-white">
{altKey === true && "Alt key must be pressed"}
{altKey === undefined && "Alt key can be anything"}
{altKey === false && "Alt key must NOT be pressed"}
</div>
</div>
<div className="flex gap-2">
<OlToggle
onClick={() => {
if (ctrlKey === false) setCtrlKey(undefined);
else if (ctrlKey === undefined) setCtrlKey(true);
else setCtrlKey(false);
}}
toggled={ctrlKey}
></OlToggle>
<div className="text-white">
{ctrlKey === true && "Ctrl key must be pressed"}
{ctrlKey === undefined && "Ctrl key can be anything"}
{ctrlKey === false && "Ctrl key must NOT be pressed"}
</div>
</div>
</div>
<div className="text-white">
{available === true && <div className="text-green-600">Keybind is free!</div>}
{available === false && (
<div>
Keybind is already in use:{" "}
<span
className={`font-bold text-red-600`}
>
{inUseShortcut?.getOptions().label}
</span>
Keybind is already in use: <span className={`
font-bold text-red-600
`}>{inUseShortcut?.getOptions().label}</span>
</div>
)}
</div>
<div className="flex justify-end">
{available && shortcut && (
<button

View File

@ -1,4 +1,4 @@
import React, { useState } from "react";
import React, { useEffect, useState } from "react";
import { Modal } from "./components/modal";
import { Card } from "./components/card";
import { ErrorCallout } from "../../ui/components/olcallout";
@ -7,6 +7,7 @@ import { faArrowRight, faCheckCircle, faExternalLink } from "@fortawesome/free-s
import { getApp, VERSION } from "../../olympusapp";
import { sha256 } from "js-sha256";
import { BLUE_COMMANDER, GAME_MASTER, OlympusState, RED_COMMANDER } from "../../constants/constants";
import { FaTrash, FaXmark } from "react-icons/fa6";
export function LoginModal(props: {}) {
// TODO: add warning if not in secure context and some features are disabled
@ -16,6 +17,11 @@ export function LoginModal(props: {}) {
const [loginError, setLoginError] = useState(false);
const [commandMode, setCommandMode] = useState(null as null | string);
useEffect(() => {
/* Set the profile name */
getApp().setProfile(profileName);
}, [profileName])
function checkPassword(password: string) {
setCheckingPassword(true);
var hash = sha256.create();
@ -44,8 +50,6 @@ export function LoginModal(props: {}) {
getApp().getServerManager().startUpdate();
getApp().setState(OlympusState.IDLE);
/* Set the profile name */
getApp().setProfile(profileName);
/* If no profile exists already with that name, create it from scratch from the defaults */
if (getApp().getProfile() === null)
getApp().saveProfile();
@ -56,7 +60,7 @@ export function LoginModal(props: {}) {
return (
<Modal
className={`
inline-flex h-[75%] max-h-[530px] w-[80%] max-w-[1100px] overflow-y-auto
inline-flex h-[75%] max-h-[570px] w-[80%] max-w-[1100px] overflow-y-auto
scroll-smooth bg-white
dark:bg-olympus-800
max-md:h-full max-md:max-h-full max-md:w-full max-md:rounded-none
@ -236,7 +240,7 @@ export function LoginModal(props: {}) {
/>
</div>
<div className="text-xs text-gray-400">
The profile name you choose determines what keybinds/groups/options get loaded and edited. Be careful!
The profile name you choose determines the saved key binds, groups and options you see.
</div>
<div className="flex">
<button
@ -279,8 +283,8 @@ export function LoginModal(props: {}) {
) : (
<>
<ErrorCallout
title="Server could not be reached"
description="The Olympus Server at this address could not be reached. Check the address is correct, restart the Olympus server or reinstall Olympus. Ensure the ports set are not already used."
title="Server could not be reached or password is incorrect"
description="The Olympus Server at this address could not be reached or the password is incorrect. Check your password. If correct, check the address is correct, restart the Olympus server or reinstall Olympus. Ensure the ports set are not already used."
></ErrorCallout>
<div className={`text-sm font-medium text-gray-200`}>
Still having issues? See our

View File

@ -11,9 +11,7 @@ import { UnitSinkPanel } from "./components/unitsinkpanel";
import { UnitSink } from "../../audio/unitsink";
import { FaMinus, FaVolumeHigh } from "react-icons/fa6";
import { getRandomColor } from "../../other/utils";
import { AudioManagerStateChangedEvent, AudioSinksChangedEvent, AudioSourcesChangedEvent } from "../../events";
let shortcutKeys = ["Z", "X", "C", "V", "B", "N", "M", "K", "L"];
import { AudioManagerStateChangedEvent, AudioSinksChangedEvent, AudioSourcesChangedEvent, ShortcutsChangedEvent } from "../../events";
export function AudioMenu(props: { open: boolean; onClose: () => void; children?: JSX.Element | JSX.Element[] }) {
const [sinks, setSinks] = useState([] as AudioSink[]);
@ -21,6 +19,7 @@ export function AudioMenu(props: { open: boolean; onClose: () => void; children?
const [audioManagerEnabled, setAudioManagerEnabled] = useState(false);
const [activeSource, setActiveSource] = useState(null as AudioSource | null);
const [count, setCount] = useState(0);
const [shortcuts, setShortcuts] = useState({})
/* Preallocate 128 references for the source and sink panels. If the number of references changes, React will give an error */
const sourceRefs = Array(128)
@ -60,6 +59,8 @@ export function AudioMenu(props: { open: boolean; onClose: () => void; children?
AudioManagerStateChangedEvent.on(() => {
setAudioManagerEnabled(getApp().getAudioManager().isRunning());
});
ShortcutsChangedEvent.on((shortcuts) => setShortcuts(shortcuts));
}, []);
/* When the sinks or sources change, use the count state to force a rerender to update the connection lines */
@ -180,7 +181,7 @@ export function AudioMenu(props: { open: boolean; onClose: () => void; children?
if (sink instanceof RadioSink)
return (
<RadioSinkPanel
shortcutKey={shortcutKeys[idx]}
shortcutKeys={shortcuts[`PTT${idx}Active`].toActions()}
key={sink.getName()}
radio={sink}
onExpanded={() => {
@ -218,7 +219,7 @@ export function AudioMenu(props: { open: boolean; onClose: () => void; children?
if (sink instanceof UnitSink)
return (
<UnitSinkPanel
shortcutKey={shortcutKeys[idx]}
shortcutKeys={shortcuts[`PTT${idx}Active`].toActions()}
key={sink.getName()}
sink={sink}
ref={sinkRefs[idx]}

View File

@ -7,7 +7,7 @@ import { faEarListen, faMicrophoneLines } from "@fortawesome/free-solid-svg-icon
import { RadioSink } from "../../../audio/radiosink";
import { getApp } from "../../../olympusapp";
export const RadioSinkPanel = forwardRef((props: { radio: RadioSink; shortcutKey: string; onExpanded: () => void }, ref: ForwardedRef<HTMLDivElement>) => {
export const RadioSinkPanel = forwardRef((props: { radio: RadioSink; shortcutKeys: string[]; onExpanded: () => void }, ref: ForwardedRef<HTMLDivElement>) => {
const [expanded, setExpanded] = useState(false);
useEffect(() => {
@ -39,22 +39,20 @@ export const RadioSinkPanel = forwardRef((props: { radio: RadioSink; shortcutKey
data-expanded={expanded}
/>
</div>
{props.shortcutKey && (
{props.shortcutKeys && (
<>
<kbd
className={`
my-auto ml-auto rounded-lg border border-gray-200 bg-gray-100
px-2 py-1.5 text-xs font-semibold text-gray-800
my-auto ml-auto text-nowrap rounded-lg border border-gray-200
bg-gray-100 px-2 py-1.5 text-xs font-semibold text-gray-800
dark:border-gray-500 dark:bg-gray-600 dark:text-gray-100
`}
>
{props.shortcutKey}
{props.shortcutKeys.flatMap((key, idx, array) => [key, idx < array.length - 1 ? " + " : ""])}
</kbd>
</>
)}
<span className="my-auto w-full">
{props.radio.getName()} {!expanded && `: ${props.radio.getFrequency() / 1e6} MHz ${props.radio.getModulation() ? "FM" : "AM"}`} {}{" "}
</span>
<span className="my-auto w-full">{props.radio.getName()}</span>
<div
className={`
mb-auto ml-auto aspect-square cursor-pointer rounded-md p-2
@ -89,8 +87,12 @@ export const RadioSinkPanel = forwardRef((props: { radio: RadioSink; shortcutKey
className="ml-auto"
checked={props.radio.getPtt()}
icon={faMicrophoneLines}
onClick={() => {
props.radio.setPtt(!props.radio.getPtt());
onClick={() => {}}
onMouseDown={() => {
props.radio.setPtt(true);
}}
onMouseUp={() => {
props.radio.setPtt(false);
}}
tooltip="Talk on frequency"
></OlStateButton>

View File

@ -6,7 +6,7 @@ import { OlStateButton } from "../../components/olstatebutton";
import { faMicrophoneLines } from "@fortawesome/free-solid-svg-icons";
import { OlRangeSlider } from "../../components/olrangeslider";
export const UnitSinkPanel = forwardRef((props: { sink: UnitSink; shortcutKey: string; onExpanded: () => void }, ref: ForwardedRef<HTMLDivElement>) => {
export const UnitSinkPanel = forwardRef((props: { sink: UnitSink; shortcutKeys: string[]; onExpanded: () => void }, ref: ForwardedRef<HTMLDivElement>) => {
const [expanded, setExpanded] = useState(false);
useEffect(() => {
@ -36,20 +36,21 @@ export const UnitSinkPanel = forwardRef((props: { sink: UnitSink; shortcutKey: s
data-expanded={expanded}
/>
</div>
{props.shortcutKey && (<>
<kbd
className={`
my-auto ml-auto rounded-lg border border-gray-200 bg-gray-100 px-2
py-1.5 text-xs font-semibold text-gray-800
dark:border-gray-500 dark:bg-gray-600 dark:text-gray-100
`}
>
{props.shortcutKey}
</kbd>
{props.shortcutKeys && (
<>
<kbd
className={`
my-auto ml-auto text-nowrap rounded-lg border border-gray-200
bg-gray-100 px-2 py-1.5 text-xs font-semibold text-gray-800
dark:border-gray-500 dark:bg-gray-600 dark:text-gray-100
`}
>
{props.shortcutKeys.flatMap((key, idx, array) => [key, idx < array.length - 1 ? " + " : ""])}
</kbd>
</>
)}
<div className="flex w-full overflow-hidden">
<span className="my-auto truncate"> {props.sink.getName()}</span>
<span className="my-auto truncate"> {props.sink.getName()}</span>
</div>
<div
className={`
@ -79,8 +80,12 @@ export const UnitSinkPanel = forwardRef((props: { sink: UnitSink; shortcutKey: s
<OlStateButton
checked={props.sink.getPtt()}
icon={faMicrophoneLines}
onClick={() => {
props.sink.setPtt(!props.sink.getPtt());
onClick={() => {}}
onMouseDown={() => {
props.sink.setPtt(true);
}}
onMouseUp={() => {
props.sink.setPtt(false);
}}
tooltip="Talk on frequency"
></OlStateButton>

View File

@ -1,17 +1,17 @@
import React, { useCallback, useEffect, useState } from "react";
import { faHandPointer, faJetFighter, faMap, IconDefinition } from "@fortawesome/free-solid-svg-icons";
import { faFighterJet, faHandPointer, faJetFighter, faMap, IconDefinition } from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { ContextActionTarget, MAP_OPTIONS_DEFAULTS, NO_SUBSTATE, OlympusState, OlympusSubState, SpawnSubState } from "../../constants/constants";
import { AppStateChangedEvent, MapOptionsChangedEvent } from "../../events";
import { getApp } from "../../olympusapp";
import { MAP_OPTIONS_DEFAULTS, NO_SUBSTATE, OlympusState, OlympusSubState, SpawnSubState } from "../../constants/constants";
import { AppStateChangedEvent, ContextActionSetChangedEvent, MapOptionsChangedEvent, ShortcutsChangedEvent } from "../../events";
import { ContextAction } from "../../unit/contextaction";
import { ContextActionSet } from "../../unit/contextactionset";
export function ControlsPanel(props: {}) {
const [controls, setControls] = useState(
null as
| {
actions: (string | number | IconDefinition)[];
target: IconDefinition | null;
target?: IconDefinition;
text: string;
}[]
| null
@ -19,6 +19,8 @@ export function ControlsPanel(props: {}) {
const [appState, setAppState] = useState(OlympusState.NOT_INITIALIZED);
const [appSubState, setAppSubState] = useState(NO_SUBSTATE as OlympusSubState);
const [mapOptions, setMapOptions] = useState(MAP_OPTIONS_DEFAULTS);
const [shortcuts, setShortcuts] = useState({})
const [contextActionSet, setContextActionSet] = useState(null as null | ContextActionSet)
useEffect(() => {
AppStateChangedEvent.on((state, subState) => {
@ -26,65 +28,61 @@ export function ControlsPanel(props: {}) {
setAppSubState(subState);
});
MapOptionsChangedEvent.on((mapOptions) => setMapOptions({ ...mapOptions }));
ShortcutsChangedEvent.on((shortcuts) => setShortcuts(shortcuts));
ContextActionSetChangedEvent.on((contextActionSet) => setContextActionSet(contextActionSet));
}, []);
const callback = useCallback(() => {
const touch = matchMedia("(hover: none)").matches;
let controls: {
actions: (string | number | IconDefinition)[];
target: IconDefinition | null;
target?: IconDefinition;
text: string;
}[] = [];
const baseControls = [
{
actions: [touch ? faHandPointer : "LMB"],
text: "Select unit",
},
{
actions: [touch ? faHandPointer : "LMB", "Drag"],
text: "Box selection",
},
{
actions: [touch ? faHandPointer : "Wheel", "Drag"],
text: "Move map",
},
];
if (!touch) {
controls.push({
actions: ["Shift", "LMB", "Drag"],
text: "Box selection",
});
}
if (appState === OlympusState.IDLE) {
controls = [
{
actions: [touch ? faHandPointer : "LMB"],
target: faJetFighter,
text: "Select unit",
},
{
actions: touch ? [faHandPointer, "Hold"] : ["RMB"],
target: faMap,
text: "Quick spawn menu",
},
{
actions: touch ? [faHandPointer, "Drag"] : ["Shift", "LMB", "Drag"],
target: faMap,
text: "Box selection",
},
{
actions: [touch ? faHandPointer : "LMB", "Drag"],
target: faMap,
text: "Move map location",
},
];
controls = baseControls;
controls.push({
actions: touch ? [faHandPointer, "Hold"] : ["RMB"],
text: "Quick spawn menu",
});
} else if (appState === OlympusState.SPAWN_CONTEXT) {
controls = [
controls = baseControls;
controls.push(
{
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() ?? {})
controls = Object.values(contextActionSet?.getContextActions() ?? {})
.sort((a: ContextAction, b: ContextAction) => (a.getLabel() > b.getLabel() ? 1 : -1))
.filter((contextAction: ContextAction) => contextAction.getOptions().code)
.map((contextAction: ContextAction) => {
@ -95,53 +93,73 @@ export function ControlsPanel(props: {}) {
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")
.replace("ControlLeft", "Left Ctrl")
.replace("AltLeft", "Left Alt")
.replace("ShiftLeft", "Left Shift")
.replace("ControlRight", "Right Ctrl")
.replace("AltRight", "Right Alt")
.replace("ShiftRight", "Right Shift")
);
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(),
};
});
controls.unshift({
actions: ["RMB"],
text: "Move",
});
controls.push({
actions: ["RMB", "Hold"],
target: faMap,
text: "Show point actions",
});
controls.push({
actions: ["RMB", "Hold"],
target: faFighterJet,
text: "Show unit actions",
});
controls.push({
actions: shortcuts["toggleRelativePositions"]?.toActions(),
text: "Activate group movement",
});
controls.push({
actions: [...shortcuts["toggleRelativePositions"]?.toActions(), "Wheel"],
text: "Rotate formation",
});
}
} else if (appState === OlympusState.SPAWN) {
controls = [
{
actions: [touch ? faHandPointer : "LMB", 2],
target: faMap,
text: appSubState === SpawnSubState.NO_SUBSTATE ? "Close spawn menu" : "Return to spawn menu",
},
{
actions: touch ? [faHandPointer, "Drag"] : ["Shift", "LMB", "Drag"],
target: faMap,
text: "Box selection",
},
{
actions: [touch ? faHandPointer : "LMB", "Drag"],
target: faMap,
text: "Move map location",
},
];
if (appSubState === SpawnSubState.SPAWN_UNIT) {
controls.unshift({
actions: [touch ? faHandPointer : "LMB"],
target: faMap,
text: "Spawn unit",
});
} else if (appSubState === SpawnSubState.SPAWN_EFFECT) {
controls.unshift({
actions: [touch ? faHandPointer : "LMB"],
target: faMap,
text: "Spawn effect",
});
}
} else {
controls = baseControls;
controls.push({
actions: ["LMB"],
text: "Return to idle state"
})
}
setControls(controls);
@ -178,20 +196,23 @@ export function ControlsPanel(props: {}) {
return (
<div key={idx} className="flex gap-1">
<div>
{typeof action === "string" || typeof action === "number" ? (
action
) : (
<FontAwesomeIcon
icon={action}
className={`my-auto ml-auto`}
/>
)}
{typeof action === "string" || typeof action === "number" ? action : <FontAwesomeIcon icon={action} className={`
my-auto ml-auto
`} />}
</div>
{idx < control.actions.length - 1 && typeof control.actions[idx + 1] === "string" && <div>+</div>}
{idx < control.actions.length - 1 && typeof control.actions[idx + 1] === "number" && <div>x</div>}
</div>
);
})}
{control.target && (
<>
<div>+</div>
<div>
<FontAwesomeIcon icon={control.target} />
</div>
</>
)}
</div>
</div>
);

View File

@ -9,6 +9,7 @@ import { BindShortcutRequestEvent, MapOptionsChangedEvent, ShortcutsChangedEvent
import { OlAccordion } from "../components/olaccordion";
import { Shortcut } from "../../shortcut/shortcut";
import { OlSearchBar } from "../components/olsearchbar";
import { FaTrash, FaXmark } from "react-icons/fa6";
const enum Accordion {
NONE,
@ -32,14 +33,12 @@ export function OptionsMenu(props: { open: boolean; onClose: () => void; childre
<Menu title="User preferences" open={props.open} showBackButton={false} onClose={props.onClose}>
<div
className={`
flex flex-col gap-2 p-5 font-normal text-gray-800
flex h-full flex-col justify-end gap-2 p-5 font-normal text-gray-800
dark:text-white
`}
>
<OlAccordion
onClick={() =>
setOpenAccordion(openAccordion === Accordion.NONE ? Accordion.BINDINGS: Accordion.NONE )
}
onClick={() => setOpenAccordion(openAccordion === Accordion.NONE ? Accordion.BINDINGS : Accordion.NONE)}
open={openAccordion === Accordion.BINDINGS}
title="Key bindings"
>
@ -65,10 +64,40 @@ export function OptionsMenu(props: { open: boolean; onClose: () => void; childre
}}
>
<span>{shortcut.getOptions().label}</span>
<span>
{shortcut.getOptions().altKey ? "Alt + " : ""}
{shortcut.getOptions().ctrlKey ? "Ctrl + " : ""}
{shortcut.getOptions().shiftKey ? "Shift + " : ""}
<span className="flex gap-1">
{shortcut.getOptions().altKey ? (
<div className="flex gap-1">
<div className={`text-green-500`}>Alt</div> +{" "}
</div>
) : shortcut.getOptions().altKey === false ? (
<div className={`flex gap-1`}>
<div className={`text-red-500`}>Alt</div> +{" "}
</div>
) : (
""
)}
{shortcut.getOptions().ctrlKey ? (
<div className="flex gap-1">
<div className={`text-green-500`}>Shift</div> +{" "}
</div>
) : shortcut.getOptions().ctrlKey === false ? (
<div className={`flex gap-1`}>
<div className={`text-red-500`}>Shift</div> +{" "}
</div>
) : (
""
)}
{shortcut.getOptions().shiftKey ? (
<div className="flex gap-1">
<div className={`text-green-500`}>Ctrl</div> +{" "}
</div>
) : shortcut.getOptions().shiftKey === false ? (
<div className={`flex gap-1`}>
<div className={`text-red-500`}>Ctrl</div> +{" "}
</div>
) : (
""
)}
{shortcut.getOptions().code}
</span>
</div>
@ -77,7 +106,11 @@ export function OptionsMenu(props: { open: boolean; onClose: () => void; childre
</div>
</OlAccordion>
<OlAccordion onClick={() => setOpenAccordion(openAccordion === Accordion.NONE ? Accordion.MAP_OPTIONS: Accordion.NONE )} open={openAccordion === Accordion.MAP_OPTIONS} title="Map options">
<OlAccordion
onClick={() => setOpenAccordion(openAccordion === Accordion.NONE ? Accordion.MAP_OPTIONS : Accordion.NONE)}
open={openAccordion === Accordion.MAP_OPTIONS}
title="Map options"
>
<div
className={`
group flex flex-row rounded-md justify-content cursor-pointer
@ -133,17 +166,7 @@ export function OptionsMenu(props: { open: boolean; onClose: () => void; childre
<OlCheckbox checked={mapOptions.hideUnitsShortRangeRings} onChange={() => {}}></OlCheckbox>
<span>Hide Short range Rings</span>
</div>
<div
className={`
group flex flex-row gap-4 rounded-md justify-content
cursor-pointer p-2 text-sm
dark:hover:bg-olympus-400
`}
onClick={() => getApp().getMap().setOption("keepRelativePositions", !mapOptions.keepRelativePositions)}
>
<OlCheckbox checked={mapOptions.keepRelativePositions} onChange={() => {}}></OlCheckbox>
<span>Keep units relative positions</span>
</div>
<div
className={`
group flex flex-row gap-4 rounded-md justify-content
@ -168,7 +191,11 @@ export function OptionsMenu(props: { open: boolean; onClose: () => void; childre
</div>
</OlAccordion>
<OlAccordion onClick={() => setOpenAccordion(openAccordion === Accordion.NONE ? Accordion.CAMERA_PLUGIN: Accordion.NONE )} open={openAccordion === Accordion.CAMERA_PLUGIN} title="Camera plugin options">
<OlAccordion
onClick={() => setOpenAccordion(openAccordion === Accordion.NONE ? Accordion.CAMERA_PLUGIN : Accordion.NONE)}
open={openAccordion === Accordion.CAMERA_PLUGIN}
title="Camera plugin options"
>
<hr
className={`
m-2 my-1 w-auto border-[1px] bg-gray-700
@ -231,6 +258,41 @@ export function OptionsMenu(props: { open: boolean; onClose: () => void; childre
</div>
</div>
</OlAccordion>
<div className="mt-auto flex">
<button
type="button"
onClick={() => getApp().resetProfile()}
className={`
mb-2 me-2 flex content-center items-center gap-2 rounded-sm
border-[1px] bg-blue-700 px-5 py-2.5 text-sm font-medium
text-white
dark:border-red-600 dark:bg-gray-800 dark:text-gray-400
dark:hover:bg-gray-700 dark:focus:ring-blue-800
focus:outline-none focus:ring-4 focus:ring-blue-300
hover:bg-blue-800
`}
>
Reset profile
<FaXmark />
</button>
<button
type="button"
onClick={() => getApp().resetAllProfiles()}
className={`
mb-2 me-2 flex content-center items-center gap-2 rounded-sm
border-[1px] bg-blue-700 px-5 py-2.5 text-sm font-medium
text-white
dark:border-red-600 dark:bg-red-800 dark:text-gray-400
dark:hover:bg-red-700 dark:focus:ring-blue-800
focus:outline-none focus:ring-4 focus:ring-blue-300
hover:bg-red-800
`}
>
Reset all profiles
<FaTrash />
</button>
</div>
</div>
</Menu>
);

View File

@ -0,0 +1,74 @@
import React, { useEffect, useState } from "react";
import { AudioSinksChangedEvent } from "../../events";
import { AudioSink } from "../../audio/audiosink";
import { RadioSink } from "../../audio/radiosink";
import { FaJetFighter, FaRadio } from "react-icons/fa6";
import { OlStateButton } from "../components/olstatebutton";
import { UnitSink } from "../../audio/unitsink";
export function RadiosSummaryPanel(props: {}) {
const [audioSinks, setAudioSinks] = useState([] as AudioSink[]);
useEffect(() => {
AudioSinksChangedEvent.on((audioSinks) => setAudioSinks(audioSinks));
}, []);
return (
<>
{audioSinks.length > 0 && (
<div
className={`
absolute bottom-[20px] right-[700px] flex w-fit flex-col
items-center justify-between gap-2 rounded-lg bg-gray-200 p-3
text-sm backdrop-blur-lg backdrop-grayscale
dark:bg-olympus-800/90 dark:text-gray-200
`}
>
<div className="flex w-full items-center justify-between gap-2">
<FaRadio className="text-xl" />
{audioSinks
.filter((audioSinks) => audioSinks instanceof RadioSink)
.map((radioSink, idx) => {
return (
<OlStateButton
checked={radioSink.getReceiving()}
onClick={() => {}}
onMouseDown={() => {
radioSink.setPtt(true);
}}
onMouseUp={() => {
radioSink.setPtt(false);
}}
tooltip="Click to talk, lights up when receiving"
>
<span className={`font-bold text-gray-200`}>{idx + 1}</span>
</OlStateButton>
);
})}
<FaJetFighter className="text-xl" />
{audioSinks
.filter((audioSinks) => audioSinks instanceof UnitSink)
.map((radioSink, idx) => {
return (
<OlStateButton
checked={false}
onClick={() => {}}
onMouseDown={() => {
radioSink.setPtt(true);
}}
onMouseUp={() => {
radioSink.setPtt(false);
}}
tooltip="Click to talk"
>
<span className={`font-bold text-gray-200`}>{idx + 1}</span>
</OlStateButton>
);
})}
</div>
</div>
)}
</>
);
}

View File

@ -8,6 +8,7 @@ import { FaInfoCircle } from "react-icons/fa";
import { FaChevronDown, FaChevronLeft, FaChevronRight, FaChevronUp } from "react-icons/fa6";
import { OlympusState } from "../../constants/constants";
import { AppStateChangedEvent, ContextActionChangedEvent, ContextActionSetChangedEvent, MapOptionsChangedEvent } from "../../events";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
export function UnitControlBar(props: {}) {
const [appState, setAppState] = useState(OlympusState.NOT_INITIALIZED);
@ -111,24 +112,20 @@ export function UnitControlBar(props: {}) {
{contextAction && (
<div
className={`
absolute left-[50%] top-16 flex min-w-[300px]
translate-x-[calc(-50%+2rem)] items-center gap-2 rounded-md
bg-gray-200 p-4
absolute left-[50%] top-16 flex translate-x-[calc(-50%+2rem)]
items-center gap-2 rounded-md bg-gray-200 p-4
dark:bg-olympus-800
`}
>
<FaInfoCircle
<FontAwesomeIcon
icon={contextAction.getIcon()}
className={`
mr-2 hidden min-w-8 text-sm text-blue-500
mr-2 hidden text-xl text-blue-500
md:block
`}
/>
<div
className={`
px-2
dark:text-gray-400
md:border-l-[1px] md:px-5
`}
className={`text-gray-200`}
>
{contextAction.getDescription()}
</div>

View File

@ -24,12 +24,13 @@ import { ProtectionPrompt } from "./modals/protectionprompt";
import { KeybindModal } from "./modals/keybindmodal";
import { UnitExplosionMenu } from "./panels/unitexplosionmenu";
import { JTACMenu } from "./panels/jtacmenu";
import { AppStateChangedEvent, MapOptionsChangedEvent } from "../events";
import { AppStateChangedEvent } from "../events";
import { GameMasterMenu } from "./panels/gamemastermenu";
import { InfoBar } from "./panels/infobar";
import { HotGroupBar } from "./panels/hotgroupsbar";
import { SpawnContextMenu } from "./contextmenus/spawncontextmenu";
import { CoordinatesPanel } from "./panels/coordinatespanel";
import { RadiosSummaryPanel } from "./panels/radiossummarypanel";
export type OlympusUIState = {
mainMenuVisible: boolean;
@ -105,6 +106,7 @@ export function UI() {
<MiniMapPanel />
<ControlsPanel />
<CoordinatesPanel />
<RadiosSummaryPanel />
<UnitControlBar />
<SideBar />

View File

@ -40,6 +40,7 @@ import {
UnitControlSubState,
ContextActions,
ContextActionTarget,
SHORT_PRESS_MILLISECONDS,
} from "../constants/constants";
import { DataExtractor } from "../server/dataextractor";
import { Weapon } from "../weapon/weapon";
@ -157,10 +158,13 @@ export abstract class Unit extends CustomMarker {
#detectionMethods: number[] = [];
/* Inputs timers */
#mouseCooldownTimer: number = 0;
#shortPressTimer: number = 0;
#isMouseOnCooldown: boolean = false;
#isMouseDown: boolean = false;
#debounceTimeout: number | null = null;
#isLeftMouseDown: boolean = false;
#isRightMouseDown: boolean = false;
#leftMouseDownEpoch: number = 0;
#rightMouseDownEpoch: number = 0;
#leftMouseDownTimeout: number = 0;
#rightMouseDownTimeout: number = 0;
/* Getters for backend driven data */
getAlive() {
@ -342,10 +346,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("mouseup", (e: any) => this.#onMouseUp(e));
this.on("mousedown", (e: any) => this.#onMouseDown(e));
this.on("dblclick", (e: any) => this.#onDoubleClick(e));
this.on("click", (e: any) => e.originalEvent.preventDefault());
this.on("contextmenu", (e: any) => e.originalEvent.preventDefault());
this.on("mouseover", () => {
if (this.belongsToCommandedCoalition()) this.setHighlighted(true);
@ -1281,58 +1286,72 @@ export abstract class Unit extends CustomMarker {
}
/***********************************************/
#onMouseDown(e: any) {
if (e.originalEvent.button === 2) return;
this.#isMouseDown = true;
DomEvent.stop(e);
DomEvent.preventDefault(e);
e.originalEvent.stopImmediatePropagation();
if (this.#isMouseOnCooldown) return;
this.#shortPressTimer = window.setTimeout(() => {
/* If the mouse is no longer being pressed, execute the short press action */
if (!this.#isMouseDown) this.#onLeftClick(e);
}, 200);
}
#onMouseUp(e: any) {
if (e.originalEvent.button === 2) return;
if (e.originalEvent?.button === 0) {
DomEvent.stop(e);
DomEvent.preventDefault(e);
e.originalEvent.stopImmediatePropagation();
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()?.getTarget() === ContextActionTarget.UNIT) {
getApp().getMap().executeContextAction(this, null, e.originalEvent);
} else {
if (!e.originalEvent.ctrlKey) getApp().getUnitsManager().deselectAllUnits();
this.setSelected(!this.getSelected());
if (Date.now() - this.#leftMouseDownEpoch < SHORT_PRESS_MILLISECONDS) this.#onLeftShortClick(e);
this.#isLeftMouseDown = false;
} else if (e.originalEvent?.button === 2) {
if (getApp().getState() === OlympusState.UNIT_CONTROL && getApp().getMap().getContextAction()?.getTarget() !== ContextActionTarget.POINT) {
DomEvent.stop(e);
DomEvent.preventDefault(e);
e.originalEvent.stopImmediatePropagation();
}
if (Date.now() - this.#rightMouseDownEpoch < SHORT_PRESS_MILLISECONDS) this.#onRightShortClick(e);
this.#isRightMouseDown = false;
}
}
#onRightClick(e: any) {
console.log(`Right click on ${this.getUnitName()}`);
#onMouseDown(e: any) {
if (e.originalEvent?.button === 0) {
DomEvent.stop(e);
DomEvent.preventDefault(e);
e.originalEvent.stopImmediatePropagation();
this.#isLeftMouseDown = true;
this.#leftMouseDownEpoch = Date.now();
} else if (e.originalEvent?.button === 2) {
if (getApp().getState() === OlympusState.UNIT_CONTROL && getApp().getMap().getContextAction()?.getTarget() !== ContextActionTarget.POINT) {
DomEvent.stop(e);
DomEvent.preventDefault(e);
e.originalEvent.stopImmediatePropagation();
}
this.#isRightMouseDown = true;
this.#rightMouseDownEpoch = Date.now();
this.#rightMouseDownTimeout = window.setTimeout(() => {
this.#onRightLongClick(e);
}, SHORT_PRESS_MILLISECONDS);
}
}
#onLeftShortClick(e: any) {
DomEvent.stop(e);
DomEvent.preventDefault(e);
e.originalEvent.stopImmediatePropagation();
if (this.#debounceTimeout) window.clearTimeout(this.#debounceTimeout);
this.#debounceTimeout = window.setTimeout(() => {
console.log(`Left short click on ${this.getUnitName()}`);
if (!e.originalEvent.ctrlKey) getApp().getUnitsManager().deselectAllUnits();
this.setSelected(!this.getSelected());
}, SHORT_PRESS_MILLISECONDS);
}
#onRightShortClick(e: any) {
console.log(`Right short click on ${this.getUnitName()}`);
if (getApp().getState() === OlympusState.UNIT_CONTROL && getApp().getMap().getContextAction()?.getTarget() === ContextActionTarget.UNIT)
getApp().getMap().executeContextAction(this, null, e.originalEvent);
}
#onRightLongClick(e: any) {
console.log(`Right long click on ${this.getUnitName()}`);
if (getApp().getState() === OlympusState.UNIT_CONTROL && !getApp().getMap().getContextAction()) {
DomEvent.stop(e);
@ -1347,18 +1366,17 @@ export abstract class Unit extends CustomMarker {
#onDoubleClick(e: any) {
DomEvent.stop(e);
DomEvent.preventDefault(e);
e.originalEvent.stopImmediatePropagation();
console.log(`Double click on ${this.getUnitName()}`);
window.clearTimeout(this.#shortPressTimer);
if (this.#debounceTimeout) window.clearTimeout(this.#debounceTimeout);
/* 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);
});
}
#updateMarker() {

View File

@ -79,36 +79,66 @@ export class UnitsManager {
},
code: "KeyA",
ctrlKey: true,
shiftKey: false,
altKey: false
})
.addShortcut("copyUnits", {
label: "Copy units",
keyUpCallback: () => this.copy(),
code: "KeyC",
ctrlKey: true,
shiftKey: false,
altKey: false
})
.addShortcut("pasteUnits", {
label: "Paste units",
keyUpCallback: () => this.paste(),
code: "KeyV",
ctrlKey: true,
shiftKey: false,
altKey: false
});
const digits = ["Digit1", "Digit2", "Digit3", "Digit4", "Digit5", "Digit6", "Digit7", "Digit8", "Digit9"];
digits.forEach((code, idx) => {
getApp()
.getShortcutManager()
.addShortcut(`hotgroup${idx}`, {
label: `Hotgroup ${idx} management`,
.addShortcut(`hotgroup${idx + 1}only`, {
label: `Hotgroup ${idx + 1} (Select only)`,
keyUpCallback: (ev: KeyboardEvent) => {
if (ev.ctrlKey && ev.shiftKey) this.selectUnitsByHotgroup(parseInt(ev.code.substring(5)), false);
// "Select hotgroup X in addition to any units already selected"
else if (ev.ctrlKey && !ev.shiftKey) this.setHotgroup(parseInt(ev.code.substring(5)));
// "These selected units are hotgroup X (forget any previous membership)"
else if (!ev.ctrlKey && ev.shiftKey) this.addToHotgroup(parseInt(ev.code.substring(5)));
// "Add (append) these units to hotgroup X (in addition to any existing members)"
else this.selectUnitsByHotgroup(parseInt(ev.code.substring(5))); // "Select hotgroup X, deselect any units not in it."
this.selectUnitsByHotgroup(parseInt(ev.code.substring(5)));
},
code: code,
shiftKey: false,
altKey: false,
ctrlKey: false
}).addShortcut(`hotgroup${idx + 1}add`, {
label: `Hotgroup ${idx + 1} (Add to)`,
keyUpCallback: (ev: KeyboardEvent) => {
this.addToHotgroup(parseInt(ev.code.substring(5)));
},
code: code,
shiftKey: true,
altKey: false,
ctrlKey: false
}).addShortcut(`hotgroup${idx + 1}set`, {
label: `Hotgroup ${idx + 1} (Set)`,
keyUpCallback: (ev: KeyboardEvent) => {
this.setHotgroup(parseInt(ev.code.substring(5)));
},
code: code,
ctrlKey: true,
altKey: false,
shiftKey: false
}).addShortcut(`hotgroup${idx + 1}also`, {
label: `Hotgroup ${idx + 1} (Select also)`,
keyUpCallback: (ev: KeyboardEvent) => {
this.selectUnitsByHotgroup(parseInt(ev.code.substring(5)), false);
},
code: code,
ctrlKey: true,
shiftKey: true,
altKey: false
});
});

View File

@ -1,33 +1,74 @@
import express = require('express');
import fs = require('fs');
import express = require("express");
import fs = require("fs");
const router = express.Router();
module.exports = function (configLocation) {
router.get('/config', function (req, res, next) {
if (fs.existsSync(configLocation)) {
let rawdata = fs.readFileSync(configLocation, "utf-8");
const config = JSON.parse(rawdata);
res.send(JSON.stringify({frontend:{...config.frontend}, audio:{...(config.audio ?? {})}, profiles: {...(config.profiles ?? {})} }));
res.end()
} else {
res.sendStatus(404);
}
});
router.get("/config", function (req, res, next) {
if (fs.existsSync(configLocation)) {
let rawdata = fs.readFileSync(configLocation, "utf-8");
const config = JSON.parse(rawdata);
res.send(
JSON.stringify({
frontend: { ...config.frontend },
audio: { ...(config.audio ?? {}) },
profiles: { ...(config.profiles ?? {}) },
})
);
res.end();
} else {
res.sendStatus(404);
}
});
router.put('/profile/:profileName', function (req, res, next) {
if (fs.existsSync(configLocation)) {
let rawdata = fs.readFileSync(configLocation, "utf-8");
const config = JSON.parse(rawdata);
if (config.profiles === undefined)
config.profiles = {}
config.profiles[req.params.profileName] = req.body;
fs.writeFileSync(configLocation, JSON.stringify(config, null, 2), "utf-8");
res.end()
} else {
res.sendStatus(404);
}
});
router.put("/profile/:profileName", function (req, res, next) {
if (fs.existsSync(configLocation)) {
let rawdata = fs.readFileSync(configLocation, "utf-8");
const config = JSON.parse(rawdata);
if (config.profiles === undefined) config.profiles = {};
config.profiles[req.params.profileName] = req.body;
fs.writeFileSync(
configLocation,
JSON.stringify(config, null, 2),
"utf-8"
);
res.end();
} else {
res.sendStatus(404);
}
});
return router;
}
router.put("/profile/reset/:profileName", function (req, res, next) {
if (fs.existsSync(configLocation)) {
let rawdata = fs.readFileSync(configLocation, "utf-8");
const config = JSON.parse(rawdata);
if (config.profiles[req.params.profileName])
delete config.profiles[req.params.profileName];
fs.writeFileSync(
configLocation,
JSON.stringify(config, null, 2),
"utf-8"
);
res.end();
} else {
res.sendStatus(404);
}
});
router.put("/profile/resetall", function (req, res, next) {
if (fs.existsSync(configLocation)) {
let rawdata = fs.readFileSync(configLocation, "utf-8");
const config = JSON.parse(rawdata);
config.profiles = {};
fs.writeFileSync(
configLocation,
JSON.stringify(config, null, 2),
"utf-8"
);
res.end();
} else {
res.sendStatus(404);
}
});
return router;
};