Implemented context menu and multiple control tweaks

This commit is contained in:
Davide Passoni 2024-08-08 15:32:59 +02:00
parent 7cf77a63be
commit 6fdfb194a6
21 changed files with 592 additions and 275 deletions

1
.gitignore vendored
View File

@ -43,3 +43,4 @@ manager/manager.log
/frontend/server/public/assets
/frontend/server/public/vite
/frontend/server/build
/frontend/react/.vite

View File

@ -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 {

View File

@ -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 />

View File

@ -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%;
}

View File

@ -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 }));
}
}
}
}

View File

@ -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}`);
}

View File

@ -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;

View File

@ -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 ?? (() => {})}

View 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>
</>
)}
</>
);
}

View File

@ -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>}
</>
);
})}

View File

@ -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>
);
})}

View File

@ -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

View File

@ -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(() => {

View File

@ -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}

View File

@ -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]
`}

View File

@ -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()
});
}
}

View File

@ -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>

View File

@ -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);
}

View File

@ -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;
}
}

View File

@ -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);
}

View File

@ -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);