Merge branch 'release-candidate' into features/redgreen-unit

This commit is contained in:
Pax1601 2025-03-24 23:02:04 +01:00
commit 70b6143fd9
21 changed files with 778 additions and 335 deletions

View File

@ -23,7 +23,7 @@ jobs:
vcpkg integrate install
- name: Setup Node.js
uses: actions/setup-node@v2
uses: actions/setup-node@v4
- name: Build
working-directory: .
@ -41,5 +41,6 @@ jobs:
with:
name: zip_only_package
path: ./zip
include-hidden-files: true

View File

@ -892,3 +892,19 @@ export class WeaponsRefreshedEvent {
if (DEBUG) console.log(`Event ${this.name} dispatched`);
}
}
export class CoordinatesFreezeEvent {
static on(callback: () => void) {
document.addEventListener(
this.name,
(ev: CustomEventInit) => {
callback();
}
)
}
static dispatch() {
document.dispatchEvent(new CustomEvent(this.name));
if (DEBUG) console.log(`Event ${this.name} dispatched`);
}
}

View File

@ -16,13 +16,13 @@ export var BoxSelect = Handler.extend({
},
addHooks: function () {
DomEvent.on(this._container, "mousedown", this._onMouseDown, this);
DomEvent.on(this._container, "touchstart", this._onMouseDown, this);
if ("ontouchstart" in window) DomEvent.on(this._container, "touchend", this._onMouseDown, this);
else DomEvent.on(this._container, "mousedown", this._onMouseDown, this);
},
removeHooks: function () {
DomEvent.off(this._container, "mousedown", this._onMouseDown, this);
DomEvent.off(this._container, "touchend", this._onMouseDown, this);
if ("ontouchstart" in window) DomEvent.off(this._container, "touchstart", this._onMouseDown, this);
else DomEvent.off(this._container, "mousedown", this._onMouseDown, this);
},
moved: function () {
@ -48,18 +48,29 @@ export var BoxSelect = Handler.extend({
if (e.type === "touchstart") this._startPoint = this._map.mouseEventToContainerPoint(e.touches[0]);
else this._startPoint = this._map.mouseEventToContainerPoint(e);
DomEvent.on(
//@ts-ignore
document,
{
contextmenu: DomEvent.stop,
touchmove: this._onMouseMove,
touchend: this._onMouseUp,
mousemove: this._onMouseMove,
mouseup: this._onMouseUp,
},
this
);
if ("ontouchstart" in window) {
DomEvent.on(
//@ts-ignore
document,
{
contextmenu: DomEvent.stop,
touchmove: this._onMouseMove,
touchend: this._onMouseUp,
},
this
);
} else {
DomEvent.on(
//@ts-ignore
document,
{
contextmenu: DomEvent.stop,
mousemove: this._onMouseMove,
mouseup: this._onMouseUp,
},
this
);
}
} else {
return false;
}
@ -109,18 +120,29 @@ export var BoxSelect = Handler.extend({
DomUtil.enableImageDrag();
this._map.dragging.enable();
DomEvent.off(
//@ts-ignore
document,
{
contextmenu: DomEvent.stop,
touchmove: this._onMouseMove,
touchend: this._onMouseUp,
mousemove: this._onMouseMove,
mouseup: this._onMouseUp,
},
this
);
if ("ontouchstart" in window) {
DomEvent.off(
//@ts-ignore
document,
{
contextmenu: DomEvent.stop,
touchmove: this._onMouseMove,
touchend: this._onMouseUp,
},
this
);
} else {
DomEvent.off(
//@ts-ignore
document,
{
contextmenu: DomEvent.stop,
mousemove: this._onMouseMove,
mouseup: this._onMouseUp,
},
this
);
}
this._resetState();
},

View File

@ -3,7 +3,7 @@ import { getApp } from "../olympusapp";
import { BoxSelect } from "./boxselect";
import { Airbase } from "../mission/airbase";
import { Unit } from "../unit/unit";
import { areaContains, deepCopyTable, deg2rad, getGroundElevation } from "../other/utils";
import { areaContains, deepCopyTable, deg2rad, getGroundElevation, getMagvar, rad2deg } from "../other/utils";
import { TemporaryUnitMarker } from "./markers/temporaryunitmarker";
import { ClickableMiniMap } from "./clickableminimap";
import {
@ -49,6 +49,7 @@ import {
ConfigLoadedEvent,
ContextActionChangedEvent,
ContextActionSetChangedEvent,
CoordinatesFreezeEvent,
HiddenTypesChangedEvent,
MapContextMenuRequestEvent,
MapOptionsChangedEvent,
@ -69,6 +70,7 @@ import { ContextActionSet } from "../unit/contextactionset";
import { SmokeMarker } from "./markers/smokemarker";
import { Measure } from "./measure";
import { FlakMarker } from "./markers/flakmarker";
import { MapMouseHandler } from "./mapMouseHandler";
/* Register the handler for the box selection */
L.Map.addInitHook("addHandler", "boxSelect", BoxSelect);
@ -109,14 +111,8 @@ export class Map extends L.Map {
#isDragging: boolean = false;
#isSelecting: boolean = false;
#originalMouseClickLatLng: L.LatLng | null = null;
#debounceTimeout: number | null = null;
#isLeftMouseDown: boolean = false;
#isRightMouseDown: boolean = false;
#leftMouseDownEpoch: number = 0;
#rightMouseDownEpoch: number = 0;
#leftMouseDownTimeout: number = 0;
#rightMouseDownTimeout: number = 0;
#mouseHandler: MapMouseHandler = new MapMouseHandler(this);
#lastMousePosition: L.Point = new L.Point(0, 0);
#lastMouseCoordinates: L.LatLng = new L.LatLng(0, 0);
#previousZoom: number = 0;
@ -138,7 +134,7 @@ export class Map extends L.Map {
#destinationPreviewMarkers: { [key: number]: TemporaryUnitMarker | TargetMarker } = {};
#destinationRotation: number = 0;
#isRotatingDestination: boolean = false;
#destionationWasRotated: boolean = false;
#destinationRotationCenter: L.LatLng = new L.LatLng(0, 0);
/* Unit context actions */
#contextActionSet: null | ContextActionSet = null;
@ -208,22 +204,20 @@ export class Map extends L.Map {
this.on("selectionstart", (e: any) => this.#onSelectionStart(e));
this.on("selectionend", (e: any) => this.#onSelectionEnd(e));
this.on("mouseup", (e: any) => this.#onMouseUp(e));
this.on("touchend", (e: any) => this.#onMouseUp(e));
this.on("mousedown", (e: any) => this.#onMouseDown(e));
this.on("touchstart", (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("mousemove", (e: any) => this.#onMouseMove(e));
this.on("move", (e: any) => this.#onMapMove(e));
/* 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);
this.#mouseHandler.leftMousePressed = (e: L.LeafletMouseEvent) => this.#onLeftMousePressed(e);
this.#mouseHandler.leftMouseReleased = (e: L.LeafletMouseEvent) => this.#onLeftMouseReleased(e);
this.#mouseHandler.rightMousePressed = (e: L.LeafletMouseEvent) => this.#onRightMousePressed(e);
this.#mouseHandler.rightMouseReleased = (e: L.LeafletMouseEvent) => this.#onRightMouseReleased(e);
this.#mouseHandler.mouseWheelPressed = (e: L.LeafletMouseEvent) => this.#onMouseWheelPressed(e);
this.#mouseHandler.mouseWheelReleased = (e: L.LeafletMouseEvent) => this.#onMouseWheelReleased(e);
this.#mouseHandler.leftMouseShortClick = (e: L.LeafletMouseEvent) => this.#onLeftMouseShortClick(e);
this.#mouseHandler.rightMouseShortClick = (e: L.LeafletMouseEvent) => this.#onRightMouseShortClick(e);
this.#mouseHandler.leftMouseLongClick = (e: L.LeafletMouseEvent) => this.#onLeftMouseLongClick(e);
this.#mouseHandler.rightMouseLongClick = (e: L.LeafletMouseEvent) => this.#onRightMouseLongClick(e);
this.#mouseHandler.leftMouseDoubleClick = (e: L.LeafletMouseEvent) => this.#onLeftMouseDoubleClick(e);
this.#mouseHandler.mouseMove = (e: L.LeafletMouseEvent) => this.#onMouseMove(e);
/* Event listeners */
AppStateChangedEvent.on((state, subState) => this.#onStateChanged(state, subState));
@ -404,14 +398,14 @@ export class Map extends L.Map {
altKey: false,
ctrlKey: false,
})
.addShortcut("toggleRelativePositions", {
label: "Toggle group movement mode",
keyUpCallback: () => this.setKeepRelativePositions(false),
keyDownCallback: () => this.setKeepRelativePositions(true),
code: "AltLeft",
shiftKey: false,
ctrlKey: false,
})
//.addShortcut("toggleRelativePositions", {
// label: "Toggle group movement mode",
// keyUpCallback: () => this.setKeepRelativePositions(false),
// keyDownCallback: () => this.setKeepRelativePositions(true),
// code: "AltLeft",
// shiftKey: false,
// ctrlKey: false,
//})
.addShortcut("toggleSelectionEnabled", {
label: "Toggle box selection",
keyUpCallback: () => this.setSelectionEnabled(false),
@ -802,8 +796,6 @@ export class Map extends L.Map {
setKeepRelativePositions(keepRelativePositions: boolean) {
this.#keepRelativePositions = keepRelativePositions;
this.#updateDestinationPreviewMarkers();
if (keepRelativePositions) this.scrollWheelZoom.disable();
else this.scrollWheelZoom.enable();
}
getKeepRelativePositions() {
@ -959,7 +951,10 @@ export class Map extends L.Map {
}
#onDragEnd(e: any) {
this.#isDragging = false;
/* Delay the drag end event so that any other event in the queue still sees the map in dragging mode */
window.setTimeout(() => {
this.#isDragging = false;
}, SHORT_PRESS_MILLISECONDS + 100);
}
#onSelectionStart(e: any) {
@ -975,200 +970,219 @@ export class Map extends L.Map {
/* Delay the event so that any other event in the queue still sees the map in selection mode */
window.setTimeout(() => {
this.#isSelecting = false;
}, 300);
}, SHORT_PRESS_MILLISECONDS + 100);
}
#onMouseUp(e: any) {
#onLeftMouseReleased(e: any) {
this.dragging.enable();
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;
} else if (e.originalEvent?.button === 1) {
getApp().setState(getApp().getState() === OlympusState.MEASURE ? OlympusState.IDLE : OlympusState.MEASURE);
if (getApp().getState() === OlympusState.MEASURE) {
const newMeasure = new Measure(this);
const previousMeasure = this.#measures[this.#measures.length - 1];
this.#measures.push(newMeasure);
newMeasure.onClick(e.latlng);
if (previousMeasure && previousMeasure.isActive()) {
previousMeasure.finish();
previousMeasure.hideEndMarker();
newMeasure.onMarkerMoved = (startLatLng, endLatLng) => {
previousMeasure.moveMarkers(null, startLatLng);
};
}
if (this.#isRotatingDestination && getApp().getState() === OlympusState.UNIT_CONTROL && this.getContextAction() !== null) {
this.executeContextAction(null, this.#destinationRotationCenter, e.originalEvent);
}
this.#isRotatingDestination = false;
this.setKeepRelativePositions(false);
/* Delay the event so that any other event in the queue still sees the map in selection mode */
window.setTimeout(() => {
this.setSelectionEnabled(false);
this.#isSelecting = false;
}, SHORT_PRESS_MILLISECONDS + 100);
}
#onMouseWheelReleased(e: any) {
this.dragging.enable();
getApp().setState(getApp().getState() === OlympusState.MEASURE ? OlympusState.IDLE : OlympusState.MEASURE);
if (getApp().getState() === OlympusState.MEASURE) {
const newMeasure = new Measure(this);
const previousMeasure = this.#measures[this.#measures.length - 1];
this.#measures.push(newMeasure);
newMeasure.onClick(e.latlng);
if (previousMeasure && previousMeasure.isActive()) {
previousMeasure.finish();
previousMeasure.hideEndMarker();
newMeasure.onMarkerMoved = (startLatLng, endLatLng) => {
previousMeasure.moveMarkers(null, startLatLng);
};
}
}
}
#onMouseDown(e: any) {
if (e.originalEvent?.button === 1) {
this.dragging.disable();
} // Disable dragging when right clicking
#onRightMouseReleased(e: any) {
this.dragging.enable();
this.#originalMouseClickLatLng = e.latlng;
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);
if (this.#isRotatingDestination && getApp().getState() === OlympusState.UNIT_CONTROL) {
this.executeDefaultContextAction(null, this.#destinationRotationCenter, e.originalEvent);
}
this.#isRotatingDestination = false;
this.setKeepRelativePositions(false);
}
#onLeftMousePressed(e: any) {
if (getApp().getState() === OlympusState.UNIT_CONTROL && getApp().getSubState() === UnitControlSubState.MAP_CONTEXT_MENU) {
getApp().setState(OlympusState.UNIT_CONTROL);
}
}
#onMouseWheel(e: any) {
if (this.#keepRelativePositions) {
this.#destinationRotation += e.deltaY / 20;
this.#moveDestinationPreviewMarkers();
}
#onMouseWheelPressed(e: any) {}
#onRightMousePressed(e: any) {
this.dragging.disable();
}
#onLeftShortClick(e: L.LeafletMouseEvent) {
if (Date.now() - this.#leftMouseDownEpoch < SHORT_PRESS_MILLISECONDS) {
this.#debounceTimeout = window.setTimeout(() => {
if (!this.#isSelecting) {
console.log(`Left short click at ${e.latlng}`);
#onLeftMouseShortClick(e: L.LeafletMouseEvent) {
CoordinatesFreezeEvent.dispatch();
if (this.#pasteEnabled) {
getApp().getUnitsManager().paste(e.latlng);
}
if (this.#isDragging || this.#isSelecting) return;
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;
this.#spawnRequestTable.unit.heading = deg2rad(this.#spawnHeading);
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",
false,
hash
);
}
);
if (this.#pasteEnabled) {
getApp().getUnitsManager().paste(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;
this.#spawnRequestTable.unit.heading = deg2rad(this.#spawnHeading);
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",
false,
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");
}
}
}
} else if (getApp().getState() === OlympusState.DRAW) {
getApp().getCoalitionAreasManager().onLeftShortClick(e);
} 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().getState() === OlympusState.UNIT_CONTROL) {
if (this.#contextAction !== null) this.executeContextAction(null, e.latlng, e.originalEvent);
else if (getApp().getSubState() === NO_SUBSTATE) getApp().setState(OlympusState.IDLE);
else getApp().setState(OlympusState.UNIT_CONTROL);
} else if (getApp().getState() === OlympusState.MEASURE) {
const newMeasure = new Measure(this);
const previousMeasure = this.#measures[this.#measures.length - 1];
this.#measures.push(newMeasure);
newMeasure.onClick(e.latlng);
if (previousMeasure && previousMeasure.isActive()) {
previousMeasure.finish();
previousMeasure.hideEndMarker();
newMeasure.onMarkerMoved = (startLatLng, endLatLng) => {
previousMeasure.moveMarkers(null, startLatLng);
};
}
} else {
if (getApp().getSubState() === NO_SUBSTATE) getApp().setState(OlympusState.IDLE);
else getApp().setState(OlympusState.UNIT_CONTROL);
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");
}
}
if (this.#debounceTimeout) window.clearTimeout(this.#debounceTimeout);
this.#debounceTimeout = null;
}, DEBOUNCE_MILLISECONDS);
}
} else if (getApp().getState() === OlympusState.DRAW) {
getApp().getCoalitionAreasManager().onLeftShortClick(e);
} 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().getState() === OlympusState.UNIT_CONTROL) {
if (this.#contextAction !== null) this.executeContextAction(null, e.latlng, e.originalEvent);
else if (getApp().getSubState() === NO_SUBSTATE) getApp().setState(OlympusState.IDLE);
else getApp().setState(OlympusState.UNIT_CONTROL);
} else if (getApp().getState() === OlympusState.MEASURE) {
const newMeasure = new Measure(this);
const previousMeasure = this.#measures[this.#measures.length - 1];
this.#measures.push(newMeasure);
newMeasure.onClick(e.latlng);
if (previousMeasure && previousMeasure.isActive()) {
previousMeasure.finish();
previousMeasure.hideEndMarker();
newMeasure.onMarkerMoved = (startLatLng, endLatLng) => {
previousMeasure.moveMarkers(null, startLatLng);
};
}
} else {
if (getApp().getSubState() === NO_SUBSTATE) getApp().setState(OlympusState.IDLE);
else getApp().setState(OlympusState.UNIT_CONTROL);
}
}
#onRightShortClick(e: L.LeafletMouseEvent) {
console.log(`Right short click at ${e.latlng}`);
#onLeftMouseLongClick(e: any) {
if (this.#isDragging || this.#isSelecting) return;
console.log(`Left long click at ${e.latlng}`);
window.clearTimeout(this.#rightMouseDownTimeout);
if (getApp().getState() === OlympusState.UNIT_CONTROL) {
if (!this.getContextAction()) {
getApp().setState(OlympusState.UNIT_CONTROL, UnitControlSubState.MAP_CONTEXT_MENU);
MapContextMenuRequestEvent.dispatch(e.latlng);
} else {
this.#destinationRotationCenter = e.latlng;
this.#isRotatingDestination = true;
this.setKeepRelativePositions(true);
this.dragging.disable();
}
} else {
getApp().setState(OlympusState.IDLE);
this.setSelectionEnabled(true);
//@ts-ignore We force the boxselect to enter in selection mode
this.boxSelect._onMouseDown(e.originalEvent);
}
}
#onRightMouseShortClick(e: L.LeafletMouseEvent) {
console.log(`Right short click at ${e.latlng}`);
if (getApp().getState() === OlympusState.IDLE || getApp().getState() === OlympusState.SPAWN_CONTEXT) {
SpawnContextMenuRequestEvent.dispatch(e.latlng);
@ -1178,22 +1192,17 @@ export class Map extends L.Map {
}
}
#onRightLongClick(e: L.LeafletMouseEvent) {
#onRightMouseLongClick(e: L.LeafletMouseEvent) {
console.log(`Right long click at ${e.latlng}`);
if (getApp().getState() === OlympusState.UNIT_CONTROL) {
if (!this.getContextAction()) {
getApp().setState(OlympusState.UNIT_CONTROL, UnitControlSubState.MAP_CONTEXT_MENU);
MapContextMenuRequestEvent.dispatch(e.latlng);
}
}
this.#destinationRotationCenter = e.latlng;
this.#isRotatingDestination = true;
this.setKeepRelativePositions(true);
}
#onDoubleClick(e: L.LeafletMouseEvent) {
#onLeftMouseDoubleClick(e: L.LeafletMouseEvent) {
console.log(`Double click at ${e.latlng}`);
if (this.#debounceTimeout) window.clearTimeout(this.#debounceTimeout);
this.setPasteEnabled(false);
if (getApp().getState() === OlympusState.DRAW) {
@ -1209,7 +1218,6 @@ export class Map extends L.Map {
#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;
@ -1223,18 +1231,33 @@ export class Map extends L.Map {
if (this.#currentSpawnMarker) this.#currentSpawnMarker.setLatLng(e.latlng);
if (this.#currentEffectMarker) this.#currentEffectMarker.setLatLng(e.latlng);
} else if (getApp().getState() === OlympusState.MEASURE) {
if (this.#debounceTimeout === null) {
this.#measures[this.#measures.length - 1]?.onMouseMove(e.latlng);
let totalLength = 0;
this.#measures.forEach((measure) => {
measure.setTotalDistance(totalLength);
totalLength += measure.getDistance();
});
}
//if (this.#debounceTimeout === null) {
this.#measures[this.#measures.length - 1]?.onMouseMove(e.latlng);
let totalLength = 0;
this.#measures.forEach((measure) => {
measure.setTotalDistance(totalLength);
totalLength += measure.getDistance();
});
//}
}
} else {
this.#destionationWasRotated = true;
this.#destinationRotation -= e.originalEvent.movementX;
if (this.#destinationRotationCenter) {
/* Compute the average heading of the units */
const selectedUnits = getApp()
.getUnitsManager()
.getSelectedUnits()
.filter((unit) => !unit.getHuman());
let averageHeading = 0;
selectedUnits.forEach((unit) => {
averageHeading += unit.getHeading();
});
averageHeading /= selectedUnits.length;
/* Compute the rotation of the destination */
let angle = Math.atan2(e.latlng.lng - this.#destinationRotationCenter.lng, e.latlng.lat - this.#destinationRotationCenter.lat);
this.#destinationRotation = -(rad2deg(angle) - getMagvar(e.latlng.lat, e.latlng.lng) - rad2deg(averageHeading));
}
}
if (getApp().getState() === OlympusState.DRAW && (getApp().getSubState() === DrawSubState.NO_SUBSTATE || getApp().getSubState() === DrawSubState.EDIT)) {
@ -1343,7 +1366,7 @@ export class Map extends L.Map {
#moveDestinationPreviewMarkers() {
if (this.#keepRelativePositions) {
Object.entries(getApp().getUnitsManager().computeGroupDestination(this.#lastMouseCoordinates, this.#destinationRotation)).forEach(([ID, latlng]) => {
Object.entries(getApp().getUnitsManager().computeGroupDestination(this.#destinationRotationCenter, this.#destinationRotation)).forEach(([ID, latlng]) => {
this.#destinationPreviewMarkers[ID]?.setLatLng(latlng);
});
} else {

View File

@ -0,0 +1,198 @@
import { DomEvent, LeafletMouseEvent, Point } from "leaflet";
import { Map } from "./map";
enum MapMouseHandlerState {
IDLE = "IDLE",
LEFT_MOUSE_DOWN = "Left mouse down",
MOUSE_WHEEL_DOWN = "Mouse wheel down",
RIGHT_MOUSE_DOWN = "Right mouse down",
DEBOUNCING = "Debouncing",
}
export class MapMouseHandler {
#map: Map;
#state: string = MapMouseHandlerState.IDLE;
#leftMouseDownEpoch: number = 0;
#rightMouseDownEpoch: number = 0;
#mouseWheelDownEpoch: number = 0;
#leftMouseDownTimeout: number | null = null;
#rightMouseDownTimeout: number | null = null;
#mouseWheelDownTimeout: number | null = null;
#debounceTimeout: number | null = null;
leftMousePressed: (event: LeafletMouseEvent) => void = () => {};
leftMouseReleased: (event: LeafletMouseEvent) => void = () => {};
rightMousePressed: (event: LeafletMouseEvent) => void = () => {};
rightMouseReleased: (event: LeafletMouseEvent) => void = () => {};
mouseWheelPressed: (event: LeafletMouseEvent) => void = () => {};
mouseWheelReleased: (event: LeafletMouseEvent) => void = () => {};
leftMouseDoubleClick: (event: LeafletMouseEvent) => void = () => {};
leftMouseShortClick: (event: LeafletMouseEvent) => void = () => {};
rightMouseShortClick: (event: LeafletMouseEvent) => void = () => {};
mouseWheelShortClick: (event: LeafletMouseEvent) => void = () => {};
leftMouseLongClick: (event: LeafletMouseEvent) => void = () => {};
rightMouseLongClick: (event: LeafletMouseEvent) => void = () => {};
mouseWheelLongClick: (event: LeafletMouseEvent) => void = () => {};
mouseMove: (event: LeafletMouseEvent) => void = () => {};
mouseWheel: (event: LeafletMouseEvent) => void = () => {};
constructor(map) {
this.#map = map;
/* Events for touchscreen and mouse */
if ("ontouchstart" in window) {
DomEvent.on(this.#map.getContainer(), "touchstart", (e: any) => this.#onTouchDown(e), this);
DomEvent.on(this.#map.getContainer(), "touchend", (e: any) => this.#onTouchUp(e), this);
DomEvent.on(this.#map.getContainer(), "touchmove", (e: any) => this.#onTouchMove(e), this);
} else {
this.#map.on("mouseup", (e: any) => this.#onMouseUp(e));
this.#map.on("mousedown", (e: any) => this.#onMouseDown(e));
this.#map.on("mousemove", (e: any) => this.#onMouseMove(e));
}
this.#map.on("dblclick", (e: any) => this.#onDoubleClick(e));
/* Disable unwanted events */
this.#map.on("click", (e: any) => e.originalEvent.preventDefault());
this.#map.on("contextmenu", (e: any) => e.originalEvent.preventDefault());
/* Mouse wheel event */
DomEvent.on(this.#map.getContainer(), "wheel", (e: any) => this.#onMouseWheel(e), this);
}
setState(state: string) {
console.log("MouseHandler switching state from", this.#state, "to", state);
this.#state = state;
}
#onMouseDown = (e: LeafletMouseEvent) => {
if (e.originalEvent.button === 0) {
this.leftMousePressed(e);
this.setState(MapMouseHandlerState.LEFT_MOUSE_DOWN);
this.#leftMouseDownEpoch = Date.now();
this.#leftMouseDownTimeout = window.setTimeout(() => {
this.leftMouseLongClick(e);
this.#leftMouseDownTimeout = null;
}, 300);
} else if (e.originalEvent.button === 1) {
this.mouseWheelPressed(e);
this.setState(MapMouseHandlerState.MOUSE_WHEEL_DOWN);
this.#mouseWheelDownEpoch = Date.now();
this.#mouseWheelDownTimeout = window.setTimeout(() => {
this.mouseWheelLongClick(e);
this.#mouseWheelDownTimeout = null;
}, 300);
} else if (e.originalEvent.button === 2) {
this.rightMousePressed(e);
this.setState(MapMouseHandlerState.RIGHT_MOUSE_DOWN);
this.#rightMouseDownEpoch = Date.now();
this.#rightMouseDownTimeout = window.setTimeout(() => {
this.rightMouseLongClick(e);
this.#rightMouseDownTimeout = null;
}, 300);
}
};
#onMouseUp = (e: LeafletMouseEvent) => {
if (this.#leftMouseDownTimeout) {
clearTimeout(this.#leftMouseDownTimeout);
this.#leftMouseDownTimeout = null;
}
if (this.#rightMouseDownTimeout) {
clearTimeout(this.#rightMouseDownTimeout);
this.#rightMouseDownTimeout = null;
}
if (this.#rightMouseDownTimeout) {
clearTimeout(this.#rightMouseDownTimeout);
this.#rightMouseDownTimeout = null;
}
if (this.#state === MapMouseHandlerState.LEFT_MOUSE_DOWN) {
this.leftMouseReleased(e);
if (Date.now() - this.#leftMouseDownEpoch < 300) {
this.setState(MapMouseHandlerState.DEBOUNCING);
this.#debounceTimeout = window.setTimeout(() => {
this.leftMouseShortClick(e);
}, 300);
}
} else if (this.#state === MapMouseHandlerState.MOUSE_WHEEL_DOWN) {
this.mouseWheelReleased(e);
if (Date.now() - this.#mouseWheelDownEpoch < 300) {
this.mouseWheelShortClick(e);
}
} else if (this.#state === MapMouseHandlerState.RIGHT_MOUSE_DOWN) {
this.rightMouseReleased(e);
if (Date.now() - this.#rightMouseDownEpoch < 300) {
this.rightMouseShortClick(e);
}
}
this.setState(MapMouseHandlerState.IDLE);
};
#onDoubleClick = (e: LeafletMouseEvent) => {
this.leftMouseDoubleClick(e);
if (this.#debounceTimeout) {
clearTimeout(this.#debounceTimeout);
}
};
#onMouseWheel = (e: LeafletMouseEvent) => {
this.mouseWheel(e);
};
#onTouchDown = (e: TouchEvent) => {
let newEvent = {
latlng: this.#map.containerPointToLatLng(this.#map.mouseEventToContainerPoint(e.changedTouches[0] as unknown as MouseEvent)),
originalEvent: e,
} as unknown as LeafletMouseEvent;
this.leftMousePressed(newEvent);
this.setState(MapMouseHandlerState.LEFT_MOUSE_DOWN);
this.#leftMouseDownEpoch = Date.now();
this.#leftMouseDownTimeout = window.setTimeout(() => {
this.leftMouseLongClick(newEvent);
this.#leftMouseDownTimeout = null;
}, 300);
};
#onTouchUp = (e: TouchEvent) => {
let newEvent = {
latlng: this.#map.containerPointToLatLng(this.#map.mouseEventToContainerPoint(e.changedTouches[0] as unknown as MouseEvent)),
originalEvent: e,
} as unknown as LeafletMouseEvent;
if (this.#leftMouseDownTimeout) {
clearTimeout(this.#leftMouseDownTimeout);
this.#leftMouseDownTimeout = null;
}
if (this.#state === MapMouseHandlerState.LEFT_MOUSE_DOWN) {
this.leftMouseReleased(newEvent);
if (Date.now() - this.#leftMouseDownEpoch < 300) {
this.#debounceTimeout = window.setTimeout(() => {
this.leftMouseShortClick(newEvent);
}, 300);
}
}
this.setState(MapMouseHandlerState.IDLE);
};
#onMouseMove = (e: LeafletMouseEvent) => {
this.mouseMove(e);
};
#onTouchMove = (e: TouchEvent) => {
let newEvent = {
latlng: this.#map.containerPointToLatLng(this.#map.mouseEventToContainerPoint(e.changedTouches[0] as unknown as MouseEvent)),
originalEvent: e,
} as unknown as LeafletMouseEvent;
this.mouseMove(newEvent);
};
}

View File

@ -22,6 +22,10 @@ export function bearing(lat1: number, lon1: number, lat2: number, lon2: number,
return brng;
}
export function getMagvar(lat: number, lon: number) {
return MagVar.get(lat, lon);
}
export function distance(lat1: number, lon1: number, lat2: number, lon2: number) {
const R = 6371e3; // metres
const φ1 = deg2rad(lat1); // φ, λ in radians
@ -83,6 +87,13 @@ export function ConvertDDToDMS(D: number, lng: boolean) {
else return zeroPad(deg, 2) + "°" + zeroPad(min, 2) + "'" + zeroPad(sec, 2) + "." + zeroPad(dec, 2) + '"';
}
export function DDToDDM(decimalDegrees) {
const degrees = Math.trunc(decimalDegrees);
const minutes = Math.abs((decimalDegrees - degrees) * 60);
return `${Math.abs(degrees)}° ${minutes.toFixed(4)}'`;
}
export function deg2rad(deg: number) {
var pi = Math.PI;
return deg * (pi / 180);
@ -776,3 +787,7 @@ export function secondsToTimeString(seconds: number) {
return `${zeroPad(hours, 2)}:${zeroPad(minutes, 2)}:${zeroPad(secs, 2)}`;
}
export function isTrustedEnvironment() {
return window.location.protocol === "https:";
}

View File

@ -122,6 +122,7 @@ export class ServerManager {
/* If provided, set the credentials */
xmlHttp.setRequestHeader("Authorization", "Basic " + btoa(`${this.#username ?? ""}:${this.#password ?? ""}`));
xmlHttp.setRequestHeader("X-Command-Mode", this.#activeCommandMode);
xmlHttp.timeout = 2000;
/* If specified, set the response type */
if (responseType) xmlHttp.responseType = responseType as XMLHttpRequestResponseType;
@ -216,7 +217,7 @@ export class ServerManager {
}
getUnits(callback: CallableFunction, refresh: boolean = false, errorCallback: CallableFunction = () => {}) {
this.GET(callback, errorCallback, UNITS_URI, { time: refresh ? 0 : this.#lastUpdateTimes[UNITS_URI] }, "arraybuffer", false);
this.GET(callback, errorCallback, UNITS_URI, { time: refresh ? 0 : this.#lastUpdateTimes[UNITS_URI] }, "arraybuffer", refresh);
}
getWeapons(callback: CallableFunction, refresh: boolean = false, errorCallback: CallableFunction = () => {}) {

View File

@ -1,8 +1,8 @@
import React, { useState } from "react";
import { LatLng } from "leaflet";
import { ConvertDDToDMS, latLngToMGRS, latLngToUTM, zeroAppend } from "../../other/utils";
import { ConvertDDToDMS, DDToDDM, latLngToMGRS, latLngToUTM, zeroAppend } from "../../other/utils";
export function OlLocation(props: { location: LatLng; className?: string; referenceSystem?: string; onClick?: () => void }) {
export function OlLocation(props: { location: LatLng; className?: string; referenceSystem?: string; onClick?: () => void; onRefSystemChange?: (refSystem: string) => any }) {
const [referenceSystem, setReferenceSystem] = props.referenceSystem ? [props.referenceSystem, () => {}] : useState("LatLngDec");
const MGRS = latLngToMGRS(props.location.lat, props.location.lng, 6);
if (referenceSystem === "MGRS") {
@ -17,6 +17,7 @@ export function OlLocation(props: { location: LatLng; className?: string; refere
? props.onClick
: (ev) => {
setReferenceSystem("LatLngDec");
props.onRefSystemChange ? props.onRefSystemChange("LatLngDec") : null;
ev.stopPropagation();
}
}
@ -44,6 +45,7 @@ export function OlLocation(props: { location: LatLng; className?: string; refere
? props.onClick
: (ev) => {
setReferenceSystem("LatLngDMS");
props.onRefSystemChange ? props.onRefSystemChange("LatLngDMS") : null;
ev.stopPropagation();
}
}
@ -82,7 +84,8 @@ export function OlLocation(props: { location: LatLng; className?: string; refere
props.onClick
? props.onClick
: (ev) => {
setReferenceSystem("MGRS");
setReferenceSystem("LatLngDDM");
props.onRefSystemChange ? props.onRefSystemChange("LatLngDDM") : null;
ev.stopPropagation();
}
}
@ -109,6 +112,46 @@ export function OlLocation(props: { location: LatLng; className?: string; refere
</div>
</div>
);
} else if (referenceSystem === "LatLngDDM") {
return (
<div
className={`
${props.className ?? ""}
my-auto flex cursor-pointer justify-between gap-2 bg-olympus-400 p-2
text-white
`}
onClick={
props.onClick
? props.onClick
: (ev) => {
setReferenceSystem("MGRS");
props.onRefSystemChange ? props.onRefSystemChange("MGRS") : null;
ev.stopPropagation();
}
}
>
<div className="flex gap-2">
<span
className={`
w-5 rounded-sm bg-white text-center font-bold text-olympus-700
`}
>
{props.location.lat >= 0 ? "N" : "S"}
</span>
{DDToDDM(props.location.lat)}
</div>
<div className="flex w-[50%] gap-2">
<span
className={`
w-5 rounded-sm bg-white text-center font-bold text-olympus-700
`}
>
{props.location.lng >= 0 ? "E" : "W"}
</span>
{DDToDDM(props.location.lng)}
</div>
</div>
);
} else {
}
}

View File

@ -37,12 +37,14 @@ export function MapContextMenu(props: {}) {
});
ContextActionSetChangedEvent.on((contextActionSet) => setcontextActionSet(contextActionSet));
MapContextMenuRequestEvent.on((latlng) => {
setUnit(null);
setLatLng(latlng);
const containerPoint = getApp().getMap().latLngToContainerPoint(latlng);
setXPosition(getApp().getMap().getContainer().offsetLeft + containerPoint.x);
setYPosition(getApp().getMap().getContainer().offsetTop + containerPoint.y);
});
UnitContextMenuRequestEvent.on((unit) => {
setLatLng(null);
setUnit(unit);
const containerPoint = getApp().getMap().latLngToContainerPoint(unit.getPosition());
setXPosition(getApp().getMap().getContainer().offsetLeft + containerPoint.x);
@ -100,8 +102,8 @@ export function MapContextMenu(props: {}) {
<div
ref={contentRef}
className={`
absolute flex min-w-80 gap-2 rounded-md bg-olympus-600
`}
absolute flex min-w-80 gap-2 rounded-md bg-olympus-600
`}
>
<div
className={`

View File

@ -62,7 +62,7 @@ export function KeybindModal(props: { open: boolean }) {
return (
<Modal open={props.open} size={"sm"}>
<div className="flex flex-col gap-4 h-full w-full">
<div className="flex h-full w-full flex-col gap-4">
<div className={`flex flex-col gap-2`}>
<span
className={`
@ -97,7 +97,7 @@ export function KeybindModal(props: { open: boolean }) {
className={`flex flex-wrap gap-2 font-bold text-orange-600`}
>
{inUseShortcuts.map((shortcut) => (
<span>{shortcut.getOptions().label}</span>
<span key={shortcut.getId()}>{shortcut.getOptions().label}</span>
))}
</div>
</div>
@ -105,7 +105,7 @@ export function KeybindModal(props: { open: boolean }) {
</div>
)}
</div>
<div className="flex justify-end mt-auto ">
<div className="mt-auto flex justify-end">
{shortcut && (
<button
type="button"

View File

@ -91,14 +91,14 @@ export function ControlsPanel(props: {}) {
target: faFighterJet,
text: "Show unit actions",
});
controls.push({
actions: shortcuts["toggleRelativePositions"]?.toActions(),
text: "Activate group movement",
});
controls.push({
actions: [...shortcuts["toggleRelativePositions"]?.toActions(), "Wheel"],
text: "Rotate formation",
});
//controls.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 = [
{

View File

@ -1,11 +1,12 @@
import React, { useEffect, useState } from "react";
import React, { useEffect, useRef, useState } from "react";
import { OlLocation } from "../components/ollocation";
import { LatLng } from "leaflet";
import { FaBullseye, FaChevronDown, FaChevronUp, FaJetFighter, FaMountain } from "react-icons/fa6";
import { BullseyesDataChangedEvent, MouseMovedEvent, SelectedUnitsChangedEvent, SelectionClearedEvent } from "../../events";
import { computeBearingRangeString, mToFt } from "../../other/utils";
import { FaBullseye, FaChevronDown, FaChevronUp, FaJetFighter, FaMountain, FaCopy, FaXmark } from "react-icons/fa6";
import { BullseyesDataChangedEvent, CoordinatesFreezeEvent, MouseMovedEvent, SelectedUnitsChangedEvent, SelectionClearedEvent } from "../../events";
import { computeBearingRangeString, ConvertDDToDMS, DDToDDM, isTrustedEnvironment, latLngToMGRS, mToFt, zeroAppend } from "../../other/utils";
import { Bullseye } from "../../mission/bullseye";
import { Unit } from "../../unit/unit";
import { getApp } from "../../olympusapp";
export function CoordinatesPanel(props: {}) {
const [latlng, setLatlng] = useState(new LatLng(0, 0));
@ -13,7 +14,10 @@ export function CoordinatesPanel(props: {}) {
const [bullseyes, setBullseyes] = useState(null as null | { [name: string]: Bullseye });
const [selectedUnits, setSelectedUnits] = useState([] as Unit[]);
const [open, setOpen] = useState(true);
const [copyCoordsOpen, setCopyCoordsOpen] = useState(false);
const [refSystem, setRefSystem] = useState("LatLngDec");
const [copyableCoordinates, setCopyableCoordinates] = useState("To start, click any point on the map.");
useEffect(() => {
MouseMovedEvent.on((latlng, elevation) => {
setLatlng(latlng);
@ -23,18 +27,43 @@ export function CoordinatesPanel(props: {}) {
BullseyesDataChangedEvent.on((bullseyes) => setBullseyes(bullseyes));
SelectedUnitsChangedEvent.on((selectedUnits) => setSelectedUnits(selectedUnits));
SelectionClearedEvent.on(() => setSelectedUnits([]));
}, []);
CoordinatesFreezeEvent.on( () => {
setCopyableCoordinates(getCopyableCoordinates());
});
}, [refSystem, latlng, elevation]);
const getCopyableCoordinates = () => {
let returnString = '';
switch (refSystem) {
case "LatLngDec":
returnString = `${latlng.lat >= 0 ? "N" : "S"} ${zeroAppend(latlng.lat, 3, true, 6)}°, ${latlng.lng >= 0 ? "E" : "W"} ${zeroAppend(latlng.lng, 3, true, 6)}°,`
break;
case "LatLngDMS":
returnString = `${latlng.lat >= 0 ? "N" : "S"} ${ConvertDDToDMS(latlng.lat, false)}, ${latlng.lng >= 0 ? "E" : "W"} ${ConvertDDToDMS(latlng.lng, false)},`
break;
case "LatLngDDM":
returnString = `${latlng.lat >= 0 ? "N" : "S"} ${DDToDDM(latlng.lat)}, ${latlng.lng >= 0 ? "E" : "W"} ${DDToDDM(latlng.lng)}`;
break;
case "MGRS":
returnString = latLngToMGRS(latlng.lat, latlng.lng, 6)?.string || "Error";
break;
}
returnString += ` Elevation: ${Math.round(elevation)}`;
return returnString;
};
return (
<div
className={`
flex w-full flex-col items-center justify-between gap-2 rounded-lg
bg-gray-200 px-3 py-3 text-sm backdrop-blur-lg backdrop-grayscale
flex w-full flex-col justify-between gap-2 rounded-lg bg-gray-200 px-3
py-3 text-sm backdrop-blur-lg backdrop-grayscale
dark:bg-olympus-800/90 dark:text-gray-200
`}
onClick={() => setOpen(!open)}
>
<div className="absolute right-[12px] top-[15px]">
<div className="absolute right-[12px] top-[15px]" onClick={() => setOpen(!open)}>
{open ? (
<FaChevronDown className="cursor-pointer" />
) : (
@ -44,11 +73,7 @@ export function CoordinatesPanel(props: {}) {
)}
</div>
{open && bullseyes && (
<div
className={`
flex w-full flex-col items-start justify-start gap-2
`}
>
<div className={`flex w-full flex-col items-start justify-start gap-2`}>
<div
className={`
flex flex min-w-64 max-w-64 items-start justify-between gap-2
@ -98,26 +123,74 @@ export function CoordinatesPanel(props: {}) {
)}
<div
className={`
flex w-full items-center justify-between pointer-events-all
`}
className={`flex w-full items-center justify-between pointer-events-all`}
>
<OlLocation className="!min-w-64 !max-w-64 bg-transparent !p-0" location={latlng} />
<OlLocation onRefSystemChange={(evt) => setRefSystem(evt)} className={`
!min-w-64 !max-w-64 bg-transparent !p-0
`} location={latlng} />
</div>
{open && (
<div className="flex w-full items-center justify-start">
<span
className={`
mr-2 rounded-sm bg-white px-1 py-1 text-center font-bold
text-olympus-700
`}
{open && [
<div
className={`
flex w-full min-w-64 max-w-64 items-center justify-between
`}
>
<div className="flex">
<span
className={`
mr-2 rounded-sm bg-white px-1 py-1 text-center font-bold
text-olympus-700
`}
>
<FaMountain />
</span>
<div className="min-w-12">{mToFt(elevation).toFixed()}ft</div>
</div>
<div
className="ml-auto flex w-[50%]"
onClick={async (evt) => {
evt.stopPropagation();
setCopyCoordsOpen(true);
if (isTrustedEnvironment()) {
try {
await navigator.clipboard.writeText(copyableCoordinates);
getApp().addInfoMessage(`Coordinates copied to clipboard: ${copyableCoordinates}`);
} catch (err) {
console.error('Failed to copy text: ', err);
}
}
}}
>
<FaMountain />
</span>
<div className="min-w-12">{mToFt(elevation).toFixed()}ft</div>
</div>
)}
<span
className={`
mr-2 rounded-sm bg-white px-1 py-1 text-center font-bold
text-olympus-700
`}
>
<FaCopy />
</span>
<div className="min-w-12">Copy Coords</div>
</div>
</div>,
open && copyCoordsOpen && (
<div
className={`
relative mt-4 flex w-full min-w-64 items-center justify-between
`}
onClick={(evt) => evt.stopPropagation()}
>
<textarea readOnly={true} className="resize-none p-2 text-black" name="coordsTextArea" id="coordsTextArea" cols={25} rows={2} value={copyableCoordinates}></textarea>
<div className="absolute right-[0] top-[0px] background-transparent" onClick={() => setCopyCoordsOpen(false)}>
<FaXmark className="cursor-pointer" />
</div>
</div>
),
]}
</div>
);
}

View File

@ -21,9 +21,10 @@ export function InfoBar(props: {}) {
<div
key={idx}
className={`
absolute w-fit translate-x-[-50%] gap-2 text-nowrap rounded-full
absolute w-[250px] translate-x-[-50%] gap-2 rounded-full
bg-olympus-800/90 px-4 py-2 text-center text-sm text-white
shadow-md backdrop-blur-lg backdrop-grayscale
sm:w-fit sm:text-nowrap
`}
style={{ top: `${idx * 20}px` }}
>

View File

@ -98,6 +98,7 @@ export function JTACMenu(props: { open: boolean; onClose: () => void; children?:
onClick={() => {
if (referenceSystem === "MGRS") setReferenceSystem("LatLngDec");
else if (referenceSystem === "LatLngDec") setReferenceSystem("LatLngDMS");
else if (referenceSystem === "LatLngDMS") setReferenceSystem("LatLngDDM");
else setReferenceSystem("MGRS");
}}
referenceSystem={referenceSystem}
@ -133,6 +134,7 @@ export function JTACMenu(props: { open: boolean; onClose: () => void; children?:
onClick={() => {
if (referenceSystem === "MGRS") setReferenceSystem("LatLngDec");
else if (referenceSystem === "LatLngDec") setReferenceSystem("LatLngDMS");
else if (referenceSystem === "LatLngDMS") setReferenceSystem("LatLngDDM");
else setReferenceSystem("MGRS");
}}
referenceSystem={referenceSystem}
@ -168,6 +170,7 @@ export function JTACMenu(props: { open: boolean; onClose: () => void; children?:
onClick={() => {
if (referenceSystem === "MGRS") setReferenceSystem("LatLngDec");
else if (referenceSystem === "LatLngDec") setReferenceSystem("LatLngDMS");
else if (referenceSystem === "LatLngDMS") setReferenceSystem("LatLngDDM");
else setReferenceSystem("MGRS");
}}
referenceSystem={referenceSystem}

View File

@ -140,12 +140,16 @@ export function MapToolBar(props: {}) {
{!scrolledTop && (
<FaChevronUp
className={`
absolute top-0 h-6 w-full rounded-lg px-3.5 py-1 text-gray-200
absolute top-0 h-6 w-full rounded-lg bg-red-500 px-3.5 py-1
text-gray-200
dark:bg-olympus-900
`}
/>
)}
<div className={`flex flex-col gap-2 overflow-y-auto no-scrollbar p-2`} onScroll={(ev) => onScroll(ev.target)} ref={scrollRef}>
<div className={`
pointer-events-auto flex flex-col gap-2 overflow-y-auto no-scrollbar
p-2
`} onScroll={(ev) => onScroll(ev.target)} ref={scrollRef}>
<>
<div className="flex flex-col gap-1">
<OlStateButton

View File

@ -103,15 +103,13 @@ export function UI() {
<GameMasterMenu open={appState === OlympusState.GAME_MASTER} onClose={() => getApp().setState(OlympusState.IDLE)} />
<UnitExplosionMenu
open={appState === OlympusState.UNIT_CONTROL && appSubState === UnitControlSubState.UNIT_EXPLOSION_MENU}
onClose={() => getApp().setState(OlympusState.IDLE)}
onClose={() => {}}
/>
{/*}<JTACMenu open={appState === OlympusState.JTAC} onClose={() => getApp().setState(OlympusState.IDLE)} />
<AWACSMenu open={appState === OlympusState.AWACS} onClose={() => getApp().setState(OlympusState.IDLE)} />{*/}
<MiniMapPanel />
<ControlsPanel />
<SideBar />
<InfoBar />
@ -131,15 +129,15 @@ export function UI() {
backdrop-blur-sm
`}
>
<div className={`
flex w-[400px] flex-col items-center justify-center gap-4
`}>
<div
className={`
flex w-[400px] flex-col items-center justify-center gap-4
`}
>
<div className="bouncing-ball">
<img
src="images/olympus-500x500.png"
alt="Olympus Logo"
className={`ball-logo`}
/>
<img src="images/olympus-500x500.png" alt="Olympus Logo" className={`
ball-logo
`} />
</div>
{!connectedOnce && <div>Establishing connection</div>}
{connectedOnce && <div>Connection lost</div>}

View File

@ -61,6 +61,7 @@ import * as turf from "@turf/turf";
import { Carrier } from "../mission/carrier";
import {
ContactsUpdatedEvent,
CoordinatesFreezeEvent,
HiddenTypesChangedEvent,
MapOptionsChangedEvent,
UnitContextMenuRequestEvent,
@ -1628,6 +1629,9 @@ export abstract class Unit extends CustomMarker {
this.#isLeftMouseDown = true;
this.#leftMouseDownEpoch = Date.now();
this.#leftMouseDownTimeout = window.setTimeout(() => {
this.#onLeftLongClick(e);
}, SHORT_PRESS_MILLISECONDS);
} else if (e.originalEvent?.button === 2) {
if (
getApp().getState() === OlympusState.IDLE ||
@ -1647,10 +1651,13 @@ export abstract class Unit extends CustomMarker {
}
#onLeftShortClick(e: any) {
CoordinatesFreezeEvent.dispatch();
DomEvent.stop(e);
DomEvent.preventDefault(e);
e.originalEvent.stopImmediatePropagation();
window.clearTimeout(this.#leftMouseDownTimeout);
if (this.#debounceTimeout) window.clearTimeout(this.#debounceTimeout);
this.#debounceTimeout = window.setTimeout(() => {
console.log(`Left short click on ${this.getUnitName()}`);
@ -1665,20 +1672,8 @@ export abstract class Unit extends CustomMarker {
}, SHORT_PRESS_MILLISECONDS);
}
#onRightShortClick(e: any) {
console.log(`Right short click on ${this.getUnitName()}`);
window.clearTimeout(this.#rightMouseDownTimeout);
if (
getApp().getState() === OlympusState.UNIT_CONTROL &&
getApp().getMap().getDefaultContextAction() &&
getApp().getMap().getDefaultContextAction()?.getTarget() === ContextActionTarget.POINT
)
getApp().getMap().executeDefaultContextAction(null, this.getPosition(), e.originalEvent);
}
#onRightLongClick(e: any) {
console.log(`Right long click on ${this.getUnitName()}`);
#onLeftLongClick(e: any) {
console.log(`Left long click on ${this.getUnitName()}`);
if (getApp().getState() === OlympusState.IDLE) {
this.setSelected(!this.getSelected());
@ -1703,6 +1698,22 @@ export abstract class Unit extends CustomMarker {
}
}
#onRightShortClick(e: any) {
console.log(`Right short click on ${this.getUnitName()}`);
window.clearTimeout(this.#rightMouseDownTimeout);
if (
getApp().getState() === OlympusState.UNIT_CONTROL &&
getApp().getMap().getDefaultContextAction() &&
getApp().getMap().getDefaultContextAction()?.getTarget() === ContextActionTarget.POINT
)
getApp().getMap().executeDefaultContextAction(null, this.getPosition(), e.originalEvent);
}
#onRightLongClick(e: any) {
console.log(`Right long click on ${this.getUnitName()}`);
}
#onDoubleClick(e: any) {
DomEvent.stop(e);
DomEvent.preventDefault(e);

View File

@ -1,5 +1,5 @@
import * as turf from "@turf/turf";
import { LatLng, LatLngBounds } from "leaflet";
import { DomEvent, LatLng, LatLngBounds } from "leaflet";
import {
BLUE_COMMANDER,
DataIndexes,
@ -49,6 +49,7 @@ import { UnitDatabase } from "./databases/unitdatabase";
import { Group } from "./group";
import { AirUnit, GroundUnit, NavyUnit, Unit } from "./unit";
/** The UnitsManager handles the creation, update, and control of units. Data is strictly updated by the server ONLY. This means that any interaction from the user will always and only
* result in a command to the server, executed by means of a REST PUT request. Any subsequent change in data will be reflected only when the new data is sent back by the server. This strategy allows
* to avoid client/server and client/client inconsistencies.
@ -339,10 +340,16 @@ export class UnitsManager {
pathMarkersCoordinates.forEach((latlng: LatLng) => {
if (!this.#pathMarkers.some((pathMarker: PathMarker) => pathMarker.getLatLng().equals(latlng))) {
const pathMarker = new PathMarker(latlng);
pathMarker.on("mousedown", (event) => {
DomEvent.stopPropagation(event);
});
pathMarker.on("mouseup", (event) => {
DomEvent.stopPropagation(event);
});
pathMarker.on("dragstart", (event) => {
event.target.options["freeze"] = true;
event.target.options["originalPosition"] = event.target.getLatLng();
DomEvent.stopPropagation(event);
});
pathMarker.on("dragend", (event) => {
event.target.options["freeze"] = false;
@ -354,6 +361,7 @@ export class UnitsManager {
getApp().getServerManager().addDestination(unit.ID, path);
}
});
DomEvent.stopPropagation(event);
});
pathMarker.addTo(getApp().getMap());

View File

@ -60,6 +60,7 @@ async function installHooks(folder) {
*/
async function installMod(folder, name) {
/* Timestamp string */
logger.log(`Installing mod in ${folder}`)
await fsp.cp(path.join("..", "mod"), path.join(folder, "Mods", "Services", "Olympus"), { recursive: true });
@ -242,6 +243,11 @@ async function deleteMod(folder, name) {
else
logger.warn(`No mods.lua found in ${folder}, skipping backup...`)
if (await exists(path.join(folder, "Mods", "Services", "Olympus", "scripts", "unitPayloads.lua")))
await fsp.cp(path.join(folder, "Mods", "Services", "Olympus", "scripts", "unitPayloads.lua"), path.join(__dirname, "..", "..", "..", "DCS Olympus backups", name, "scripts", "unitPayloads.lua"));
else
logger.warn(`No unitPayloads.lua found in ${folder}, skipping backup...`)
/* Remove the mod folder */
await fsp.rmdir(path.join(folder, "Mods", "Services", "Olympus"), { recursive: true, force: true })
logger.log(`Mod succesfully removed from ${folder}`)

View File

@ -659,7 +659,7 @@ end
-- lat: (number)
-- lng: (number)
-- alt: (number, optional) only for air units
-- loadout: (string, optional) only for air units, must be one of the loadouts defined in unitPayloads.lua
-- loadout: (string, optional) only for air units, must be one of the loadouts defined in unitPayloads.lua or mods.lua
-- payload: (table, optional) overrides loadout, specifies directly the loadout of the unit
-- liveryID: (string, optional)
function Olympus.spawnUnits(spawnTable)
@ -731,6 +731,8 @@ function Olympus.generateAirUnitsTable(units)
if payload == nil then
if loadout ~= nil and loadout ~= "" and Olympus.unitPayloads[unit.unitType] and Olympus.unitPayloads[unit.unitType][loadout] then
payload = { ["pylons"] = Olympus.unitPayloads[unit.unitType][loadout], ["fuel"] = 999999, ["flare"] = 60, ["ammo_type"] = 1, ["chaff"] = 60, ["gun"] = 100 }
elseif loadout ~= nil and loadout ~= "" and Olympus.modsUnitPayloads ~= nil and Olympus.modsUnitPayloads[unit.unitType] and Olympus.modsUnitPayloads[unit.unitType][loadout] then
payload = { ["pylons"] = Olympus.modsUnitPayloads[unit.unitType][loadout], ["fuel"] = 999999, ["flare"] = 60, ["ammo_type"] = 1, ["chaff"] = 60, ["gun"] = 100 }
else
payload = { ["pylons"] = {}, ["fuel"] = 999999, ["flare"] = 60, ["ammo_type"] = 1, ["chaff"] = 60, ["gun"] = 100 }
end

View File

@ -9,3 +9,19 @@ Olympus.modsList = {
["A-4E-C"] = "Aircraft",
["Bronco-OV-10A"] = "Aircraft"
}
-- Enter here any unitPayloads you want to use for your mods. Remember to add the payload to the database in mods.json!
-- DO NOT ADD PAYLOADS TO "ORIGINAL" DCS UNITS HERE! To add payloads to original DCS units, use the "unitPayload.lua" table instead and add them under the correct unit section.
-- Provided example is for the A-4E-C mod, with a payload of 76 FFAR Mk1 HE rockets and a 300 gallon fuel tank.
Olympus.modsUnitPayloads = {
["A-4E-C"] = {
["FFAR Mk1 HE *76, Fuel 300G"] = {
[1] = {["CLSID"] = "{LAU3_FFAR_MK1HE}"},
[2] = {["CLSID"] = "{LAU3_FFAR_MK1HE}"},
[3] = {["CLSID"] = "{LAU3_FFAR_MK1HE}"},
[4] = {["CLSID"] = "{LAU3_FFAR_MK1HE}"},
[5] = {["CLSID"] = "{DFT-300gal}"}
}
}
}