mirror of
https://github.com/Pax1601/DCSOlympus.git
synced 2025-10-29 16:56:34 +00:00
Implemented context menu and multiple control tweaks
This commit is contained in:
parent
7cf77a63be
commit
6fdfb194a6
1
.gitignore
vendored
1
.gitignore
vendored
@ -43,3 +43,4 @@ manager/manager.log
|
||||
/frontend/server/public/assets
|
||||
/frontend/server/public/vite
|
||||
/frontend/server/build
|
||||
/frontend/react/.vite
|
||||
|
||||
5
frontend/react/src/dom.d.ts
vendored
5
frontend/react/src/dom.d.ts
vendored
@ -11,7 +11,6 @@ interface CustomEventMap {
|
||||
unitDeath: CustomEvent<Unit>;
|
||||
unitUpdated: CustomEvent<Unit>;
|
||||
mapStateChanged: CustomEvent<string>;
|
||||
mapContextMenu: CustomEvent<any>;
|
||||
mapOptionChanged: CustomEvent<any>;
|
||||
mapSourceChanged: CustomEvent<string>;
|
||||
mapOptionsChanged: CustomEvent<any>; // TODO not very clear, why the two options?
|
||||
@ -23,6 +22,10 @@ interface CustomEventMap {
|
||||
serverStatusUpdated: CustomEvent<ServerStatus>;
|
||||
mapForceBoxSelect: CustomEvent<any>;
|
||||
coalitionAreaSelected: CustomEvent<CoalitionPolygon>;
|
||||
showMapContextMenu: CustomEvent<any>;
|
||||
hideMapContextMenu: CustomEvent<any>;
|
||||
showUnitContextMenu: CustomEvent<any>;
|
||||
hideUnitContextMenu: CustomEvent<any>;
|
||||
}
|
||||
|
||||
declare global {
|
||||
|
||||
@ -1,11 +1,12 @@
|
||||
/***************** UI *******************/
|
||||
import React from "react";
|
||||
import ReactDOM from "react-dom/client";
|
||||
import { setupApp } from "./olympusapp.js";
|
||||
import { UI } from "./ui/ui.js";
|
||||
|
||||
import "./index.css";
|
||||
|
||||
window.addEventListener("contextmenu", e => e. preventDefault());
|
||||
|
||||
ReactDOM.createRoot(document.getElementById("root") as HTMLElement).render(
|
||||
<React.StrictMode>
|
||||
<UI />
|
||||
|
||||
@ -129,4 +129,10 @@
|
||||
|
||||
.ol-coalitionarea-label.red {
|
||||
color: #461818;
|
||||
}
|
||||
|
||||
.ol-target-icon {
|
||||
background-image: url("/vite/images/markers/target.svg");
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
}
|
||||
@ -23,7 +23,7 @@ import {
|
||||
import { CoalitionPolygon } from "./coalitionarea/coalitionpolygon";
|
||||
import { MapHiddenTypes, MapOptions } from "../types/types";
|
||||
import { SpawnRequestTable } from "../interfaces";
|
||||
import { ContextAction } from "../unit/contextaction";
|
||||
import { ContextAction, ContextActionCallback } from "../unit/contextaction";
|
||||
|
||||
/* Stylesheets */
|
||||
import "./markers/stylesheets/airbase.css";
|
||||
@ -33,7 +33,7 @@ import "./map.css";
|
||||
import { CoalitionCircle } from "./coalitionarea/coalitioncircle";
|
||||
|
||||
import { initDraggablePath } from "./coalitionarea/draggablepath";
|
||||
import { faComputerMouse, faDrawPolygon, faHandPointer, faJetFighter, faMap } from "@fortawesome/free-solid-svg-icons";
|
||||
import { faDrawPolygon, faHandPointer, faJetFighter, faMap } from "@fortawesome/free-solid-svg-icons";
|
||||
|
||||
/* Register the handler for the box selection */
|
||||
L.Map.addInitHook("addHandler", "boxSelect", BoxSelect);
|
||||
@ -103,6 +103,7 @@ export class Map extends L.Map {
|
||||
|
||||
/* Unit context actions */
|
||||
#contextAction: null | ContextAction = null;
|
||||
#defaultContextAction: null | ContextAction = null;
|
||||
|
||||
/* Unit spawning */
|
||||
#spawnRequestTable: SpawnRequestTable | null = null;
|
||||
@ -156,6 +157,9 @@ export class Map extends L.Map {
|
||||
this.on("dblclick", (e: any) => this.#onDoubleClick(e));
|
||||
this.on("mouseup", (e: any) => this.#onMouseUp(e));
|
||||
this.on("mousedown", (e: any) => this.#onMouseDown(e));
|
||||
this.on("contextmenu", (e: any) => {
|
||||
e.originalEvent.preventDefault();
|
||||
});
|
||||
|
||||
this.on("mousemove", (e: any) => this.#onMouseMove(e));
|
||||
|
||||
@ -337,6 +341,7 @@ export class Map extends L.Map {
|
||||
options?: {
|
||||
spawnRequestTable?: SpawnRequestTable;
|
||||
contextAction?: ContextAction | null;
|
||||
defaultContextAction?: ContextAction | null;
|
||||
}
|
||||
) {
|
||||
console.log(`Switching from state ${this.#state} to ${state}`);
|
||||
@ -358,8 +363,11 @@ export class Map extends L.Map {
|
||||
} else if (this.#state === CONTEXT_ACTION) {
|
||||
this.deselectAllCoalitionAreas();
|
||||
this.#contextAction = options?.contextAction ?? null;
|
||||
this.#defaultContextAction = options?.defaultContextAction ?? null;
|
||||
console.log(`Context action:`);
|
||||
console.log(this.#contextAction);
|
||||
console.log(`Default context action callback:`);
|
||||
console.log(this.#defaultContextAction);
|
||||
} else if (this.#state === COALITIONAREA_DRAW_POLYGON) {
|
||||
getApp().getUnitsManager().deselectAllUnits();
|
||||
this.#coalitionAreas.push(new CoalitionPolygon([]));
|
||||
@ -382,7 +390,7 @@ export class Map extends L.Map {
|
||||
if (this.#state === IDLE) {
|
||||
return [
|
||||
{
|
||||
actions: [touch ? faHandPointer : faComputerMouse],
|
||||
actions: [touch ? faHandPointer : "LMB"],
|
||||
target: faJetFighter,
|
||||
text: "Select unit",
|
||||
},
|
||||
@ -393,12 +401,12 @@ export class Map extends L.Map {
|
||||
text: "Box selection",
|
||||
}
|
||||
: {
|
||||
actions: ["Shift", faComputerMouse, "Drag"],
|
||||
actions: ["Shift", "LMB", "Drag"],
|
||||
target: faMap,
|
||||
text: "Box selection",
|
||||
},
|
||||
{
|
||||
actions: [touch ? faHandPointer : faComputerMouse, "Drag"],
|
||||
actions: [touch ? faHandPointer : "LMB", "Drag"],
|
||||
target: faMap,
|
||||
text: "Move map location",
|
||||
},
|
||||
@ -406,17 +414,17 @@ export class Map extends L.Map {
|
||||
} else if (this.#state === SPAWN_UNIT) {
|
||||
return [
|
||||
{
|
||||
actions: [touch ? faHandPointer : faComputerMouse],
|
||||
actions: [touch ? faHandPointer : "LMB"],
|
||||
target: faMap,
|
||||
text: "Spawn unit",
|
||||
},
|
||||
{
|
||||
actions: [touch ? faHandPointer : faComputerMouse, touch ? faHandPointer : faComputerMouse],
|
||||
actions: [touch ? faHandPointer : "LMB", 2],
|
||||
target: faMap,
|
||||
text: "Exit spawn mode",
|
||||
},
|
||||
{
|
||||
actions: [touch ? faHandPointer : faComputerMouse, "Drag"],
|
||||
actions: [touch ? faHandPointer : "LMB", "Drag"],
|
||||
target: faMap,
|
||||
text: "Move map location",
|
||||
},
|
||||
@ -424,42 +432,53 @@ export class Map extends L.Map {
|
||||
} else if (this.#state === CONTEXT_ACTION) {
|
||||
let controls = [
|
||||
{
|
||||
actions: [touch ? faHandPointer : faComputerMouse, touch ? faHandPointer : faComputerMouse],
|
||||
actions: [touch ? faHandPointer : "LMB"],
|
||||
target: faMap,
|
||||
text: "Deselect units",
|
||||
},
|
||||
{
|
||||
actions: [touch ? faHandPointer : faComputerMouse, "Drag"],
|
||||
actions: [touch ? faHandPointer : "LMB", "Drag"],
|
||||
target: faMap,
|
||||
text: "Move map location",
|
||||
},
|
||||
];
|
||||
|
||||
if (this.#contextAction) {
|
||||
/* TODO: I don't like this approach, it relies on the arguments names of the callback. We should find a better method */
|
||||
const args = getFunctionArguments(this.#contextAction.getCallback());
|
||||
controls.push({
|
||||
actions: [touch ? faHandPointer : faComputerMouse],
|
||||
target: args.includes("targetUnit") ? faJetFighter : faMap,
|
||||
actions: [touch ? faHandPointer : "LMB"],
|
||||
target: this.#contextAction.getTarget() === "unit" ? faJetFighter : faMap,
|
||||
text: this.#contextAction?.getLabel() ?? "",
|
||||
});
|
||||
}
|
||||
|
||||
if (!touch && this.#defaultContextAction) {
|
||||
controls.push({
|
||||
actions: ["RMB"],
|
||||
target: faMap,
|
||||
text: this.#defaultContextAction?.getLabel() ?? "",
|
||||
});
|
||||
controls.push({
|
||||
actions: ["RMB", "hold"],
|
||||
target: faMap,
|
||||
text: "Open context menu",
|
||||
});
|
||||
}
|
||||
|
||||
return controls;
|
||||
} else if (this.#state === COALITIONAREA_EDIT) {
|
||||
return [
|
||||
{
|
||||
actions: [touch ? faHandPointer : faComputerMouse],
|
||||
actions: [touch ? faHandPointer : "LMB"],
|
||||
target: faDrawPolygon,
|
||||
text: "Select shape",
|
||||
},
|
||||
{
|
||||
actions: [touch ? faHandPointer : faComputerMouse, touch ? faHandPointer : faComputerMouse],
|
||||
actions: [touch ? faHandPointer : "LMB", 2],
|
||||
target: faMap,
|
||||
text: "Exit drawing mode",
|
||||
},
|
||||
{
|
||||
actions: [touch ? faHandPointer : faComputerMouse, "Drag"],
|
||||
actions: [touch ? faHandPointer : "LMB", "Drag"],
|
||||
target: faMap,
|
||||
text: "Move map location",
|
||||
},
|
||||
@ -467,17 +486,17 @@ export class Map extends L.Map {
|
||||
} else if (this.#state === COALITIONAREA_DRAW_POLYGON) {
|
||||
return [
|
||||
{
|
||||
actions: [touch ? faHandPointer : faComputerMouse],
|
||||
actions: [touch ? faHandPointer : "LMB"],
|
||||
target: faMap,
|
||||
text: "Add vertex to polygon",
|
||||
},
|
||||
{
|
||||
actions: [touch ? faHandPointer : faComputerMouse, touch ? faHandPointer : faComputerMouse],
|
||||
actions: [touch ? faHandPointer : "LMB", 2],
|
||||
target: faMap,
|
||||
text: "Finalize polygon",
|
||||
},
|
||||
{
|
||||
actions: [touch ? faHandPointer : faComputerMouse, "Drag"],
|
||||
actions: [touch ? faHandPointer : "LMB", "Drag"],
|
||||
target: faMap,
|
||||
text: "Move map location",
|
||||
},
|
||||
@ -485,12 +504,12 @@ export class Map extends L.Map {
|
||||
} else if (this.#state === COALITIONAREA_DRAW_CIRCLE) {
|
||||
return [
|
||||
{
|
||||
actions: [touch ? faHandPointer : faComputerMouse],
|
||||
actions: [touch ? faHandPointer : "LMB"],
|
||||
target: faMap,
|
||||
text: "Add circle",
|
||||
},
|
||||
{
|
||||
actions: [touch ? faHandPointer : faComputerMouse, "Drag"],
|
||||
actions: [touch ? faHandPointer : "LMB", "Drag"],
|
||||
target: faMap,
|
||||
text: "Move map location",
|
||||
},
|
||||
@ -732,6 +751,18 @@ export class Map extends L.Map {
|
||||
this.#contextAction?.executeCallback(targetUnit, targetPosition);
|
||||
}
|
||||
|
||||
getContextAction() {
|
||||
return this.#contextAction;
|
||||
}
|
||||
|
||||
executeDefaultContextAction(targetUnit: Unit | null, targetPosition: L.LatLng | null) {
|
||||
if (this.#defaultContextAction) this.#defaultContextAction.executeCallback(targetUnit, targetPosition);
|
||||
}
|
||||
|
||||
getDefaultContextAction() {
|
||||
return this.#defaultContextAction;
|
||||
}
|
||||
|
||||
preventClicks() {
|
||||
console.log("Preventing clicks on map");
|
||||
window.clearTimeout(this.#shortPressTimer);
|
||||
@ -779,7 +810,7 @@ export class Map extends L.Map {
|
||||
this.#longPressTimer = window.setTimeout(() => {
|
||||
/* If the mouse is still being pressed, execute the long press action */
|
||||
if (this.#isMouseDown && !this.#isDragging && !this.#isZooming) this.#onLongPress(e);
|
||||
}, 500);
|
||||
}, 350);
|
||||
}
|
||||
|
||||
#onDoubleClick(e: any) {
|
||||
@ -796,17 +827,20 @@ export class Map extends L.Map {
|
||||
}
|
||||
|
||||
#onShortPress(e: any) {
|
||||
let touchLocation: L.LatLng;
|
||||
if (e.type === "touchstart") touchLocation = this.containerPointToLatLng(this.mouseEventToContainerPoint(e.touches[0]));
|
||||
else touchLocation = new L.LatLng(e.latlng.lat, e.latlng.lng);
|
||||
let pressLocation: L.LatLng;
|
||||
if (e.type === "touchstart") pressLocation = this.containerPointToLatLng(this.mouseEventToContainerPoint(e.touches[0]));
|
||||
else pressLocation = new L.LatLng(e.latlng.lat, e.latlng.lng);
|
||||
|
||||
console.log(`Short press at ${touchLocation}`);
|
||||
console.log(`Short press at ${pressLocation}`);
|
||||
|
||||
document.dispatchEvent(new CustomEvent("hideMapContextMenu"));
|
||||
document.dispatchEvent(new CustomEvent("hideUnitContextMenu"));
|
||||
|
||||
/* Execute the short click action */
|
||||
if (this.#state === IDLE) {
|
||||
} else if (this.#state === SPAWN_UNIT) {
|
||||
if (this.#spawnRequestTable !== null) {
|
||||
this.#spawnRequestTable.unit.location = touchLocation;
|
||||
this.#spawnRequestTable.unit.location = pressLocation;
|
||||
getApp()
|
||||
.getUnitsManager()
|
||||
.spawnUnits(
|
||||
@ -817,25 +851,25 @@ export class Map extends L.Map {
|
||||
undefined,
|
||||
undefined,
|
||||
(hash) => {
|
||||
this.addTemporaryMarker(touchLocation, this.#spawnRequestTable?.unit.unitType ?? "unknown", this.#spawnRequestTable?.coalition ?? "blue", hash);
|
||||
this.addTemporaryMarker(pressLocation, this.#spawnRequestTable?.unit.unitType ?? "unknown", this.#spawnRequestTable?.coalition ?? "blue", hash);
|
||||
}
|
||||
);
|
||||
}
|
||||
} else if (this.#state === COALITIONAREA_DRAW_POLYGON) {
|
||||
const selectedArea = this.getSelectedCoalitionArea();
|
||||
if (selectedArea && selectedArea instanceof CoalitionPolygon) {
|
||||
selectedArea.addTemporaryLatLng(touchLocation);
|
||||
selectedArea.addTemporaryLatLng(pressLocation);
|
||||
}
|
||||
} else if (this.#state === COALITIONAREA_DRAW_CIRCLE) {
|
||||
const selectedArea = this.getSelectedCoalitionArea();
|
||||
if (selectedArea && selectedArea instanceof CoalitionCircle) {
|
||||
if (selectedArea.getLatLng().lat == 0 && selectedArea.getLatLng().lng == 0) selectedArea.setLatLng(touchLocation);
|
||||
if (selectedArea.getLatLng().lat == 0 && selectedArea.getLatLng().lng == 0) selectedArea.setLatLng(pressLocation);
|
||||
this.setState(COALITIONAREA_EDIT);
|
||||
}
|
||||
} else if (this.#state == COALITIONAREA_EDIT) {
|
||||
this.deselectAllCoalitionAreas();
|
||||
for (let idx = 0; idx < this.#coalitionAreas.length; idx++) {
|
||||
if (areaContains(touchLocation, this.#coalitionAreas[idx])) {
|
||||
if (areaContains(pressLocation, this.#coalitionAreas[idx])) {
|
||||
this.#coalitionAreas[idx].setSelected(true);
|
||||
document.dispatchEvent(
|
||||
new CustomEvent("coalitionAreaSelected", {
|
||||
@ -846,23 +880,35 @@ export class Map extends L.Map {
|
||||
}
|
||||
}
|
||||
} else if (this.#state === CONTEXT_ACTION) {
|
||||
this.executeContextAction(null, touchLocation);
|
||||
if (e.originalEvent.buttons === 1) {
|
||||
if (this.#contextAction !== null) this.executeContextAction(null, pressLocation);
|
||||
else this.setState(IDLE);
|
||||
} else if (e.originalEvent.buttons === 2) {
|
||||
if (this.#defaultContextAction !== null) this.executeDefaultContextAction(null, pressLocation);
|
||||
}
|
||||
} else {
|
||||
}
|
||||
}
|
||||
|
||||
#onLongPress(e: any) {
|
||||
let touchLocation: L.LatLng;
|
||||
if (e.type === "touchstart") touchLocation = this.containerPointToLatLng(this.mouseEventToContainerPoint(e.touches[0]));
|
||||
else touchLocation = new L.LatLng(e.latlng.lat, e.latlng.lng);
|
||||
let pressLocation: L.LatLng;
|
||||
if (e.type === "touchstart") pressLocation = this.containerPointToLatLng(this.mouseEventToContainerPoint(e.touches[0]));
|
||||
else pressLocation = new L.LatLng(e.latlng.lat, e.latlng.lng);
|
||||
|
||||
console.log(`Long press at ${touchLocation}`);
|
||||
console.log(`Long press at ${pressLocation}`);
|
||||
|
||||
if (!this.#isDragging && !this.#isZooming) {
|
||||
this.deselectAllCoalitionAreas();
|
||||
if (this.#state === IDLE) {
|
||||
if (e.type === "touchstart") document.dispatchEvent(new CustomEvent("mapForceBoxSelect", { detail: e }));
|
||||
else document.dispatchEvent(new CustomEvent("mapForceBoxSelect", { detail: e.originalEvent }));
|
||||
} else if (this.#state === CONTEXT_ACTION) {
|
||||
if (e.originalEvent.button === 2) {
|
||||
document.dispatchEvent(new CustomEvent("showMapContextMenu", { detail: e }));
|
||||
} else {
|
||||
if (e.type === "touchstart") document.dispatchEvent(new CustomEvent("mapForceBoxSelect", { detail: e }));
|
||||
else document.dispatchEvent(new CustomEvent("mapForceBoxSelect", { detail: e.originalEvent }));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -141,7 +141,10 @@ export class ServerManager {
|
||||
}
|
||||
|
||||
setAddress(address: string) {
|
||||
this.#REST_ADDRESS = `${address.replace("vite/", "")}olympus`;
|
||||
this.#REST_ADDRESS = `${address.replace("vite/", "").replace("vite", "")}olympus`;
|
||||
// TODO: TEMPORARY FOR DEBUGGING
|
||||
// this.#REST_ADDRESS = `https://refugees.dcsolympus.com/olympus`;
|
||||
|
||||
console.log(`Setting REST address to ${this.#REST_ADDRESS}`);
|
||||
}
|
||||
|
||||
|
||||
@ -1,5 +1,5 @@
|
||||
import { createContext } from "react";
|
||||
import { MAP_HIDDEN_TYPES_DEFAULTS, MAP_OPTIONS_DEFAULTS } from "./constants/constants";
|
||||
import { IDLE, MAP_HIDDEN_TYPES_DEFAULTS, MAP_OPTIONS_DEFAULTS } from "./constants/constants";
|
||||
|
||||
export const StateContext = createContext({
|
||||
mainMenuVisible: false,
|
||||
@ -12,7 +12,7 @@ export const StateContext = createContext({
|
||||
mapOptions: MAP_OPTIONS_DEFAULTS,
|
||||
mapSources: [] as string[],
|
||||
activeMapSource: "",
|
||||
mapBoxSelection: false,
|
||||
mapState: IDLE
|
||||
});
|
||||
|
||||
export const StateProvider = StateContext.Provider;
|
||||
|
||||
@ -103,7 +103,12 @@ export function OlDropdown(props: {
|
||||
`}
|
||||
type="button"
|
||||
>
|
||||
{props.leftIcon && <FontAwesomeIcon icon={props.leftIcon} className={`mr-3`} />}
|
||||
{props.leftIcon && (
|
||||
<FontAwesomeIcon
|
||||
icon={props.leftIcon}
|
||||
className={`mr-3`}
|
||||
/>
|
||||
)}
|
||||
<span className="overflow-hidden text-ellipsis text-nowrap">{props.label ?? ""}</span>
|
||||
<svg
|
||||
className={`
|
||||
@ -144,7 +149,11 @@ export function OlDropdown(props: {
|
||||
}
|
||||
|
||||
/* Conveniency Component for dropdown elements */
|
||||
export function OlDropdownItem(props) {
|
||||
export function OlDropdownItem(props: {
|
||||
onClick?: () => void;
|
||||
className?: string;
|
||||
children?: string | JSX.Element | JSX.Element[];
|
||||
}) {
|
||||
return (
|
||||
<button
|
||||
onClick={props.onClick ?? (() => {})}
|
||||
|
||||
139
frontend/react/src/ui/contextmenus/mapcontextmenu.tsx
Normal file
139
frontend/react/src/ui/contextmenus/mapcontextmenu.tsx
Normal file
@ -0,0 +1,139 @@
|
||||
import React, { useEffect, useRef, useState } from "react";
|
||||
import { Unit } from "../../unit/unit";
|
||||
import { ContextActionSet } from "../../unit/contextactionset";
|
||||
import { OlStateButton } from "../components/olstatebutton";
|
||||
import { getApp } from "../../olympusapp";
|
||||
import { ContextAction } from "../../unit/contextaction";
|
||||
import { CONTEXT_ACTION } from "../../constants/constants";
|
||||
import { OlDropdownItem } from "../components/oldropdown";
|
||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||
import { LatLng } from "leaflet";
|
||||
|
||||
export function MapContextMenu(props: {}) {
|
||||
const [open, setOpen] = useState(false);
|
||||
const [contextActionsSet, setContextActionsSet] = useState(new ContextActionSet());
|
||||
const [activeContextAction, setActiveContextAction] = useState(null as null | ContextAction);
|
||||
const [xPosition, setXPosition] = useState(0);
|
||||
const [yPosition, setYPosition] = useState(0);
|
||||
const [latLng, setLatLng] = useState(null as null | LatLng);
|
||||
const [unit, setUnit] = useState(null as null | Unit)
|
||||
|
||||
var contentRef = useRef(null);
|
||||
|
||||
useEffect(() => {
|
||||
if (contentRef.current) {
|
||||
const content = contentRef.current as HTMLDivElement;
|
||||
|
||||
content.style.left = `${xPosition}px`;
|
||||
content.style.top = `${yPosition}px`;
|
||||
|
||||
let newXPosition = xPosition;
|
||||
let newYposition = yPosition;
|
||||
|
||||
let [cxr, cyb] = [content.getBoundingClientRect().x + content.clientWidth, content.getBoundingClientRect().y + content.clientHeight];
|
||||
|
||||
/* Try and move the content so it is inside the screen */
|
||||
if (cxr > window.innerWidth) newXPosition -= cxr - window.innerWidth;
|
||||
if (cyb > window.innerHeight) newYposition -= cyb - window.innerHeight;
|
||||
|
||||
content.style.left = `${newXPosition}px`;
|
||||
content.style.top = `${newYposition}px`;
|
||||
}
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
document.addEventListener("showMapContextMenu", (ev: CustomEventInit) => {
|
||||
setOpen(true);
|
||||
|
||||
updateData();
|
||||
|
||||
setXPosition(ev.detail.originalEvent.clientX);
|
||||
setYPosition(ev.detail.originalEvent.clientY);
|
||||
setLatLng(ev.detail.latlng);
|
||||
setUnit(null);
|
||||
});
|
||||
|
||||
document.addEventListener("showUnitContextMenu", (ev: CustomEventInit) => {
|
||||
setOpen(true);
|
||||
|
||||
updateData();
|
||||
|
||||
setXPosition(ev.detail.originalEvent.clientX);
|
||||
setYPosition(ev.detail.originalEvent.clientY);
|
||||
setLatLng(null);
|
||||
setUnit(ev.detail.sourceTarget);
|
||||
});
|
||||
|
||||
document.addEventListener("hideMapContextMenu", (ev: CustomEventInit) => {
|
||||
setOpen(false);
|
||||
});
|
||||
|
||||
document.addEventListener("hideUnitContextMenu", (ev: CustomEventInit) => {
|
||||
setOpen(false);
|
||||
});
|
||||
|
||||
document.addEventListener("clearSelection", () => {
|
||||
setOpen(false);
|
||||
});
|
||||
}, []);
|
||||
|
||||
/* Update the current values of the shown data */
|
||||
function updateData() {
|
||||
var newContextActionSet = new ContextActionSet();
|
||||
|
||||
getApp()
|
||||
.getUnitsManager()
|
||||
.getSelectedUnits()
|
||||
.forEach((unit: Unit) => {
|
||||
unit.appendContextActions(newContextActionSet);
|
||||
});
|
||||
|
||||
setContextActionsSet(newContextActionSet);
|
||||
return newContextActionSet;
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
{open && (
|
||||
<>
|
||||
<div
|
||||
ref={contentRef}
|
||||
className={`
|
||||
absolute flex min-w-80 gap-2 rounded-md bg-olympus-600
|
||||
`}
|
||||
>
|
||||
<div
|
||||
className={`
|
||||
flex w-full flex-col gap-2 overflow-x-auto no-scrollbar p-2
|
||||
`}
|
||||
>
|
||||
{Object.values(contextActionsSet.getContextActions(latLng? "position": "unit")).map((contextAction) => {
|
||||
return (
|
||||
<OlDropdownItem
|
||||
className="flex w-full content-center gap-2 text-white"
|
||||
onClick={() => {
|
||||
if (contextAction.getOptions().executeImmediately) {
|
||||
contextAction.executeCallback(null, null);
|
||||
} else {
|
||||
if (latLng !== null) {
|
||||
contextAction.executeCallback(null, latLng);
|
||||
setOpen(false);
|
||||
} else if (unit !== null) {
|
||||
contextAction.executeCallback(unit, null);
|
||||
setOpen(false);
|
||||
}
|
||||
}
|
||||
}}
|
||||
>
|
||||
<FontAwesomeIcon className="my-auto" icon={contextAction.getIcon()} />
|
||||
<div>{contextAction.getLabel()}</div>
|
||||
</OlDropdownItem>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
}
|
||||
@ -5,7 +5,7 @@ import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||
export function ControlsPanel(props: {}) {
|
||||
const [controls, setControls] = useState(
|
||||
[] as {
|
||||
actions: (string | IconDefinition)[];
|
||||
actions: (string | number | IconDefinition)[];
|
||||
target: IconDefinition;
|
||||
text: string;
|
||||
}[]
|
||||
@ -17,9 +17,11 @@ export function ControlsPanel(props: {}) {
|
||||
}
|
||||
});
|
||||
|
||||
document.addEventListener("mapStateChanged", (ev) => {
|
||||
setControls(getApp().getMap().getCurrentControls());
|
||||
});
|
||||
useEffect(() => {
|
||||
document.addEventListener("mapStateChanged", (ev) => {
|
||||
setControls(getApp().getMap().getCurrentControls());
|
||||
});
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<div
|
||||
@ -48,18 +50,12 @@ export function ControlsPanel(props: {}) {
|
||||
return (
|
||||
<>
|
||||
<div className={``}>
|
||||
{typeof action === "string" ? (
|
||||
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 && <div>+</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>}
|
||||
</>
|
||||
);
|
||||
})}
|
||||
|
||||
@ -39,26 +39,26 @@ export function DrawingMenu(props: { open: boolean; onClose: () => void }) {
|
||||
|
||||
/* Align the state of the coalition toggle to the coalition of the area */
|
||||
if (activeCoalitionArea && activeCoalitionArea?.getCoalition() !== areaCoalition) setAreaCoalition(activeCoalitionArea?.getCoalition());
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
if (!props.open) {
|
||||
if ([COALITIONAREA_EDIT, COALITIONAREA_DRAW_CIRCLE, COALITIONAREA_DRAW_POLYGON].includes(getApp()?.getMap()?.getState()))
|
||||
getApp().getMap().setState(IDLE);
|
||||
}
|
||||
});
|
||||
|
||||
document.addEventListener("mapStateChanged", (event: any) => {
|
||||
if (drawingPolygon && getApp().getMap().getState() !== COALITIONAREA_DRAW_POLYGON) setDrawingPolygon(false);
|
||||
useEffect(() => {
|
||||
document.addEventListener("mapStateChanged", (event: any) => {
|
||||
if (drawingPolygon && getApp().getMap().getState() !== COALITIONAREA_DRAW_POLYGON) setDrawingPolygon(false);
|
||||
|
||||
if (getApp().getMap().getState() == COALITIONAREA_EDIT) {
|
||||
setActiveCoalitionArea(getApp().getMap().getSelectedCoalitionArea() ?? null);
|
||||
}
|
||||
});
|
||||
if (getApp().getMap().getState() == COALITIONAREA_EDIT) {
|
||||
setActiveCoalitionArea(getApp().getMap().getSelectedCoalitionArea() ?? null);
|
||||
}
|
||||
});
|
||||
|
||||
document.addEventListener("coalitionAreaSelected", (event: any) => {
|
||||
setActiveCoalitionArea(event.detail);
|
||||
});
|
||||
document.addEventListener("coalitionAreaSelected", (event: any) => {
|
||||
setActiveCoalitionArea(event.detail);
|
||||
});
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<Menu
|
||||
@ -232,7 +232,7 @@ export function DrawingMenu(props: { open: boolean; onClose: () => void }) {
|
||||
setTypesSelection(JSON.parse(JSON.stringify(typesSelection)));
|
||||
}}
|
||||
/>
|
||||
{type}
|
||||
<div>{type}</div>
|
||||
</OlDropdownItem>
|
||||
);
|
||||
})}
|
||||
@ -256,7 +256,7 @@ export function DrawingMenu(props: { open: boolean; onClose: () => void }) {
|
||||
setErasSelection(JSON.parse(JSON.stringify(erasSelection)));
|
||||
}}
|
||||
/>
|
||||
{era}
|
||||
<div>{era}</div>
|
||||
</OlDropdownItem>
|
||||
);
|
||||
})}
|
||||
@ -277,7 +277,7 @@ export function DrawingMenu(props: { open: boolean; onClose: () => void }) {
|
||||
setErasSelection(JSON.parse(JSON.stringify(rangesSelection)));
|
||||
}}
|
||||
/>
|
||||
{range}
|
||||
<div>{range}</div>
|
||||
</OlDropdownItem>
|
||||
);
|
||||
})}
|
||||
|
||||
@ -27,9 +27,8 @@ export function Header() {
|
||||
/* Initialize the "scroll" position of the element */
|
||||
var scrollRef = useRef(null);
|
||||
useEffect(() => {
|
||||
if (scrollRef.current) {
|
||||
if (scrollRef.current)
|
||||
onScroll(scrollRef.current);
|
||||
}
|
||||
});
|
||||
|
||||
function onScroll(el) {
|
||||
@ -56,10 +55,8 @@ export function Header() {
|
||||
`}
|
||||
>
|
||||
<img
|
||||
src="vite/images/icon.png"
|
||||
className={`
|
||||
my-auto h-10 w-10 rounded-md p-0
|
||||
`}
|
||||
src="images/icon.png"
|
||||
className={`my-auto h-10 w-10 rounded-md p-0`}
|
||||
></img>
|
||||
{!scrolledLeft && (
|
||||
<FaChevronLeft
|
||||
|
||||
@ -18,15 +18,17 @@ export function MiniMapPanel(props: {}) {
|
||||
const [showMissionTime, setShowMissionTime] = useState(false);
|
||||
const [showMinimap, setShowMinimap] = useState(false);
|
||||
|
||||
document.addEventListener("serverStatusUpdated", (ev) => {
|
||||
const detail = (ev as CustomEvent).detail;
|
||||
setFrameRate(detail.frameRate);
|
||||
setLoad(detail.load);
|
||||
setElapsedTime(detail.elapsedTime);
|
||||
setMissionTime(detail.missionTime);
|
||||
setConnected(detail.connected);
|
||||
setPaused(detail.paused);
|
||||
});
|
||||
useEffect(() => {
|
||||
document.addEventListener("serverStatusUpdated", (ev) => {
|
||||
const detail = (ev as CustomEvent).detail;
|
||||
setFrameRate(detail.frameRate);
|
||||
setLoad(detail.load);
|
||||
setElapsedTime(detail.elapsedTime);
|
||||
setMissionTime(detail.missionTime);
|
||||
setConnected(detail.connected);
|
||||
setPaused(detail.paused);
|
||||
});
|
||||
}, []);
|
||||
|
||||
// A bit of a hack to set the rounded borders to the minimap
|
||||
useEffect(() => {
|
||||
|
||||
@ -1,8 +1,9 @@
|
||||
import React, { useState } from "react";
|
||||
import { OlStateButton } from "../components/olstatebutton";
|
||||
import { faGamepad, faRuler, faPencil, faEllipsisV, faCog, faQuestionCircle, faPlusSquare } from "@fortawesome/free-solid-svg-icons";
|
||||
import { faGamepad, faRuler, faPencil, faEllipsisV, faCog, faQuestionCircle, faPlusSquare, faMagnifyingGlass } from "@fortawesome/free-solid-svg-icons";
|
||||
import { EventsConsumer } from "../../eventscontext";
|
||||
import { StateConsumer } from "../../statecontext";
|
||||
import { IDLE } from "../../constants/constants";
|
||||
|
||||
export function SideBar() {
|
||||
return (
|
||||
@ -12,7 +13,7 @@ export function SideBar() {
|
||||
{(events) => (
|
||||
<nav
|
||||
className={`
|
||||
absolute left-0 z-20 flex h-full flex-col bg-gray-300
|
||||
z-20 flex h-full flex-col bg-gray-300
|
||||
dark:bg-olympus-900
|
||||
`}
|
||||
>
|
||||
@ -41,10 +42,10 @@ export function SideBar() {
|
||||
<OlStateButton
|
||||
onClick={events.toggleUnitControlMenuVisible}
|
||||
checked={appState.unitControlMenuVisible}
|
||||
icon={faGamepad}
|
||||
tooltip=""
|
||||
icon={appState.mapState === IDLE? faMagnifyingGlass: faGamepad}
|
||||
tooltip="Hide/show selection tool and unit control menu"
|
||||
></OlStateButton>
|
||||
<OlStateButton onClick={events.toggleMeasureMenuVisible} checked={appState.measureMenuVisible} icon={faRuler} tooltip=""></OlStateButton>
|
||||
<OlStateButton onClick={events.toggleMeasureMenuVisible} checked={appState.measureMenuVisible} icon={faRuler} tooltip="NOT IMPLEMENTED"></OlStateButton>
|
||||
<OlStateButton
|
||||
onClick={events.toggleDrawingMenuVisible}
|
||||
checked={appState.drawingMenuVisible}
|
||||
|
||||
@ -38,7 +38,7 @@ import {
|
||||
olButtonsVisibilityOlympus,
|
||||
} from "../components/olicons";
|
||||
import { Coalition } from "../../types/types";
|
||||
import { ftToM, getUnitsByLabel, knotsToMs, mToFt, msToKnots } from "../../other/utils";
|
||||
import { ftToM, getUnitDatabaseByCategory, getUnitsByLabel, knotsToMs, mToFt, msToKnots } from "../../other/utils";
|
||||
import { FaCog, FaGasPump, FaSignal, FaTag } from "react-icons/fa";
|
||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||
import { OlSearchBar } from "../components/olsearchbar";
|
||||
@ -105,9 +105,7 @@ export function UnitControlMenu(props: { open: boolean; onClose: () => void }) {
|
||||
|
||||
useEffect(() => {
|
||||
if (!searchBarRefState) setSearchBarRefState(searchBarRef);
|
||||
|
||||
if (!props.open && selectionBlueprint !== null) setSelectionBlueprint(null);
|
||||
|
||||
if (!props.open && filterString !== "") setFilterString("");
|
||||
});
|
||||
|
||||
@ -147,23 +145,23 @@ export function UnitControlMenu(props: { open: boolean; onClose: () => void }) {
|
||||
? 5
|
||||
: 10;
|
||||
|
||||
/* When a unit is selected, open the menu */
|
||||
document.addEventListener("unitsSelection", (ev: CustomEventInit) => {
|
||||
setSelectedUnits(ev.detail as Unit[]);
|
||||
useEffect(() => {
|
||||
/* When a unit is selected, update the data */
|
||||
document.addEventListener("unitsSelection", (ev: CustomEventInit) => {
|
||||
setSelectedUnits(ev.detail as Unit[]);
|
||||
updateData();
|
||||
});
|
||||
|
||||
updateData();
|
||||
});
|
||||
/* When a unit is deselected, refresh the view */
|
||||
document.addEventListener("unitDeselection", (ev: CustomEventInit) => {
|
||||
window.setTimeout(() => updateData(), 200);
|
||||
});
|
||||
|
||||
/* When a unit is deselected, refresh the view */
|
||||
document.addEventListener("unitDeselection", (ev: CustomEventInit) => {
|
||||
/* TODO add delay to avoid doing it too many times */
|
||||
updateData();
|
||||
});
|
||||
|
||||
/* When all units are deselected clean the view */
|
||||
document.addEventListener("clearSelection", () => {
|
||||
setSelectedUnits([]);
|
||||
});
|
||||
/* When all units are deselected clean the view */
|
||||
document.addEventListener("clearSelection", () => {
|
||||
setSelectedUnits([]);
|
||||
});
|
||||
}, []);
|
||||
|
||||
/* Update the current values of the shown data */
|
||||
function updateData() {
|
||||
@ -220,15 +218,20 @@ export function UnitControlMenu(props: { open: boolean; onClose: () => void }) {
|
||||
}
|
||||
|
||||
/* Count how many units are selected of each type, divided by coalition */
|
||||
var unitOccurences = {
|
||||
var unitOccurences: {
|
||||
blue: { [key: string]: { label: string; occurences: number } };
|
||||
red: { [key: string]: { label: string; occurences: number } };
|
||||
neutral: { [key: string]: { label: string; occurences: number } };
|
||||
} = {
|
||||
blue: {},
|
||||
red: {},
|
||||
neutral: {},
|
||||
};
|
||||
|
||||
selectedUnits.forEach((unit) => {
|
||||
if (!(unit.getName() in unitOccurences[unit.getCoalition()])) unitOccurences[unit.getCoalition()][unit.getName()] = 1;
|
||||
else unitOccurences[unit.getCoalition()][unit.getName()]++;
|
||||
if (!(unit.getName() in unitOccurences[unit.getCoalition()]))
|
||||
unitOccurences[unit.getCoalition()][unit.getName()] = { occurences: 1, label: unit.getBlueprint()?.label };
|
||||
else unitOccurences[unit.getCoalition()][unit.getName()].occurences++;
|
||||
});
|
||||
|
||||
const selectedCategories = getApp()?.getUnitsManager()?.getSelectedUnitsCategories() ?? [];
|
||||
@ -421,7 +424,7 @@ export function UnitControlMenu(props: { open: boolean; onClose: () => void }) {
|
||||
|
||||
/* If a specific unit is being selected check that the label is correct, otherwise check if the unit type is active for the coalition */
|
||||
if (selectionBlueprint) {
|
||||
if (unit.getDatabaseEntry()?.label === undefined || unit.getDatabaseEntry()?.label !== selectionBlueprint.label) return;
|
||||
if (unit.getBlueprint()?.label === undefined || unit.getBlueprint()?.label !== selectionBlueprint.label) return;
|
||||
|
||||
/* This is a trick to easily reuse the same checkboxes used to globally enable unit types for a coalition,
|
||||
since those checkboxes are checked if at least one type is selected for a specific coalition.
|
||||
@ -480,7 +483,7 @@ export function UnitControlMenu(props: { open: boolean; onClose: () => void }) {
|
||||
dark:text-white
|
||||
`}
|
||||
>
|
||||
{name}
|
||||
{unitOccurences[coalition][name].label}
|
||||
</span>
|
||||
<span
|
||||
className={`
|
||||
@ -488,7 +491,7 @@ export function UnitControlMenu(props: { open: boolean; onClose: () => void }) {
|
||||
dark:text-gray-500
|
||||
`}
|
||||
>
|
||||
x{unitOccurences[coalition][name]}
|
||||
x{unitOccurences[coalition][name].occurences}
|
||||
</span>
|
||||
</div>
|
||||
);
|
||||
@ -1337,7 +1340,7 @@ export function UnitControlMenu(props: { open: boolean; onClose: () => void }) {
|
||||
<div className="flex content-center gap-2">
|
||||
<div
|
||||
className={`
|
||||
my-auto w-6 min-w-6 rounded-full py-0.5
|
||||
my-auto w-fit rounded-full px-2 py-0.5
|
||||
text-center text-sm font-bold text-gray-500
|
||||
dark:bg-[#17212D]
|
||||
`}
|
||||
|
||||
@ -10,7 +10,6 @@ import { FaChevronLeft, FaChevronRight } from "react-icons/fa6";
|
||||
|
||||
export function UnitMouseControlBar(props: {}) {
|
||||
const [open, setOpen] = useState(false);
|
||||
const [selectedUnits, setSelectedUnits] = useState([] as Unit[]);
|
||||
const [contextActionsSet, setContextActionsSet] = useState(new ContextActionSet());
|
||||
const [activeContextAction, setActiveContextAction] = useState(null as null | ContextAction);
|
||||
const [scrolledLeft, setScrolledLeft] = useState(true);
|
||||
@ -19,36 +18,34 @@ export function UnitMouseControlBar(props: {}) {
|
||||
/* Initialize the "scroll" position of the element */
|
||||
var scrollRef = useRef(null);
|
||||
useEffect(() => {
|
||||
if (scrollRef.current) {
|
||||
if (scrollRef.current)
|
||||
onScroll(scrollRef.current);
|
||||
}
|
||||
});
|
||||
|
||||
/* When a unit is selected, open the menu */
|
||||
document.addEventListener("unitsSelection", (ev: CustomEventInit) => {
|
||||
setOpen(true);
|
||||
setSelectedUnits(ev.detail as Unit[]);
|
||||
useEffect(() => {
|
||||
/* When a unit is selected, open the menu */
|
||||
document.addEventListener("unitsSelection", (ev: CustomEventInit) => {
|
||||
setOpen(true);
|
||||
updateData();
|
||||
setActiveContextAction(null);
|
||||
});
|
||||
|
||||
updateData();
|
||||
});
|
||||
/* When a unit is deselected, refresh the view */
|
||||
document.addEventListener("unitDeselection", (ev: CustomEventInit) => {
|
||||
window.setTimeout(() => updateData(), 200);
|
||||
});
|
||||
|
||||
/* When a unit is deselected, refresh the view */
|
||||
document.addEventListener("unitDeselection", (ev: CustomEventInit) => {
|
||||
/* TODO add delay to avoid doing it too many times */
|
||||
updateData();
|
||||
});
|
||||
/* When all units are deselected clean the view */
|
||||
document.addEventListener("clearSelection", () => {
|
||||
setOpen(false);
|
||||
updateData();
|
||||
});
|
||||
|
||||
/* When all units are deselected clean the view */
|
||||
document.addEventListener("clearSelection", () => {
|
||||
setOpen(false);
|
||||
setSelectedUnits([]);
|
||||
updateData();
|
||||
});
|
||||
|
||||
/* Deselect the context action when exiting state */
|
||||
document.addEventListener("mapStateChanged", (ev) => {
|
||||
setOpen((ev as CustomEvent).detail === CONTEXT_ACTION);
|
||||
});
|
||||
/* Deselect the context action when exiting state */
|
||||
document.addEventListener("mapStateChanged", (ev) => {
|
||||
setOpen((ev as CustomEvent).detail === CONTEXT_ACTION);
|
||||
});
|
||||
}, []);
|
||||
|
||||
/* Update the current values of the shown data */
|
||||
function updateData() {
|
||||
@ -62,7 +59,7 @@ export function UnitMouseControlBar(props: {}) {
|
||||
});
|
||||
|
||||
setContextActionsSet(newContextActionSet);
|
||||
setActiveContextAction(null);
|
||||
return newContextActionSet;
|
||||
}
|
||||
|
||||
function onScroll(el) {
|
||||
@ -78,7 +75,7 @@ export function UnitMouseControlBar(props: {}) {
|
||||
|
||||
return (
|
||||
<>
|
||||
{open && (
|
||||
{open && Object.keys(contextActionsSet.getContextActions()).length > 0 && (
|
||||
<>
|
||||
<div
|
||||
className={`
|
||||
@ -108,15 +105,17 @@ export function UnitMouseControlBar(props: {}) {
|
||||
setActiveContextAction(null);
|
||||
contextAction.executeCallback(null, null);
|
||||
} else {
|
||||
if (activeContextAction != contextAction) {
|
||||
if (activeContextAction !== contextAction) {
|
||||
setActiveContextAction(contextAction);
|
||||
getApp().getMap().setState(CONTEXT_ACTION, {
|
||||
contextAction: contextAction,
|
||||
defaultContextAction: contextActionsSet.getDefaultContextAction()
|
||||
});
|
||||
} else {
|
||||
setActiveContextAction(null);
|
||||
getApp().getMap().setState(CONTEXT_ACTION, {
|
||||
contextAction: null,
|
||||
defaultContextAction: contextActionsSet.getDefaultContextAction()
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@ -11,7 +11,7 @@ import { MainMenu } from "./panels/mainmenu";
|
||||
import { SideBar } from "./panels/sidebar";
|
||||
import { Options } from "./panels/options";
|
||||
import { MapHiddenTypes, MapOptions } from "../types/types";
|
||||
import { BLUE_COMMANDER, GAME_MASTER, IDLE, MAP_HIDDEN_TYPES_DEFAULTS, MAP_OPTIONS_DEFAULTS, RED_COMMANDER } from "../constants/constants";
|
||||
import { BLUE_COMMANDER, CONTEXT_ACTION, GAME_MASTER, IDLE, MAP_HIDDEN_TYPES_DEFAULTS, MAP_OPTIONS_DEFAULTS, RED_COMMANDER } from "../constants/constants";
|
||||
import { getApp, setupApp } from "../olympusapp";
|
||||
import { LoginModal } from "./modals/login";
|
||||
import { sha256 } from "js-sha256";
|
||||
@ -19,6 +19,7 @@ import { MiniMapPanel } from "./panels/minimappanel";
|
||||
import { UnitMouseControlBar } from "./panels/unitmousecontrolbar";
|
||||
import { DrawingMenu } from "./panels/drawingmenu";
|
||||
import { ControlsPanel } from "./panels/controls";
|
||||
import { MapContextMenu } from "./contextmenus/mapcontextmenu";
|
||||
|
||||
export type OlympusState = {
|
||||
mainMenuVisible: boolean;
|
||||
@ -46,42 +47,35 @@ export function UI() {
|
||||
const [commandMode, setCommandMode] = useState(null as null | string);
|
||||
const [mapSources, setMapSources] = useState([] as string[]);
|
||||
const [activeMapSource, setActiveMapSource] = useState("");
|
||||
const [mapBoxSelection, setMapBoxSelection] = useState(false);
|
||||
const [mapState, setMapState] = useState(IDLE);
|
||||
|
||||
document.addEventListener("hiddenTypesChanged", (ev) => {
|
||||
setMapHiddenTypes({ ...getApp().getMap().getHiddenTypes() });
|
||||
});
|
||||
useEffect(() => {
|
||||
document.addEventListener("hiddenTypesChanged", (ev) => {
|
||||
setMapHiddenTypes({ ...getApp().getMap().getHiddenTypes() });
|
||||
});
|
||||
|
||||
document.addEventListener("mapOptionsChanged", (ev) => {
|
||||
setMapOptions({ ...getApp().getMap().getOptions() });
|
||||
});
|
||||
document.addEventListener("mapOptionsChanged", (ev) => {
|
||||
setMapOptions({ ...getApp().getMap().getOptions() });
|
||||
});
|
||||
|
||||
document.addEventListener("mapStateChanged", (ev) => {
|
||||
if ((ev as CustomEvent).detail === IDLE && mapState !== IDLE) hideAllMenus();
|
||||
document.addEventListener("mapStateChanged", (ev) => {
|
||||
if ((ev as CustomEvent).detail === IDLE) hideAllMenus();
|
||||
else if ((ev as CustomEvent).detail === CONTEXT_ACTION) setUnitControlMenuVisible(true);
|
||||
setMapState(String((ev as CustomEvent).detail));
|
||||
});
|
||||
|
||||
setMapState(String((ev as CustomEvent).detail));
|
||||
});
|
||||
document.addEventListener("mapSourceChanged", (ev) => {
|
||||
var source = (ev as CustomEvent).detail;
|
||||
setActiveMapSource(source);
|
||||
});
|
||||
|
||||
document.addEventListener("mapSourceChanged", (ev) => {
|
||||
var source = (ev as CustomEvent).detail;
|
||||
if (source !== activeMapSource) setActiveMapSource(source);
|
||||
});
|
||||
|
||||
document.addEventListener("configLoaded", (ev) => {
|
||||
let config = getApp().getConfig();
|
||||
var sources = Object.keys(config.mapMirrors).concat(Object.keys(config.mapLayers));
|
||||
setMapSources(sources);
|
||||
setActiveMapSource(sources[0]);
|
||||
});
|
||||
|
||||
document.addEventListener("mapForceBoxSelect", (ev) => {
|
||||
setMapBoxSelection(true);
|
||||
});
|
||||
|
||||
document.addEventListener("mapSelectionEnd", (ev) => {
|
||||
setMapBoxSelection(false);
|
||||
});
|
||||
document.addEventListener("configLoaded", (ev) => {
|
||||
let config = getApp().getConfig();
|
||||
var sources = Object.keys(config.mapMirrors).concat(Object.keys(config.mapLayers));
|
||||
setMapSources(sources);
|
||||
setActiveMapSource(sources[0]);
|
||||
});
|
||||
}, []);
|
||||
|
||||
function hideAllMenus() {
|
||||
setMainMenuVisible(false);
|
||||
@ -132,7 +126,8 @@ export function UI() {
|
||||
return (
|
||||
<div
|
||||
className={`
|
||||
absolute left-0 top-0 h-screen w-screen overflow-hidden font-sans
|
||||
absolute left-0 top-0 flex h-screen w-screen flex-col overflow-hidden
|
||||
font-sans
|
||||
`}
|
||||
onLoad={setupApp}
|
||||
>
|
||||
@ -148,7 +143,7 @@ export function UI() {
|
||||
mapHiddenTypes: mapHiddenTypes,
|
||||
mapSources: mapSources,
|
||||
activeMapSource: activeMapSource,
|
||||
mapBoxSelection: mapBoxSelection,
|
||||
mapState: mapState,
|
||||
}}
|
||||
>
|
||||
<EventsProvider
|
||||
@ -185,48 +180,43 @@ export function UI() {
|
||||
},
|
||||
}}
|
||||
>
|
||||
<div
|
||||
className={`
|
||||
absolute left-0 top-0 flex h-full w-full flex-col
|
||||
`}
|
||||
>
|
||||
<Header />
|
||||
<div className="flex justify-reverse h-full">
|
||||
{loginModalVisible && (
|
||||
<>
|
||||
<div
|
||||
className={`
|
||||
fixed left-0 top-0 z-30 h-full w-full bg-[#111111]/95
|
||||
`}
|
||||
></div>
|
||||
<LoginModal
|
||||
onLogin={(password) => {
|
||||
checkPassword(password);
|
||||
}}
|
||||
onContinue={(username) => {
|
||||
connect(username);
|
||||
}}
|
||||
onBack={() => {
|
||||
setCommandMode(null);
|
||||
}}
|
||||
checkingPassword={checkingPassword}
|
||||
loginError={loginError}
|
||||
commandMode={commandMode}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
<div id="map-container" className="z-0 h-full w-screen" />
|
||||
<MainMenu open={mainMenuVisible} onClose={() => setMainMenuVisible(false)} />
|
||||
<SpawnMenu open={spawnMenuVisible} onClose={() => setSpawnMenuVisible(false)} />
|
||||
<Options open={optionsMenuVisible} onClose={() => setOptionsMenuVisible(false)} options={mapOptions} />
|
||||
<MiniMapPanel />
|
||||
<ControlsPanel />
|
||||
<UnitControlMenu open={unitControlMenuVisible} onClose={() => setUnitControlMenuVisible(false)} />
|
||||
<DrawingMenu open={drawingMenuVisible} onClose={() => setDrawingMenuVisible(false)} />
|
||||
<Header />
|
||||
<div className="flex h-full w-full flex-row-reverse">
|
||||
{loginModalVisible && (
|
||||
<>
|
||||
<div
|
||||
className={`
|
||||
fixed left-0 top-0 z-30 h-full w-full bg-[#111111]/95
|
||||
`}
|
||||
></div>
|
||||
<LoginModal
|
||||
onLogin={(password) => {
|
||||
checkPassword(password);
|
||||
}}
|
||||
onContinue={(username) => {
|
||||
connect(username);
|
||||
}}
|
||||
onBack={() => {
|
||||
setCommandMode(null);
|
||||
}}
|
||||
checkingPassword={checkingPassword}
|
||||
loginError={loginError}
|
||||
commandMode={commandMode}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
<div id="map-container" className="z-0 h-full w-screen" />
|
||||
<MainMenu open={mainMenuVisible} onClose={() => setMainMenuVisible(false)} />
|
||||
<SpawnMenu open={spawnMenuVisible} onClose={() => setSpawnMenuVisible(false)} />
|
||||
<Options open={optionsMenuVisible} onClose={() => setOptionsMenuVisible(false)} options={mapOptions} />
|
||||
<UnitControlMenu open={unitControlMenuVisible} onClose={() => setUnitControlMenuVisible(false)} />
|
||||
<DrawingMenu open={drawingMenuVisible} onClose={() => setDrawingMenuVisible(false)} />
|
||||
|
||||
<UnitMouseControlBar />
|
||||
<SideBar />
|
||||
</div>
|
||||
<MiniMapPanel />
|
||||
<ControlsPanel />
|
||||
<UnitMouseControlBar />
|
||||
<MapContextMenu />
|
||||
<SideBar />
|
||||
</div>
|
||||
</EventsProvider>
|
||||
</StateProvider>
|
||||
|
||||
@ -16,11 +16,13 @@ export class ContextAction {
|
||||
#units: Unit[] = [];
|
||||
#icon: IconDefinition;
|
||||
#options: ContextActionOptions;
|
||||
#target: "unit" | "position" | null = null;
|
||||
|
||||
constructor(id: string, label: string, description: string, icon: IconDefinition, callback: ContextActionCallback, options: ContextActionOptions) {
|
||||
constructor(id: string, label: string, description: string, icon: IconDefinition, target: "unit" | "position" | null, callback: ContextActionCallback, options: ContextActionOptions) {
|
||||
this.#id = id;
|
||||
this.#label = label;
|
||||
this.#description = description;
|
||||
this.#target = target;
|
||||
this.#callback = callback;
|
||||
this.#icon = icon;
|
||||
this.#options = {
|
||||
@ -57,6 +59,10 @@ export class ContextAction {
|
||||
return this.#icon;
|
||||
}
|
||||
|
||||
getTarget() {
|
||||
return this.#target;
|
||||
}
|
||||
|
||||
executeCallback(targetUnit: Unit | null, targetPosition: LatLng | null) {
|
||||
if (this.#callback) this.#callback(this.#units, targetUnit, targetPosition);
|
||||
}
|
||||
|
||||
@ -4,6 +4,7 @@ import { IconDefinition } from "@fortawesome/fontawesome-svg-core";
|
||||
|
||||
export class ContextActionSet {
|
||||
#contextActions: { [key: string]: ContextAction } = {};
|
||||
#defaultContextAction: ContextAction | null = null;
|
||||
|
||||
addContextAction(
|
||||
unit: Unit,
|
||||
@ -11,18 +12,44 @@ export class ContextActionSet {
|
||||
label: string,
|
||||
description: string,
|
||||
icon: IconDefinition,
|
||||
target: "unit" | "position" | null,
|
||||
callback: ContextActionCallback,
|
||||
options?: ContextActionOptions
|
||||
) {
|
||||
options = options || {};
|
||||
|
||||
if (!(id in this.#contextActions)) {
|
||||
this.#contextActions[id] = new ContextAction(id, label, description, icon, callback, options);
|
||||
this.#contextActions[id] = new ContextAction(id, label, description, icon, target, callback, options);
|
||||
}
|
||||
this.#contextActions[id].addUnit(unit);
|
||||
}
|
||||
|
||||
getContextActions() {
|
||||
return this.#contextActions;
|
||||
getContextActions(targetFilter?: string) {
|
||||
if (targetFilter) {
|
||||
var filteredContextActionSet = new ContextActionSet();
|
||||
Object.keys(this.#contextActions).forEach((key) => {
|
||||
if (this.#contextActions[key].getTarget() === targetFilter) filteredContextActionSet[key] = this.#contextActions[key];
|
||||
});
|
||||
return filteredContextActionSet;
|
||||
} else return this.#contextActions;
|
||||
}
|
||||
|
||||
addDefaultContextAction(
|
||||
unit: Unit,
|
||||
id: string,
|
||||
label: string,
|
||||
description: string,
|
||||
icon: IconDefinition,
|
||||
target: "unit" | "position" | null,
|
||||
callback: ContextActionCallback,
|
||||
options?: ContextActionOptions
|
||||
) {
|
||||
options = options || {};
|
||||
if (this.#defaultContextAction === null) this.#defaultContextAction = new ContextAction(id, label, description, icon, target, callback, options);
|
||||
this.#defaultContextAction.addUnit(unit);
|
||||
}
|
||||
|
||||
getDefaultContextAction() {
|
||||
return this.#defaultContextAction;
|
||||
}
|
||||
}
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
import { Marker, LatLng, Polyline, Icon, DivIcon, CircleMarker, Map, Point } from "leaflet";
|
||||
import { Marker, LatLng, Polyline, Icon, DivIcon, CircleMarker, Map, Point, LeafletMouseEvent, DomEvent, DomUtil } from "leaflet";
|
||||
import { getApp } from "../olympusapp";
|
||||
import {
|
||||
enumToCoalition,
|
||||
@ -77,6 +77,7 @@ import {
|
||||
faXmarksLines,
|
||||
} from "@fortawesome/free-solid-svg-icons";
|
||||
import { FaXmarksLines } from "react-icons/fa6";
|
||||
import { ContextAction } from "./contextaction";
|
||||
|
||||
var pathIcon = new Icon({
|
||||
iconUrl: "/vite/images/markers/marker-icon.png",
|
||||
@ -160,7 +161,6 @@ export abstract class Unit extends CustomMarker {
|
||||
#selected: boolean = false;
|
||||
#hidden: boolean = false;
|
||||
#highlighted: boolean = false;
|
||||
#waitingForDoubleClick: boolean = false;
|
||||
#pathMarkers: Marker[] = [];
|
||||
#pathPolyline: Polyline;
|
||||
#contactsPolylines: Polyline[] = [];
|
||||
@ -169,10 +169,16 @@ export abstract class Unit extends CustomMarker {
|
||||
#miniMapMarker: CircleMarker | null = null;
|
||||
#targetPositionMarker: TargetMarker;
|
||||
#targetPositionPolyline: Polyline;
|
||||
#doubleClickTimer: number = 0;
|
||||
#hotgroup: number | null = null;
|
||||
#detectionMethods: number[] = [];
|
||||
|
||||
/* Inputs timers */
|
||||
#mouseCooldownTimer: number = 0;
|
||||
#shortPressTimer: number = 0;
|
||||
#longPressTimer: number = 0;
|
||||
#isMouseOnCooldown: boolean = false;
|
||||
#isMouseDown: boolean = false;
|
||||
|
||||
/* Getters for backend driven data */
|
||||
getAlive() {
|
||||
return this.#alive;
|
||||
@ -315,7 +321,7 @@ export abstract class Unit extends CustomMarker {
|
||||
}
|
||||
|
||||
constructor(ID: number) {
|
||||
super(new LatLng(0, 0), { riseOnHover: true, keyboard: false });
|
||||
super(new LatLng(0, 0), { riseOnHover: true, keyboard: false, bubblingMouseEvents: false });
|
||||
|
||||
this.ID = ID;
|
||||
|
||||
@ -353,7 +359,8 @@ export abstract class Unit extends CustomMarker {
|
||||
});
|
||||
|
||||
/* Leaflet events listeners */
|
||||
this.on("click", (e) => this.#onClick(e));
|
||||
this.on("mousedown", (e) => this.#onMouseDown(e));
|
||||
this.on("mouseup", (e) => this.#onMouseUp(e));
|
||||
this.on("dblclick", (e) => this.#onDoubleClick(e));
|
||||
this.on("mouseover", () => {
|
||||
if (this.belongsToCommandedCoalition()) {
|
||||
@ -802,7 +809,7 @@ export abstract class Unit extends CustomMarker {
|
||||
return this.getDatabase()?.getSpawnPointsByName(this.getName());
|
||||
}
|
||||
|
||||
getDatabaseEntry() {
|
||||
getBlueprint() {
|
||||
return this.getDatabase()?.getByName(this.#name) ?? this.getDatabase()?.getUnkownUnit(this.getName());
|
||||
}
|
||||
|
||||
@ -824,9 +831,10 @@ export abstract class Unit extends CustomMarker {
|
||||
"Set destination",
|
||||
"Click on the map to move the units there",
|
||||
faLocationDot,
|
||||
"position",
|
||||
(units: Unit[], _, targetPosition) => {
|
||||
getApp().getUnitsManager().clearDestinations(units);
|
||||
if (targetPosition) getApp().getUnitsManager().addDestination(targetPosition, false, 0);
|
||||
if (targetPosition) getApp().getUnitsManager().addDestination(targetPosition, false, 0, units);
|
||||
}
|
||||
);
|
||||
|
||||
@ -836,10 +844,26 @@ export abstract class Unit extends CustomMarker {
|
||||
"Append destination",
|
||||
"Click on the map to add a destination to the path",
|
||||
faRoute,
|
||||
"position",
|
||||
(units: Unit[], _, targetPosition) => {
|
||||
if (targetPosition) getApp().getUnitsManager().addDestination(targetPosition, false, 0, units);
|
||||
}
|
||||
);
|
||||
|
||||
contextActionSet.addDefaultContextAction(
|
||||
this,
|
||||
"default",
|
||||
"Set destination",
|
||||
"",
|
||||
faRoute,
|
||||
null,
|
||||
(units: Unit[], targetUnit, targetPosition) => {
|
||||
if (targetPosition) {
|
||||
getApp().getUnitsManager().clearDestinations(units);
|
||||
getApp().getUnitsManager().addDestination(targetPosition, false, 0, units);
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
drawLines() {
|
||||
@ -909,7 +933,7 @@ export abstract class Unit extends CustomMarker {
|
||||
/* If a unit does not belong to the commanded coalition or it is not visually detected, show it with the generic aircraft square */
|
||||
var marker;
|
||||
if (this.belongsToCommandedCoalition() || this.getDetectionMethods().some((value) => [VISUAL, OPTIC].includes(value)))
|
||||
marker = this.getDatabaseEntry()?.markerFile ?? this.getDefaultMarker();
|
||||
marker = this.getBlueprint()?.markerFile ?? this.getDefaultMarker();
|
||||
else marker = "aircraft";
|
||||
img.src = `/vite/images/units/${marker}.svg`;
|
||||
img.onload = () => SVGInjector(img);
|
||||
@ -930,7 +954,7 @@ export abstract class Unit extends CustomMarker {
|
||||
if (iconOptions.showShortLabel) {
|
||||
var shortLabel = document.createElement("div");
|
||||
shortLabel.classList.add("unit-short-label");
|
||||
shortLabel.innerText = this.getDatabaseEntry()?.shortLabel || "";
|
||||
shortLabel.innerText = this.getBlueprint()?.shortLabel || "";
|
||||
el.append(shortLabel);
|
||||
}
|
||||
|
||||
@ -1068,7 +1092,7 @@ export abstract class Unit extends CustomMarker {
|
||||
canFulfillRole(roles: string | string[]) {
|
||||
if (typeof roles === "string") roles = [roles];
|
||||
|
||||
var loadouts = this.getDatabaseEntry()?.loadouts;
|
||||
var loadouts = this.getBlueprint()?.loadouts;
|
||||
if (loadouts) {
|
||||
return loadouts.some((loadout: LoadoutBlueprint) => {
|
||||
return (roles as string[]).some((role: string) => {
|
||||
@ -1083,19 +1107,19 @@ export abstract class Unit extends CustomMarker {
|
||||
}
|
||||
|
||||
canTargetPoint() {
|
||||
return this.getDatabaseEntry()?.canTargetPoint === true;
|
||||
return this.getBlueprint()?.canTargetPoint === true;
|
||||
}
|
||||
|
||||
canRearm() {
|
||||
return this.getDatabaseEntry()?.canRearm === true;
|
||||
return this.getBlueprint()?.canRearm === true;
|
||||
}
|
||||
|
||||
canAAA() {
|
||||
return this.getDatabaseEntry()?.canAAA === true;
|
||||
return this.getBlueprint()?.canAAA === true;
|
||||
}
|
||||
|
||||
isIndirectFire() {
|
||||
return this.getDatabaseEntry()?.indirectFire === true;
|
||||
return this.getBlueprint()?.indirectFire === true;
|
||||
}
|
||||
|
||||
isTanker() {
|
||||
@ -1282,13 +1306,13 @@ export abstract class Unit extends CustomMarker {
|
||||
var contextActionSet = new ContextActionSet();
|
||||
|
||||
// TODO FIX
|
||||
contextActionSet.addContextAction(this, "trail", "Trail", "Follow unit in trail formation", olButtonsContextTrail, () =>
|
||||
contextActionSet.addContextAction(this, "trail", "Trail", "Follow unit in trail formation", olButtonsContextTrail, null, () =>
|
||||
this.applyFollowOptions("trail", units)
|
||||
);
|
||||
contextActionSet.addContextAction(this, "echelon-lh", "Echelon (LH)", "Follow unit in echelon left formation", olButtonsContextEchelonLh, () =>
|
||||
contextActionSet.addContextAction(this, "echelon-lh", "Echelon (LH)", "Follow unit in echelon left formation", olButtonsContextEchelonLh, null, () =>
|
||||
this.applyFollowOptions("echelon-lh", units)
|
||||
);
|
||||
contextActionSet.addContextAction(this, "echelon-rh", "Echelon (RH)", "Follow unit in echelon right formation", olButtonsContextEchelonRh, () =>
|
||||
contextActionSet.addContextAction(this, "echelon-rh", "Echelon (RH)", "Follow unit in echelon right formation", olButtonsContextEchelonRh, null, () =>
|
||||
this.applyFollowOptions("echelon-rh", units)
|
||||
);
|
||||
contextActionSet.addContextAction(
|
||||
@ -1297,7 +1321,7 @@ export abstract class Unit extends CustomMarker {
|
||||
"Line abreast (LH)",
|
||||
"Follow unit in line abreast left formation",
|
||||
olButtonsContextLineAbreast,
|
||||
() => this.applyFollowOptions("line-abreast-lh", units)
|
||||
null, () => this.applyFollowOptions("line-abreast-lh", units)
|
||||
);
|
||||
contextActionSet.addContextAction(
|
||||
this,
|
||||
@ -1305,13 +1329,13 @@ export abstract class Unit extends CustomMarker {
|
||||
"Line abreast (RH)",
|
||||
"Follow unit in line abreast right formation",
|
||||
olButtonsContextLineAbreast,
|
||||
() => this.applyFollowOptions("line-abreast-rh", units)
|
||||
null, () => this.applyFollowOptions("line-abreast-rh", units)
|
||||
);
|
||||
contextActionSet.addContextAction(this, "front", "Front", "Fly in front of unit", olButtonsContextFront, () => this.applyFollowOptions("front", units));
|
||||
contextActionSet.addContextAction(this, "diamond", "Diamond", "Follow unit in diamond formation", olButtonsContextDiamond, () =>
|
||||
contextActionSet.addContextAction(this, "front", "Front", "Fly in front of unit", olButtonsContextFront, null, () => this.applyFollowOptions("front", units));
|
||||
contextActionSet.addContextAction(this, "diamond", "Diamond", "Follow unit in diamond formation", olButtonsContextDiamond, null, () =>
|
||||
this.applyFollowOptions("diamond", units)
|
||||
);
|
||||
contextActionSet.addContextAction(this, "custom", "Custom", "Set a custom formation position", faExclamation, () =>
|
||||
contextActionSet.addContextAction(this, "custom", "Custom", "Set a custom formation position", faExclamation, null, () =>
|
||||
this.applyFollowOptions("custom", units)
|
||||
);
|
||||
}
|
||||
@ -1349,35 +1373,73 @@ export abstract class Unit extends CustomMarker {
|
||||
}
|
||||
|
||||
/***********************************************/
|
||||
#onClick(e: any) {
|
||||
/* Exit if we were waiting for a doubleclick */
|
||||
if (this.#waitingForDoubleClick) {
|
||||
return;
|
||||
}
|
||||
#onMouseUp(e: any) {
|
||||
this.#isMouseDown = false;
|
||||
|
||||
/* We'll wait for a doubleclick */
|
||||
this.#waitingForDoubleClick = true;
|
||||
this.#doubleClickTimer = window.setTimeout(() => {
|
||||
/* Still waiting so no doubleclick; do the click action */
|
||||
if (this.#waitingForDoubleClick) {
|
||||
if (getApp().getMap().getState() === IDLE || e.originalEvent.ctrlKey) {
|
||||
if (!e.originalEvent.ctrlKey) getApp().getUnitsManager().deselectAllUnits();
|
||||
DomEvent.stop(e);
|
||||
DomEvent.preventDefault(e);
|
||||
e.originalEvent.stopImmediatePropagation();
|
||||
|
||||
this.setSelected(!this.getSelected());
|
||||
} else if (getApp().getMap().getState() === CONTEXT_ACTION) {
|
||||
getApp().getMap().executeContextAction(this, null);
|
||||
}
|
||||
}
|
||||
e.originalEvent.stopPropagation();
|
||||
|
||||
/* No longer waiting for a doubleclick */
|
||||
this.#waitingForDoubleClick = false;
|
||||
window.clearTimeout(this.#longPressTimer);
|
||||
|
||||
this.#isMouseOnCooldown = true;
|
||||
this.#mouseCooldownTimer = window.setTimeout(() => {
|
||||
this.#isMouseOnCooldown = false;
|
||||
}, 200);
|
||||
}
|
||||
|
||||
#onMouseDown(e: any) {
|
||||
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.#onShortPress(e);
|
||||
}, 200);
|
||||
|
||||
this.#longPressTimer = window.setTimeout(() => {
|
||||
/* If the mouse is still being pressed, execute the long press action */
|
||||
if (this.#isMouseDown) this.#onLongPress(e);
|
||||
}, 350);
|
||||
}
|
||||
|
||||
#onShortPress(e: LeafletMouseEvent) {
|
||||
console.log(`Short press on ${this.getUnitName()}`);
|
||||
|
||||
if (getApp().getMap().getState() === IDLE || e.originalEvent.ctrlKey) {
|
||||
if (!e.originalEvent.ctrlKey) getApp().getUnitsManager().deselectAllUnits();
|
||||
this.setSelected(!this.getSelected());
|
||||
} else if (getApp().getMap().getState() === CONTEXT_ACTION) {
|
||||
if (getApp().getMap().getContextAction()) getApp().getMap().executeContextAction(this, null);
|
||||
else {
|
||||
getApp().getUnitsManager().deselectAllUnits();
|
||||
this.setSelected(!this.getSelected());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#onLongPress(e: any) {
|
||||
console.log(`Long press on ${this.getUnitName()}`);
|
||||
|
||||
if (e.originalEvent.button === 2) {
|
||||
document.dispatchEvent(new CustomEvent("showUnitContextMenu", { detail: e }));
|
||||
}
|
||||
}
|
||||
|
||||
#onDoubleClick(e: any) {
|
||||
/* Let single clicks work again */
|
||||
this.#waitingForDoubleClick = false;
|
||||
clearTimeout(this.#doubleClickTimer);
|
||||
console.log(`Double click on ${this.getUnitName()}`);
|
||||
|
||||
window.clearTimeout(this.#shortPressTimer);
|
||||
window.clearTimeout(this.#longPressTimer);
|
||||
|
||||
/* Select all matching units in the viewport */
|
||||
const unitsManager = getApp().getUnitsManager();
|
||||
@ -1790,6 +1852,7 @@ export abstract class AirUnit extends Unit {
|
||||
"Refuel at tanker",
|
||||
"Refuel units at the nearest AAR Tanker. If no tanker is available the unit will RTB",
|
||||
olButtonsContextRefuel,
|
||||
null,
|
||||
(units: Unit[]) => {
|
||||
getApp().getUnitsManager().refuel(units);
|
||||
},
|
||||
@ -1801,19 +1864,21 @@ export abstract class AirUnit extends Unit {
|
||||
"Center map",
|
||||
"Center the map on the unit and follow it",
|
||||
faMapLocation,
|
||||
null,
|
||||
(units: Unit[]) => {
|
||||
getApp().getMap().centerOnUnit(units[0]);
|
||||
},
|
||||
{ executeImmediately: true }
|
||||
);
|
||||
|
||||
/* Context actions with a target unit */
|
||||
/* Context actions that require a target unit */
|
||||
contextActionSet.addContextAction(
|
||||
this,
|
||||
"attack",
|
||||
"Attack unit",
|
||||
"Click on a unit to attack it using A/A or A/G weapons",
|
||||
olButtonsContextAttack,
|
||||
"unit",
|
||||
(units: Unit[], targetUnit: Unit | null, _) => {
|
||||
if (targetUnit) getApp().getUnitsManager().attackUnit(targetUnit.ID, units);
|
||||
}
|
||||
@ -1824,18 +1889,20 @@ export abstract class AirUnit extends Unit {
|
||||
"Follow unit",
|
||||
"Click on a unit to follow it in formation",
|
||||
olButtonsContextFollow,
|
||||
"unit",
|
||||
(units: Unit[], targetUnit: Unit | null, _) => {
|
||||
if (targetUnit) targetUnit.showFollowOptions(units);
|
||||
}
|
||||
);
|
||||
|
||||
/* Context actions with a target position */
|
||||
/* Context actions that require a target position */
|
||||
contextActionSet.addContextAction(
|
||||
this,
|
||||
"bomb",
|
||||
"Precision bomb location",
|
||||
"Click on a point to execute a precision bombing attack",
|
||||
faLocationCrosshairs,
|
||||
"position",
|
||||
(units: Unit[], _, targetPosition: LatLng | null) => {
|
||||
if (targetPosition) getApp().getUnitsManager().bombPoint(targetPosition, units);
|
||||
}
|
||||
@ -1846,6 +1913,7 @@ export abstract class AirUnit extends Unit {
|
||||
"Carpet bomb location",
|
||||
"Click on a point to execute a carpet bombing attack",
|
||||
faXmarksLines,
|
||||
"position",
|
||||
(units: Unit[], _, targetPosition: LatLng | null) => {
|
||||
if (targetPosition) getApp().getUnitsManager().carpetBomb(targetPosition, units);
|
||||
}
|
||||
@ -1892,6 +1960,7 @@ export class Helicopter extends AirUnit {
|
||||
"Land at location",
|
||||
"Click on a point to land there",
|
||||
olButtonsContextLandAtPoint,
|
||||
"position",
|
||||
(units: Unit[], _, targetPosition: LatLng | null) => {
|
||||
if (targetPosition) getApp().getUnitsManager().landAtPoint(targetPosition, units);
|
||||
}
|
||||
@ -1920,7 +1989,7 @@ export class GroundUnit extends Unit {
|
||||
showHealth: true,
|
||||
showHotgroup: belongsToCommandedCoalition,
|
||||
showUnitIcon: belongsToCommandedCoalition || this.getDetectionMethods().some((value) => [VISUAL, OPTIC, RADAR, IRST, DLINK].includes(value)),
|
||||
showShortLabel: this.getDatabaseEntry()?.type === "SAM Site",
|
||||
showShortLabel: this.getBlueprint()?.type === "SAM Site",
|
||||
showFuel: false,
|
||||
showAmmo: false,
|
||||
showSummary: false,
|
||||
@ -1939,6 +2008,7 @@ export class GroundUnit extends Unit {
|
||||
"Group ground units",
|
||||
"Create a group of ground units",
|
||||
faPeopleGroup,
|
||||
null,
|
||||
(units: Unit[], _1, _2) => {
|
||||
getApp().getUnitsManager().createGroup(units);
|
||||
},
|
||||
@ -1950,6 +2020,7 @@ export class GroundUnit extends Unit {
|
||||
"Center map",
|
||||
"Center the map on the unit and follow it",
|
||||
faMapLocation,
|
||||
null,
|
||||
(units: Unit[]) => {
|
||||
getApp().getMap().centerOnUnit(units[0]);
|
||||
},
|
||||
@ -1963,6 +2034,7 @@ export class GroundUnit extends Unit {
|
||||
"Scenic AAA",
|
||||
"Shoot AAA in the air without aiming at any target, when an enemy unit gets close enough. WARNING: works correctly only on neutral units, blue or red units will aim",
|
||||
olButtonsContextScenicAaa,
|
||||
null,
|
||||
(units: Unit[]) => {
|
||||
getApp().getUnitsManager().scenicAAA(units);
|
||||
},
|
||||
@ -1974,6 +2046,7 @@ export class GroundUnit extends Unit {
|
||||
"Dynamic accuracy AAA",
|
||||
"Shoot AAA towards the closest enemy unit, but don't aim precisely. WARNING: works correctly only on neutral units, blue or red units will aim",
|
||||
olButtonsContextMissOnPurpose,
|
||||
null,
|
||||
(units: Unit[]) => {
|
||||
getApp().getUnitsManager().missOnPurpose(units);
|
||||
},
|
||||
@ -1988,6 +2061,7 @@ export class GroundUnit extends Unit {
|
||||
"Attack unit",
|
||||
"Click on a unit to attack it",
|
||||
olButtonsContextAttack,
|
||||
"unit",
|
||||
(units: Unit[], targetUnit: Unit | null, _) => {
|
||||
if (targetUnit) getApp().getUnitsManager().attackUnit(targetUnit.ID, units);
|
||||
}
|
||||
@ -2001,6 +2075,7 @@ export class GroundUnit extends Unit {
|
||||
"Fire at area",
|
||||
"Click on a point to precisely fire at it (if possible)",
|
||||
faLocationCrosshairs,
|
||||
"position",
|
||||
(units: Unit[], _, targetPosition: LatLng | null) => {
|
||||
if (targetPosition) getApp().getUnitsManager().fireAtArea(targetPosition, units);
|
||||
}
|
||||
@ -2011,6 +2086,7 @@ export class GroundUnit extends Unit {
|
||||
"Simulate fire fight",
|
||||
"Simulate a fire fight by shooting randomly in a certain large area. WARNING: works correctly only on neutral units, blue or red units will aim",
|
||||
olButtonsContextSimulateFireFight,
|
||||
"position",
|
||||
(units: Unit[], _, targetPosition: LatLng | null) => {
|
||||
if (targetPosition) getApp().getUnitsManager().simulateFireFight(targetPosition, units);
|
||||
}
|
||||
@ -2028,7 +2104,7 @@ export class GroundUnit extends Unit {
|
||||
}
|
||||
|
||||
/* When a unit is a leader of a group, the map is zoomed out and grouping when zoomed out is enabled, check if the unit should be shown as a specific group. This is used to show a SAM battery instead of the group leader */
|
||||
getDatabaseEntry() {
|
||||
getBlueprint() {
|
||||
let unitWhenGrouped: string | undefined | null = null;
|
||||
if (
|
||||
!this.getSelected() &&
|
||||
@ -2038,10 +2114,10 @@ export class GroundUnit extends Unit {
|
||||
) {
|
||||
unitWhenGrouped = this.getDatabase()?.getByName(this.getName())?.unitWhenGrouped ?? null;
|
||||
let member = this.getGroupMembers().reduce((prev: Unit | null, unit: Unit, index: number) => {
|
||||
if (unit.getDatabaseEntry()?.unitWhenGrouped != undefined) return unit;
|
||||
if (unit.getBlueprint()?.unitWhenGrouped != undefined) return unit;
|
||||
return prev;
|
||||
}, null);
|
||||
unitWhenGrouped = member !== null ? member?.getDatabaseEntry()?.unitWhenGrouped : unitWhenGrouped;
|
||||
unitWhenGrouped = member !== null ? member?.getBlueprint()?.unitWhenGrouped : unitWhenGrouped;
|
||||
}
|
||||
if (unitWhenGrouped) return this.getDatabase()?.getByName(unitWhenGrouped) ?? this.getDatabase()?.getUnkownUnit(this.getName());
|
||||
else return this.getDatabase()?.getByName(this.getName()) ?? this.getDatabase()?.getUnkownUnit(this.getName());
|
||||
@ -2099,6 +2175,7 @@ export class NavyUnit extends Unit {
|
||||
"Group navy units",
|
||||
"Create a group of navy units",
|
||||
faQuestionCircle,
|
||||
null,
|
||||
(units: Unit[], _1, _2) => {
|
||||
getApp().getUnitsManager().createGroup(units);
|
||||
},
|
||||
@ -2110,6 +2187,7 @@ export class NavyUnit extends Unit {
|
||||
"Center map",
|
||||
"Center the map on the unit and follow it",
|
||||
faMapLocation,
|
||||
null,
|
||||
(units: Unit[]) => {
|
||||
getApp().getMap().centerOnUnit(units[0]);
|
||||
},
|
||||
@ -2123,6 +2201,7 @@ export class NavyUnit extends Unit {
|
||||
"Attack unit",
|
||||
"Click on a unit to attack it",
|
||||
faQuestionCircle,
|
||||
"unit",
|
||||
(units: Unit[], targetUnit: Unit | null, _) => {
|
||||
if (targetUnit) getApp().getUnitsManager().attackUnit(targetUnit.ID, units);
|
||||
}
|
||||
@ -2135,6 +2214,7 @@ export class NavyUnit extends Unit {
|
||||
"Fire at area",
|
||||
"Click on a point to precisely fire at it (if possible)",
|
||||
faQuestionCircle,
|
||||
"position",
|
||||
(units: Unit[], _, targetPosition: LatLng | null) => {
|
||||
if (targetPosition) getApp().getUnitsManager().fireAtArea(targetPosition, units);
|
||||
}
|
||||
|
||||
@ -34,6 +34,7 @@ import { Group } from "./group";
|
||||
import { UnitDataFileExport } from "./importexport/unitdatafileexport";
|
||||
import { UnitDataFileImport } from "./importexport/unitdatafileimport";
|
||||
import { CoalitionCircle } from "../map/coalitionarea/coalitioncircle";
|
||||
import { ContextActionSet } from "./contextactionset";
|
||||
|
||||
/** 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
|
||||
@ -1731,7 +1732,6 @@ export class UnitsManager {
|
||||
|
||||
#onUnitSelection(unit: Unit) {
|
||||
if (this.getSelectedUnits().length > 0) {
|
||||
getApp().getMap().setState(CONTEXT_ACTION);
|
||||
/* Disable the firing of the selection event for a certain amount of time. This avoids firing many events if many units are selected */
|
||||
if (!this.#selectionEventDisabled) {
|
||||
window.setTimeout(() => {
|
||||
@ -1740,6 +1740,14 @@ export class UnitsManager {
|
||||
detail: this.getSelectedUnits(),
|
||||
})
|
||||
);
|
||||
|
||||
let newContextActionSet = new ContextActionSet();
|
||||
this.getSelectedUnits().forEach((unit) => unit.appendContextActions(newContextActionSet));
|
||||
getApp().getMap().setState(CONTEXT_ACTION, {
|
||||
contextAction: null,
|
||||
defaultContextAction: newContextActionSet.getDefaultContextAction(),
|
||||
});
|
||||
|
||||
this.#selectionEventDisabled = false;
|
||||
this.#showNumberOfSelectedProtectedUnits();
|
||||
}, 100);
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user