mirror of
https://github.com/Pax1601/DCSOlympus.git
synced 2025-10-29 16:56:34 +00:00
feat: Improved formation menu
This commit is contained in:
@@ -1,19 +1,19 @@
|
||||
import { useCallback, useEffect, useState } from "react";
|
||||
|
||||
export const useDrag = (props: { ref, initialPosition, count}) => {
|
||||
const [finalPosition, setFinalPosition] = useState({ x: props.initialPosition.x, y: props.initialPosition.y });
|
||||
const [isDragging, setIsDragging] = useState(false);
|
||||
const [position, setPosition] = useState({ x: props.initialPosition.x, y: props.initialPosition.y });
|
||||
const [dragging, isDragging] = useState(false);
|
||||
const [count, setCount] = useState(0)
|
||||
|
||||
if (count !== props.count) {
|
||||
setCount(props.count)
|
||||
setFinalPosition({ x: props.initialPosition.x, y: props.initialPosition.y })
|
||||
setPosition({ x: props.initialPosition.x, y: props.initialPosition.y })
|
||||
}
|
||||
|
||||
const handleMouseUp = (evt) => {
|
||||
evt.preventDefault();
|
||||
|
||||
setIsDragging(false);
|
||||
isDragging(false);
|
||||
};
|
||||
|
||||
const handleMouseDown = (evt) => {
|
||||
@@ -25,14 +25,14 @@ export const useDrag = (props: { ref, initialPosition, count}) => {
|
||||
return;
|
||||
}
|
||||
|
||||
setIsDragging(true);
|
||||
isDragging(true);
|
||||
};
|
||||
|
||||
const handleMouseMove = useCallback(
|
||||
(evt) => {
|
||||
const { current: draggableElement } = props.ref;
|
||||
|
||||
if (!isDragging || !draggableElement) return;
|
||||
if (!dragging || !draggableElement) return;
|
||||
|
||||
evt.preventDefault();
|
||||
|
||||
@@ -43,12 +43,12 @@ export const useDrag = (props: { ref, initialPosition, count}) => {
|
||||
const [mouseX, mouseY] = [evt.clientX, evt.clientY];
|
||||
const [parentTop, parentLeft, parentWidth, parentHeight] = [parentRect.top, parentRect.left, parentRect.width, parentRect.height];
|
||||
|
||||
setFinalPosition({
|
||||
x: Math.round(Math.max(width / 2, Math.min(mouseX - parentLeft, parentWidth - width / 2)) / 10) * 10,
|
||||
y: Math.round(Math.max(height / 2, Math.min(mouseY - parentTop, parentHeight - height / 2)) / 10) * 10,
|
||||
setPosition({
|
||||
x: Math.max(width / 2, Math.min(mouseX - parentLeft, parentWidth - width / 2)),
|
||||
y: Math.max(height / 2, Math.min(mouseY - parentTop, parentHeight - height / 2)),
|
||||
});
|
||||
},
|
||||
[isDragging, props.ref]
|
||||
[dragging, props.ref]
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
@@ -61,8 +61,13 @@ export const useDrag = (props: { ref, initialPosition, count}) => {
|
||||
};
|
||||
}, [handleMouseMove]);
|
||||
|
||||
const forcePosition = (x, y) => {
|
||||
setPosition({x, y});
|
||||
}
|
||||
|
||||
return {
|
||||
position: finalPosition,
|
||||
handleMouseDown
|
||||
position: position,
|
||||
handleMouseDown,
|
||||
forcePosition
|
||||
};
|
||||
};
|
||||
|
||||
@@ -21,7 +21,7 @@ import { MissionData, UnitData } from "../../interfaces";
|
||||
export function ImportExportModal(props: { open: boolean }) {
|
||||
const [appState, setAppState] = useState(OlympusState.NOT_INITIALIZED);
|
||||
const [appSubState, setAppSubState] = useState(NO_SUBSTATE);
|
||||
const [units, setUnits] = useState({} as { [ID: number]: Unit });
|
||||
const [units, setUnits] = useState([] as Unit[]);
|
||||
const [missionData, setMissionData] = useState({} as MissionData);
|
||||
const [importData, setImportData] = useState({} as { [key: string]: UnitData[] });
|
||||
|
||||
@@ -86,7 +86,7 @@ export function ImportExportModal(props: { open: boolean }) {
|
||||
}
|
||||
}, [appState, appSubState]);
|
||||
|
||||
const selectableUnits = Object.values(units).filter((unit) => {
|
||||
const selectableUnits = units.filter((unit) => {
|
||||
return (
|
||||
unit.getAlive() &&
|
||||
!unit.getHuman() &&
|
||||
|
||||
62
frontend/react/src/ui/panels/components/draggable.tsx
Normal file
62
frontend/react/src/ui/panels/components/draggable.tsx
Normal file
@@ -0,0 +1,62 @@
|
||||
import React, { useCallback, useEffect, useRef, useState } from "react";
|
||||
|
||||
export function Draggable(props: {
|
||||
position: { x: number; y: number };
|
||||
children: JSX.Element | JSX.Element[];
|
||||
disabled: boolean;
|
||||
onPositionChange: (position: { x: number; y: number }) => void;
|
||||
}) {
|
||||
const [dragging, setDragging] = useState(false);
|
||||
const [refPosition, setRefPosition] = useState({ x: 0, y: 0 });
|
||||
|
||||
const handleMouseMove = useCallback(
|
||||
(e) => {
|
||||
if (dragging) {
|
||||
e.stopPropagation();
|
||||
e.preventDefault();
|
||||
setRefPosition({ x: e.clientX, y: e.clientY });
|
||||
if (!props.disabled) props.onPositionChange({ x: props.position.x + e.clientX - refPosition.x, y: props.position.y + e.clientY - refPosition.y });
|
||||
}
|
||||
},
|
||||
[dragging, refPosition]
|
||||
);
|
||||
|
||||
const handleMouseUp = useCallback(
|
||||
(e) => {
|
||||
if (dragging) {
|
||||
e.stopPropagation();
|
||||
e.preventDefault();
|
||||
setDragging(false);
|
||||
}
|
||||
},
|
||||
[dragging, refPosition]
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
document.addEventListener("mousemove", handleMouseMove);
|
||||
document.addEventListener("mouseup", handleMouseUp);
|
||||
|
||||
return () => {
|
||||
document.removeEventListener("mousemove", handleMouseMove);
|
||||
document.removeEventListener("mouseup", handleMouseUp);
|
||||
};
|
||||
}, [handleMouseMove, handleMouseUp]);
|
||||
|
||||
return (
|
||||
<div
|
||||
className={`absolute translate-x-[-50%] translate-y-[-50%]`}
|
||||
style={{
|
||||
top: props.position.y,
|
||||
left: props.position.x,
|
||||
}}
|
||||
onMouseDown={(e) => {
|
||||
e.stopPropagation();
|
||||
e.preventDefault();
|
||||
setRefPosition({ x: e.clientX, y: e.clientY });
|
||||
setDragging(true);
|
||||
}}
|
||||
>
|
||||
{props.children}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,32 @@
|
||||
import React, { useCallback, useEffect, useRef, useState } from "react";
|
||||
import { Draggable } from "./draggable";
|
||||
import { Unit } from "../../../unit/unit";
|
||||
|
||||
export function DraggableSilhouette(props: {
|
||||
position: { x: number; y: number };
|
||||
unit: Unit;
|
||||
zoom: number;
|
||||
scale: number;
|
||||
disabled: boolean;
|
||||
angle: number;
|
||||
onPositionChange: (position: { x: number; y: number }) => void;
|
||||
src?: string;
|
||||
}) {
|
||||
return (
|
||||
<Draggable position={props.position} onPositionChange={props.onPositionChange} disabled={props.disabled}>
|
||||
<img
|
||||
data-disabled = {props.disabled}
|
||||
className={`
|
||||
align-center opacity-80 invert
|
||||
data-[disabled=false]:cursor-move
|
||||
`}
|
||||
src={props.src ?? `./images/units/${props.unit?.getBlueprint()?.filename}`}
|
||||
style={{
|
||||
maxWidth: `${Math.round((props.scale * (props.disabled ? 20 : (props.unit?.getBlueprint()?.length ?? 50))) / Math.min(3, props.disabled? 1: props.zoom))}px`,
|
||||
minWidth: `${Math.round((props.scale * (props.disabled ? 20 : (props.unit?.getBlueprint()?.length ?? 50))) / Math.min(3, props.disabled? 1: props.zoom))}px`,
|
||||
rotate: `${props.disabled? props.angle: 90}deg`,
|
||||
}}
|
||||
></img>
|
||||
</Draggable>
|
||||
);
|
||||
}
|
||||
259
frontend/react/src/ui/panels/components/formationcanvas.tsx
Normal file
259
frontend/react/src/ui/panels/components/formationcanvas.tsx
Normal file
@@ -0,0 +1,259 @@
|
||||
import React, { useCallback, useEffect, useRef, useState } from "react";
|
||||
import { Unit } from "../../../unit/unit";
|
||||
import { DraggableSilhouette } from "./draggablesilhouette";
|
||||
import { FaCompressArrowsAlt, FaExclamationTriangle, FaExpand, FaExpandArrowsAlt } from "react-icons/fa";
|
||||
|
||||
const FT_TO_PX = 1;
|
||||
|
||||
export function FormationCanvas(props: {
|
||||
units: Unit[];
|
||||
unitPositions: { x: number; y: number }[];
|
||||
setUnitPositions: (positions: { x: number; y: number }[]) => void;
|
||||
}) {
|
||||
const [dragging, setDragging] = useState(false);
|
||||
const [refPosition, setRefPosition] = useState({ x: 0, y: 0 });
|
||||
const [dragDelta, setDragDelta] = useState({ x: 0, y: 0 });
|
||||
const [zoom, setZoom] = useState(1);
|
||||
|
||||
/* Init references and hooks */
|
||||
const containerRef = useRef(null);
|
||||
|
||||
let containerCenter = { x: 0, y: 0 };
|
||||
let containerSize = { width: 0, height: 0 };
|
||||
if (containerRef.current) {
|
||||
const containerDiv = containerRef.current as HTMLDivElement;
|
||||
containerCenter = {
|
||||
x: containerDiv.clientWidth / 2,
|
||||
y: containerDiv.clientHeight / 3,
|
||||
};
|
||||
containerSize = { width: containerDiv.clientWidth, height: containerDiv.clientHeight };
|
||||
}
|
||||
|
||||
/* Handle mouse movement, for dragging of the scene */
|
||||
const handleMouseMove = useCallback(
|
||||
(e) => {
|
||||
if (dragging) {
|
||||
e.stopPropagation();
|
||||
e.preventDefault();
|
||||
setDragDelta({
|
||||
x: dragDelta.x + e.clientX - refPosition.x,
|
||||
y: dragDelta.y + e.clientY - refPosition.y,
|
||||
});
|
||||
setRefPosition({ x: e.clientX, y: e.clientY });
|
||||
}
|
||||
},
|
||||
[dragging, refPosition]
|
||||
);
|
||||
|
||||
/* Handle mouse up, to stop dragging the scene */
|
||||
const handleMouseUp = useCallback(
|
||||
(e) => {
|
||||
if (dragging) {
|
||||
e.stopPropagation();
|
||||
e.preventDefault();
|
||||
setDragging(false);
|
||||
}
|
||||
},
|
||||
[dragging, refPosition]
|
||||
);
|
||||
|
||||
/* Register the dragging handlers */
|
||||
useEffect(() => {
|
||||
document.addEventListener("mousemove", handleMouseMove);
|
||||
document.addEventListener("mouseup", handleMouseUp);
|
||||
|
||||
return () => {
|
||||
document.removeEventListener("mousemove", handleMouseMove);
|
||||
document.removeEventListener("mouseup", handleMouseUp);
|
||||
};
|
||||
}, [handleMouseMove, handleMouseUp]);
|
||||
|
||||
let referenceDistance = 200 * zoom;
|
||||
if (referenceDistance < 250) referenceDistance = 100;
|
||||
else if (referenceDistance < 500) referenceDistance = 250;
|
||||
else if (referenceDistance < 1000) referenceDistance = 500;
|
||||
else if (referenceDistance < 3000) referenceDistance = 1000;
|
||||
else if (referenceDistance < 5280 * 2) referenceDistance = 5280;
|
||||
else referenceDistance = 5280 * 2;
|
||||
const referenceWidth = referenceDistance / zoom;
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className="flex w-fit gap-1 p-1">
|
||||
<button
|
||||
type="button"
|
||||
onDoubleClick={(e) => {
|
||||
e.stopPropagation();
|
||||
}}
|
||||
onClick={() => {
|
||||
props.setUnitPositions(
|
||||
props.unitPositions.map((position) => {
|
||||
return {
|
||||
x: position.x * 1.1,
|
||||
y: position.y * 1.1,
|
||||
};
|
||||
})
|
||||
);
|
||||
}}
|
||||
className={`
|
||||
rounded-lg p-2 text-md flex content-center justify-center gap-2
|
||||
bg-gray-600 font-medium text-white
|
||||
hover:bg-gray-700
|
||||
`}
|
||||
>
|
||||
<FaExpandArrowsAlt className="my-auto" /> <div> Loosen </div>
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
onDoubleClick={(e) => {
|
||||
e.stopPropagation();
|
||||
}}
|
||||
onClick={() => {
|
||||
props.setUnitPositions(
|
||||
props.unitPositions.map((position) => {
|
||||
return {
|
||||
x: position.x * 0.9,
|
||||
y: position.y * 0.9,
|
||||
};
|
||||
})
|
||||
);
|
||||
}}
|
||||
className={`
|
||||
rounded-lg p-2 text-md flex content-center justify-center gap-2
|
||||
bg-gray-600 font-medium text-white
|
||||
hover:bg-gray-700
|
||||
`}
|
||||
>
|
||||
<FaCompressArrowsAlt className="my-auto" /> <div>Tighten</div>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div
|
||||
data-dragging={dragging}
|
||||
className={`
|
||||
relative h-full w-full cursor-grab overflow-hidden rounded-md
|
||||
border-[1px] border-white/20 bg-white/10
|
||||
data-[dragging=true]:cursor-grabbing
|
||||
`}
|
||||
onWheel={(e) => {
|
||||
if (e.deltaY > 0) setZoom(Math.max(Math.min(zoom * 1.1, 100), 0.8));
|
||||
else setZoom(Math.max(Math.min(zoom * 0.9, 100), 0.8));
|
||||
}}
|
||||
onMouseDown={(e) => {
|
||||
e.stopPropagation();
|
||||
e.preventDefault();
|
||||
setRefPosition({ x: e.clientX, y: e.clientY });
|
||||
setDragging(true);
|
||||
}}
|
||||
onDoubleClick={() => {
|
||||
setDragDelta({ x: 0, y: 0 });
|
||||
setZoom(1);
|
||||
}}
|
||||
>
|
||||
<div className={`h-full w-full`} ref={containerRef}>
|
||||
{props.units.map((unit, idx) => {
|
||||
let unitPosition = props.unitPositions[idx]
|
||||
? {
|
||||
x:
|
||||
props.unitPositions[0].x +
|
||||
(((props.unitPositions[idx].x - props.unitPositions[0].x) * 1) / zoom) * FT_TO_PX +
|
||||
dragDelta.x +
|
||||
containerCenter.x,
|
||||
y:
|
||||
props.unitPositions[0].y +
|
||||
(((props.unitPositions[idx].y - props.unitPositions[0].y) * 1) / zoom) * FT_TO_PX +
|
||||
dragDelta.y +
|
||||
containerCenter.y,
|
||||
}
|
||||
: { x: 0, y: 0 };
|
||||
|
||||
let disabled = false;
|
||||
let overflowX = null as null | string;
|
||||
let overflowY = null as null | string;
|
||||
if (unitPosition.x < 0) {
|
||||
disabled = true;
|
||||
unitPosition.x = 10;
|
||||
overflowX = "left";
|
||||
} else if (unitPosition.x > containerSize.width) {
|
||||
disabled = true;
|
||||
unitPosition.x = containerSize.width - 10;
|
||||
overflowX = "right";
|
||||
}
|
||||
|
||||
if (unitPosition.y < 0) {
|
||||
disabled = true;
|
||||
unitPosition.y = 10;
|
||||
overflowY = "top";
|
||||
} else if (unitPosition.y > containerSize.height) {
|
||||
disabled = true;
|
||||
unitPosition.y = containerSize.height - 10;
|
||||
overflowY = "bottom";
|
||||
}
|
||||
|
||||
let angle = 0;
|
||||
if (overflowX === "right") {
|
||||
if (overflowY === "top") angle = 45;
|
||||
else if (overflowY === "bottom") angle = 135;
|
||||
else angle = 90;
|
||||
} else if (overflowX === "left") {
|
||||
if (overflowY === "top") angle = 360 - 45;
|
||||
else if (overflowY === "bottom") angle = 360 - 135;
|
||||
else angle = 360 - 90;
|
||||
} else {
|
||||
if (overflowY === "top") angle = 0;
|
||||
else if (overflowY === "bottom") angle = 180;
|
||||
else angle = 0;
|
||||
}
|
||||
|
||||
return (
|
||||
<DraggableSilhouette
|
||||
key={idx}
|
||||
zoom={zoom}
|
||||
position={unitPosition}
|
||||
unit={unit}
|
||||
scale={FT_TO_PX}
|
||||
disabled={disabled}
|
||||
onPositionChange={({ x, y }) => {
|
||||
props.unitPositions[idx] = {
|
||||
x: ((x - props.unitPositions[0].x - dragDelta.x - containerCenter.x) * zoom) / FT_TO_PX - props.unitPositions[0].x,
|
||||
y: ((y - props.unitPositions[0].y - dragDelta.y - containerCenter.y) * zoom) / FT_TO_PX - props.unitPositions[0].y,
|
||||
};
|
||||
props.setUnitPositions([...props.unitPositions]);
|
||||
}}
|
||||
src={disabled ? `./images/others/caret.svg` : undefined}
|
||||
angle={angle}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
{zoom > 3 && (
|
||||
<div className="absolute bottom-2 left-2 flex gap-2">
|
||||
<FaExclamationTriangle className={`text-xl text-yellow-400`} />
|
||||
<div className="text-white">Silhouettes not to scale!</div>
|
||||
</div>
|
||||
)}
|
||||
<div className="absolute left-0 top-2 m-[-0.75rem] h-0">
|
||||
<div
|
||||
className={`
|
||||
relative left-6 top-4 h-4 border-2 border-white
|
||||
border-t-transparent text-center text-white
|
||||
`}
|
||||
style={{
|
||||
width: `${referenceWidth}px`,
|
||||
}}
|
||||
>
|
||||
{referenceDistance === 5280 && <div className="translate-y-[-8px]">1 NM</div>}
|
||||
{referenceDistance === 5280 * 2 && (
|
||||
<div
|
||||
className={`translate-y-[-8px]`}
|
||||
>
|
||||
2 NM
|
||||
</div>
|
||||
)}
|
||||
{referenceDistance < 5280 && <div className="translate-y-[-8px]">{referenceDistance} ft</div>}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -2,13 +2,12 @@ import React, { useEffect, useState } from "react";
|
||||
import { OlLocation } from "../components/ollocation";
|
||||
import { LatLng } from "leaflet";
|
||||
import { FaBullseye, FaChevronDown, FaChevronUp, FaJetFighter, FaMountain } from "react-icons/fa6";
|
||||
import { BullseyesDataChangedEvent, MouseMovedEvent, SelectedUnitsChangedEvent } from "../../events";
|
||||
import { BullseyesDataChangedEvent, MouseMovedEvent, SelectedUnitsChangedEvent, SelectionClearedEvent } from "../../events";
|
||||
import { computeBearingRangeString, mToFt } from "../../other/utils";
|
||||
import { Bullseye } from "../../mission/bullseye";
|
||||
import { Unit } from "../../unit/unit";
|
||||
|
||||
export function CoordinatesPanel(props: {}) {
|
||||
const [open, setOpen] = useState(false);
|
||||
const [latlng, setLatlng] = useState(new LatLng(0, 0));
|
||||
const [elevation, setElevation] = useState(0);
|
||||
const [bullseyes, setBullseyes] = useState(null as null | { [name: string]: Bullseye });
|
||||
@@ -22,6 +21,7 @@ export function CoordinatesPanel(props: {}) {
|
||||
|
||||
BullseyesDataChangedEvent.on((bullseyes) => setBullseyes(bullseyes));
|
||||
SelectedUnitsChangedEvent.on((selectedUnits) => setSelectedUnits(selectedUnits));
|
||||
SelectionClearedEvent.on(() => setSelectedUnits([]))
|
||||
}, []);
|
||||
|
||||
return (
|
||||
@@ -33,59 +33,53 @@ export function CoordinatesPanel(props: {}) {
|
||||
dark:bg-olympus-800/90 dark:text-gray-200
|
||||
`}
|
||||
>
|
||||
{" "}
|
||||
{open && (
|
||||
<>
|
||||
{bullseyes && (
|
||||
<div className="flex w-full items-center justify-start">
|
||||
<div
|
||||
{bullseyes && (
|
||||
<div className="flex w-full items-center justify-start">
|
||||
<div
|
||||
className={`
|
||||
mr-[11px] flex min-w-64 max-w-64 items-center justify-between
|
||||
gap-2
|
||||
`}
|
||||
>
|
||||
<div className="flex justify-start gap-2">
|
||||
<span
|
||||
className={`
|
||||
flex min-w-64 max-w-64 items-center justify-between gap-2
|
||||
rounded-sm bg-blue-500 px-1 py-1 text-center font-bold
|
||||
text-olympus-700
|
||||
`}
|
||||
>
|
||||
<div className="flex justify-start gap-2">
|
||||
<span
|
||||
className={`
|
||||
rounded-sm bg-blue-500 px-1 py-1 text-center font-bold
|
||||
text-olympus-700
|
||||
`}
|
||||
>
|
||||
<FaBullseye />
|
||||
</span>{" "}
|
||||
{computeBearingRangeString(bullseyes[2].getLatLng(), latlng)}
|
||||
</div>
|
||||
<div className="flex w-[50%] justify-start gap-2">
|
||||
<span
|
||||
className={`
|
||||
rounded-sm bg-red-500 px-1 py-1 text-center font-bold
|
||||
text-olympus-700
|
||||
`}
|
||||
>
|
||||
<FaBullseye />
|
||||
</span>
|
||||
{computeBearingRangeString(bullseyes[1].getLatLng(), latlng)}
|
||||
</div>
|
||||
</div>
|
||||
{selectedUnits.length == 1 && (
|
||||
<div className="flex justify-start gap-2">
|
||||
<span
|
||||
className={`
|
||||
rounded-sm bg-white px-1 py-1 text-center font-bold
|
||||
text-olympus-700
|
||||
`}
|
||||
>
|
||||
<FaJetFighter />
|
||||
</span>
|
||||
<div>
|
||||
{" "}
|
||||
{computeBearingRangeString(selectedUnits[0].getPosition(), latlng)}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
<FaBullseye />
|
||||
</span>{" "}
|
||||
{computeBearingRangeString(bullseyes[2].getLatLng(), latlng)}
|
||||
</div>
|
||||
<div className="flex w-[50%] justify-start gap-2">
|
||||
<span
|
||||
className={`
|
||||
rounded-sm bg-red-500 px-1 py-1 text-center font-bold
|
||||
text-olympus-700
|
||||
`}
|
||||
>
|
||||
<FaBullseye />
|
||||
</span>
|
||||
{computeBearingRangeString(bullseyes[1].getLatLng(), latlng)}
|
||||
</div>
|
||||
</div>
|
||||
{selectedUnits.length == 1 && (
|
||||
<div className="flex justify-start gap-2">
|
||||
<span
|
||||
className={`
|
||||
rounded-sm bg-white px-1 py-1 text-center font-bold
|
||||
text-olympus-700
|
||||
`}
|
||||
>
|
||||
<FaJetFighter />
|
||||
</span>
|
||||
<div> {computeBearingRangeString(selectedUnits[0].getPosition(), latlng)}</div>
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="flex w-full items-center justify-between">
|
||||
<OlLocation className="!min-w-64 !max-w-64 bg-transparent !p-0" location={latlng} />
|
||||
<span
|
||||
@@ -97,11 +91,6 @@ export function CoordinatesPanel(props: {}) {
|
||||
<FaMountain />
|
||||
</span>
|
||||
<div className="min-w-12">{mToFt(elevation).toFixed()}ft</div>
|
||||
{open ? (
|
||||
<FaChevronDown className="w-10 cursor-pointer" onClick={() => setOpen(!open)} />
|
||||
) : (
|
||||
<FaChevronUp className="w-10 cursor-pointer" onClick={() => setOpen(!open)} />
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -1,106 +1,55 @@
|
||||
import React, { useEffect, useRef, useState } from "react";
|
||||
import React, { useCallback, useEffect, useRef, useState } from "react";
|
||||
import { Menu } from "./components/menu";
|
||||
import { OlDropdown, OlDropdownItem } from "../components/oldropdown";
|
||||
import { useDrag } from "../libs/useDrag";
|
||||
import { Unit } from "../../unit/unit";
|
||||
import { OlRangeSlider } from "../components/olrangeslider";
|
||||
import { FormationCreationRequestEvent } from "../../events";
|
||||
import { computeStandardFormationOffset } from "../../other/utils";
|
||||
import { formationTypes } from "../../constants/constants";
|
||||
import { FormationCanvas } from "./components/formationcanvas";
|
||||
|
||||
export function FormationMenu(props: {
|
||||
open: boolean;
|
||||
onClose: () => void;
|
||||
children?: JSX.Element | JSX.Element[];
|
||||
}) {
|
||||
const [leader, setLeader] = useState(null as Unit | null)
|
||||
const [wingmen, setWingmen] = useState(null as Unit[] | null)
|
||||
|
||||
/* The useDrag custom hook used to handle the dragging of the units requires that the number of hooks remains unchanged.
|
||||
The units array is therefore initialized to 128 units maximum. */
|
||||
let units = Array(128).fill(null) as (Unit | null)[];
|
||||
units[0] = leader;
|
||||
wingmen?.forEach((unit, idx) => {
|
||||
if (idx < units.length) units[idx + 1] = unit;
|
||||
});
|
||||
export function FormationMenu(props: { open: boolean; onClose: () => void; children?: JSX.Element | JSX.Element[] }) {
|
||||
const [leader, setLeader] = useState(null as Unit | null);
|
||||
const [wingmen, setWingmen] = useState([] as Unit[]);
|
||||
|
||||
/* Init state variables */
|
||||
const [formationType, setFormationType] = useState("echelon-lh");
|
||||
const [horizontalScale, setHorizontalScale] = useState(0);
|
||||
const [verticalScale, setVerticalScale] = useState(30);
|
||||
const [offsets, setOffsets] = useState(
|
||||
units.map((unit, idx) => {
|
||||
return computeFormationOffset(formationType, idx);
|
||||
})
|
||||
);
|
||||
const [unitPositions, setUnitPositions] = useState([] as { x: number; y: number }[]);
|
||||
|
||||
/* The count state is used to force the reset of the initial position of the silhouettes */
|
||||
// TODO it works but I don't like it, it feels like a hack
|
||||
const [count, setCount] = useState(0);
|
||||
|
||||
/* Init references and hooks */
|
||||
const containerRef = useRef(null);
|
||||
const scrollRef = useRef(null);
|
||||
const silhouetteReferences = units.map((_) => useRef(null));
|
||||
const silhouetteHandles = units.map((_, idx) => {
|
||||
/* Set the initial position of the unit to be centered in the drawing canvas, depending on the currently loaded formation */
|
||||
let offset = offsets[idx] ?? { x: 0, y: 0, z: 0 };
|
||||
let center = { x: 0, y: 0 };
|
||||
if (containerRef.current) {
|
||||
center.x = (containerRef.current as HTMLDivElement).getBoundingClientRect().width / 2;
|
||||
center.y = (containerRef.current as HTMLDivElement).getBoundingClientRect().height / 2;
|
||||
}
|
||||
return useDrag({
|
||||
ref: silhouetteReferences[idx],
|
||||
initialPosition: { x: offset.z + center.x, y: -offset.x + center.y },
|
||||
count: count,
|
||||
});
|
||||
});
|
||||
const verticalRatio = (verticalScale - 50) / 50;
|
||||
|
||||
/* Listen for the setting of a new leader and wingmen and check if the formation is too big */
|
||||
useEffect(() => {
|
||||
FormationCreationRequestEvent.on((leader, wingmen) => {
|
||||
setLeader(leader);
|
||||
setWingmen(wingmen);
|
||||
})
|
||||
})
|
||||
});
|
||||
}, []);
|
||||
|
||||
/* When the formation type is changed, reset the position to the center and the position of the silhouettes depending on the aircraft */
|
||||
useEffect(() => {
|
||||
if (scrollRef.current && containerRef.current) {
|
||||
const containerDiv = containerRef.current as HTMLDivElement;
|
||||
const scrollDiv = scrollRef.current as HTMLDivElement;
|
||||
scrollDiv.scrollTop = (containerDiv.clientHeight - scrollDiv.clientHeight) / 2 + 150;
|
||||
scrollDiv.scrollLeft = (containerDiv.clientWidth - scrollDiv.clientWidth) / 2;
|
||||
}
|
||||
|
||||
const setStandardFormation = useCallback(() => {
|
||||
/* If a standard formation is chosen, compute the positions */
|
||||
if (formationType !== "custom") {
|
||||
setOffsets(
|
||||
units.map((unit, idx) => {
|
||||
return computeFormationOffset(formationType, idx);
|
||||
setUnitPositions(
|
||||
[leader, ...wingmen].map((unit, idx) => {
|
||||
return computeStandardFormationOffset(formationType, idx);
|
||||
})
|
||||
);
|
||||
setCount(count + 1);
|
||||
}
|
||||
}, [formationType]);
|
||||
useEffect(setStandardFormation, [formationType]);
|
||||
|
||||
const horizontalRatio = 1 + (horizontalScale / 100) ** 2 * 100;
|
||||
const verticalRatio = (verticalScale - 50) / 50;
|
||||
|
||||
let referenceDistance = 200 * horizontalRatio;
|
||||
if (referenceDistance < 250) {
|
||||
referenceDistance = 100;
|
||||
} else if (referenceDistance < 500) {
|
||||
referenceDistance = 250;
|
||||
} else if (referenceDistance < 1000) {
|
||||
referenceDistance = 500;
|
||||
} else if (referenceDistance < 3000) {
|
||||
referenceDistance = 1000;
|
||||
} else if (referenceDistance < 10000) {
|
||||
referenceDistance = 5000;
|
||||
} else {
|
||||
referenceDistance = 10000;
|
||||
if (leader && unitPositions.length < [leader, ...wingmen].length) {
|
||||
/* If more units are added to the group keep the existing positions */
|
||||
setUnitPositions(
|
||||
[leader, ...wingmen].map((unit, idx) => {
|
||||
if (idx < unitPositions.length) return unitPositions[idx];
|
||||
else return computeStandardFormationOffset(formationType, idx);
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
const referenceWidth = referenceDistance / horizontalRatio;
|
||||
|
||||
return (
|
||||
<Menu title="Formation menu" open={props.open} showBackButton={false} onClose={props.onClose}>
|
||||
<div className="flex h-full flex-col gap-4 p-4">
|
||||
@@ -111,13 +60,7 @@ export function FormationMenu(props: {
|
||||
.filter((type) => type !== "custom")
|
||||
.map((optionFormationType) => {
|
||||
return (
|
||||
<OlDropdownItem
|
||||
key={optionFormationType}
|
||||
onClick={() => {
|
||||
setCount(count + 1);
|
||||
setFormationType(optionFormationType);
|
||||
}}
|
||||
>
|
||||
<OlDropdownItem key={optionFormationType} onClick={() => setFormationType(optionFormationType)}>
|
||||
{formationTypes[optionFormationType]}
|
||||
</OlDropdownItem>
|
||||
);
|
||||
@@ -126,25 +69,7 @@ export function FormationMenu(props: {
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => {
|
||||
let content = JSON.stringify(
|
||||
units
|
||||
.filter((unit) => unit !== null)
|
||||
.map((unit, idx) => {
|
||||
if (units.length > 0 && units[0] !== null) {
|
||||
const [dx, dz] = [
|
||||
-(silhouetteHandles[idx].position.y - silhouetteHandles[0].position.y),
|
||||
silhouetteHandles[idx].position.x - silhouetteHandles[0].position.x,
|
||||
];
|
||||
const distance = Math.sqrt(dx ** 2 + dz ** 2);
|
||||
const offset = {
|
||||
x: dx * horizontalRatio,
|
||||
y: distance * verticalRatio,
|
||||
z: dz * horizontalRatio,
|
||||
};
|
||||
return offset;
|
||||
}
|
||||
})
|
||||
);
|
||||
let content = JSON.stringify(unitPositions);
|
||||
var a = document.createElement("a");
|
||||
var file = new Blob([content], { type: "text/plain" });
|
||||
a.href = URL.createObjectURL(file);
|
||||
@@ -176,8 +101,7 @@ export function FormationMenu(props: {
|
||||
// @ts-ignore TODO
|
||||
var content = readerEvent.target.result;
|
||||
if (content) {
|
||||
setOffsets(JSON.parse(content.toString()));
|
||||
setCount(count + 1);
|
||||
setUnitPositions(JSON.parse(content.toString()));
|
||||
setFormationType("custom");
|
||||
}
|
||||
};
|
||||
@@ -196,24 +120,6 @@ export function FormationMenu(props: {
|
||||
Load
|
||||
</button>
|
||||
</div>
|
||||
<span className="text-white">Formation distance</span>
|
||||
<div className="flex h-fit content-center gap-4">
|
||||
<span
|
||||
className={`
|
||||
my-auto min-w-16 text-center align-middle text-sm text-white
|
||||
`}
|
||||
>
|
||||
Parade
|
||||
</span>
|
||||
<OlRangeSlider
|
||||
className="my-auto"
|
||||
value={horizontalScale}
|
||||
onChange={(ev) => {
|
||||
setHorizontalScale(Number(ev.target.value));
|
||||
}}
|
||||
/>
|
||||
<span className="my-auto min-w-16 text-center text-sm text-white">Tactical</span>
|
||||
</div>
|
||||
<span className="text-white">Vertical separation</span>
|
||||
<div className="flex h-fit content-center gap-4">
|
||||
<span className="ml-auto min-w-16 text-center text-sm text-white">Down</span>
|
||||
@@ -226,39 +132,36 @@ export function FormationMenu(props: {
|
||||
/>
|
||||
<span className="my-auto min-w-16 text-center text-sm text-white">Up</span>
|
||||
</div>
|
||||
<FormationCanvas
|
||||
units={leader ? [leader, ...wingmen] : []}
|
||||
unitPositions={unitPositions}
|
||||
setUnitPositions={(positions) => {
|
||||
setUnitPositions(positions);
|
||||
setFormationType("custom");
|
||||
}}
|
||||
/>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => {
|
||||
let center = { x: 0, y: 0 };
|
||||
|
||||
if (containerRef.current) {
|
||||
center.x = (containerRef.current as HTMLDivElement).getBoundingClientRect().width / 2;
|
||||
center.y = (containerRef.current as HTMLDivElement).getBoundingClientRect().height / 2;
|
||||
if (leader) {
|
||||
[leader, ...wingmen]
|
||||
.filter((unit) => unit !== null)
|
||||
.forEach((unit, idx) => {
|
||||
if (idx != 0) {
|
||||
const [dx, dz] = [-(unitPositions[idx].y - unitPositions[0].y), unitPositions[idx].x - unitPositions[0].x];
|
||||
const distance = Math.sqrt(dx ** 2 + dz ** 2);
|
||||
const offset = {
|
||||
x: dx,
|
||||
y: distance * verticalRatio,
|
||||
z: dz,
|
||||
};
|
||||
unit.followUnit(leader.ID, offset);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
units
|
||||
.filter((unit) => unit !== null)
|
||||
.forEach((unit, idx) => {
|
||||
if (units.length > 0 && units[0] !== null && idx != 0) {
|
||||
const ID = units[0].ID;
|
||||
|
||||
const [dx, dz] = [
|
||||
-(silhouetteHandles[idx].position.y - silhouetteHandles[0].position.y),
|
||||
silhouetteHandles[idx].position.x - silhouetteHandles[0].position.x,
|
||||
];
|
||||
const distance = Math.sqrt(dx ** 2 + dz ** 2);
|
||||
const offset = {
|
||||
x: dx * horizontalRatio,
|
||||
y: distance * verticalRatio,
|
||||
z: dz * horizontalRatio,
|
||||
};
|
||||
unit.followUnit(ID, offset);
|
||||
}
|
||||
});
|
||||
}}
|
||||
className={`
|
||||
mb-2 me-2 rounded-lg bg-blue-700 px-5 py-2.5 text-md font-medium
|
||||
text-white
|
||||
rounded-lg bg-blue-700 px-5 py-2.5 text-md font-medium text-white
|
||||
dark:bg-blue-600 dark:hover:bg-blue-700 dark:focus:ring-blue-800
|
||||
focus:outline-none focus:ring-4 focus:ring-blue-300
|
||||
hover:bg-blue-800
|
||||
@@ -266,158 +169,7 @@ export function FormationMenu(props: {
|
||||
>
|
||||
Apply
|
||||
</button>
|
||||
<div className="relative m-[-0.75rem] h-0">
|
||||
<div
|
||||
className={`
|
||||
relative left-6 top-4 h-4 border-2 border-white
|
||||
border-t-transparent text-center text-white
|
||||
`}
|
||||
style={{
|
||||
width: `${referenceWidth}px`,
|
||||
}}
|
||||
>
|
||||
<div className="translate-y-[-8px]">{referenceDistance}ft</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div
|
||||
className={`
|
||||
relative h-full w-full overflow-scroll rounded-md border-[1px]
|
||||
border-white/20 bg-white/10
|
||||
`}
|
||||
ref={scrollRef}
|
||||
>
|
||||
<div className={`h-[1000px] w-[1000px] h-max-[1000px] w-max-[1000px]`} ref={containerRef}>
|
||||
<>
|
||||
{Array(100)
|
||||
.fill(0)
|
||||
.map((_, idx) => {
|
||||
return (
|
||||
<div
|
||||
key={idx}
|
||||
className={`
|
||||
absolute top-0 h-[1000px] w-[1px] border-[1px]
|
||||
border-white/10
|
||||
`}
|
||||
style={{
|
||||
left: `${idx * 10}px`,
|
||||
}}
|
||||
></div>
|
||||
);
|
||||
})}
|
||||
</>
|
||||
<>
|
||||
{Array(100)
|
||||
.fill(0)
|
||||
.map((_, idx) => {
|
||||
return (
|
||||
<div
|
||||
key={idx}
|
||||
className={`
|
||||
absolute left-0 h-[1px] w-[1000px] border-[1px]
|
||||
border-white/5
|
||||
`}
|
||||
style={{
|
||||
top: `${idx * 10}px`,
|
||||
}}
|
||||
></div>
|
||||
);
|
||||
})}
|
||||
</>
|
||||
<>
|
||||
{units.map((unit, idx) => {
|
||||
return (
|
||||
<div
|
||||
key={`${count}-${idx}`}
|
||||
className={`
|
||||
absolute
|
||||
${unit ? "" : "hidden"}
|
||||
`}
|
||||
ref={silhouetteReferences[idx]}
|
||||
style={{
|
||||
top: silhouetteHandles[idx].position.y,
|
||||
left: silhouetteHandles[idx].position.x,
|
||||
}}
|
||||
onMouseDown={(e) => {
|
||||
silhouetteHandles[idx].handleMouseDown(e);
|
||||
setFormationType("custom");
|
||||
}}
|
||||
>
|
||||
<img
|
||||
className={`
|
||||
h-10 min-h-10 w-10 min-w-10 translate-x-[-50%]
|
||||
translate-y-[-50%] rotate-90 cursor-move opacity-80
|
||||
invert
|
||||
`}
|
||||
src={`./images/units/${unit?.getBlueprint()?.filename}`}
|
||||
></img>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</Menu>
|
||||
);
|
||||
}
|
||||
|
||||
function computeFormationOffset(formation, idx) {
|
||||
let offset = { x: 0, z: 0 };
|
||||
if (formation === "trail") {
|
||||
offset.x = -50 * idx;
|
||||
offset.z = 0;
|
||||
} else if (formation === "echelon-lh") {
|
||||
offset.x = -50 * idx;
|
||||
offset.z = -50 * idx;
|
||||
} else if (formation === "echelon-rh") {
|
||||
offset.x = -50 * idx;
|
||||
offset.z = 50 * idx;
|
||||
} else if (formation === "line-abreast-lh") {
|
||||
offset.x = 0;
|
||||
offset.z = -50 * idx;
|
||||
} else if (formation === "line-abreast-rh") {
|
||||
offset.x = 0;
|
||||
offset.z = 50 * idx;
|
||||
} else if (formation === "front") {
|
||||
offset.x = 100 * idx;
|
||||
offset.z = 0;
|
||||
} else if (formation === "diamond") {
|
||||
var xr = 0;
|
||||
var yr = 1;
|
||||
var zr = -1;
|
||||
var layer = 1;
|
||||
|
||||
for (let i = 0; i < idx; i++) {
|
||||
var xl = xr * Math.cos(Math.PI / 4) - yr * Math.sin(Math.PI / 4);
|
||||
var yl = xr * Math.sin(Math.PI / 4) + yr * Math.cos(Math.PI / 4);
|
||||
offset = { x: -yl * 50, z: xl * 50 };
|
||||
if (yr == 0) {
|
||||
layer++;
|
||||
xr = 0;
|
||||
yr = layer;
|
||||
zr = -layer;
|
||||
} else {
|
||||
if (xr < layer) {
|
||||
xr++;
|
||||
zr--;
|
||||
} else {
|
||||
yr--;
|
||||
zr++;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return offset;
|
||||
}
|
||||
|
||||
let formationTypes = {
|
||||
"echelon-lh": "Echelon left",
|
||||
"echelon-rh": "Echelon right",
|
||||
"line-abreast-rh": "Line abreast right",
|
||||
"line-abreast-lh": "Line abreast left",
|
||||
trail: "Trail",
|
||||
front: "Front",
|
||||
diamond: "Diamond",
|
||||
custom: "Custom",
|
||||
};
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import React, { MutableRefObject, useEffect, useRef, useState } from "react";
|
||||
import React, { MutableRefObject, useCallback, useEffect, useRef, useState } from "react";
|
||||
import { Menu } from "./components/menu";
|
||||
import { Unit } from "../../unit/unit";
|
||||
import { OlLabelToggle } from "../components/ollabeltoggle";
|
||||
@@ -51,37 +51,41 @@ import { FaCog, FaGasPump, FaSignal, FaTag } from "react-icons/fa";
|
||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||
import { OlSearchBar } from "../components/olsearchbar";
|
||||
import { OlDropdown, OlDropdownItem } from "../components/oldropdown";
|
||||
import { UnitBlueprint } from "../../interfaces";
|
||||
import { FaRadio, FaVolumeHigh } from "react-icons/fa6";
|
||||
import { OlNumberInput } from "../components/olnumberinput";
|
||||
import { Radio, TACAN } from "../../interfaces";
|
||||
import { OlStringInput } from "../components/olstringinput";
|
||||
import { OlFrequencyInput } from "../components/olfrequencyinput";
|
||||
import { UnitSink } from "../../audio/unitsink";
|
||||
import { AudioManagerStateChangedEvent, SelectedUnitsChangedEvent, SelectionClearedEvent } from "../../events";
|
||||
import { AudioManagerStateChangedEvent, SelectedUnitsChangedEvent, SelectionClearedEvent, UnitsUpdatedEvent } from "../../events";
|
||||
|
||||
export function UnitControlMenu(props: { open: boolean; onClose: () => void }) {
|
||||
function initializeUnitsData() {
|
||||
return {
|
||||
desiredAltitude: undefined as undefined | number,
|
||||
desiredAltitudeType: undefined as undefined | string,
|
||||
desiredSpeed: undefined as undefined | number,
|
||||
desiredSpeedType: undefined as undefined | string,
|
||||
ROE: undefined as undefined | string,
|
||||
reactionToThreat: undefined as undefined | string,
|
||||
emissionsCountermeasures: undefined as undefined | string,
|
||||
scenicAAA: undefined as undefined | boolean,
|
||||
missOnPurpose: undefined as undefined | boolean,
|
||||
shotsScatter: undefined as undefined | number,
|
||||
shotsIntensity: undefined as undefined | number,
|
||||
operateAs: undefined as undefined | Coalition,
|
||||
followRoads: undefined as undefined | boolean,
|
||||
isActiveAWACS: undefined as undefined | boolean,
|
||||
isActiveTanker: undefined as undefined | boolean,
|
||||
onOff: undefined as undefined | boolean,
|
||||
isAudioSink: undefined as undefined | boolean,
|
||||
};
|
||||
}
|
||||
|
||||
const [selectedUnits, setSelectedUnits] = useState([] as Unit[]);
|
||||
const [audioManagerState, setAudioManagerState] = useState(false);
|
||||
const [selectedUnitsData, setSelectedUnitsData] = useState({
|
||||
desiredAltitude: undefined as undefined | number,
|
||||
desiredAltitudeType: undefined as undefined | string,
|
||||
desiredSpeed: undefined as undefined | number,
|
||||
desiredSpeedType: undefined as undefined | string,
|
||||
ROE: undefined as undefined | string,
|
||||
reactionToThreat: undefined as undefined | string,
|
||||
emissionsCountermeasures: undefined as undefined | string,
|
||||
scenicAAA: undefined as undefined | boolean,
|
||||
missOnPurpose: undefined as undefined | boolean,
|
||||
shotsScatter: undefined as undefined | number,
|
||||
shotsIntensity: undefined as undefined | number,
|
||||
operateAs: undefined as undefined | Coalition,
|
||||
followRoads: undefined as undefined | boolean,
|
||||
isActiveAWACS: undefined as undefined | boolean,
|
||||
isActiveTanker: undefined as undefined | boolean,
|
||||
onOff: undefined as undefined | boolean,
|
||||
isAudioSink: undefined as undefined | boolean,
|
||||
});
|
||||
const [selectedUnitsData, setSelectedUnitsData] = useState(initializeUnitsData);
|
||||
const [forcedUnitsData, setForcedUnitsData] = useState(initializeUnitsData);
|
||||
const [selectionFilter, setSelectionFilter] = useState({
|
||||
control: {
|
||||
human: true,
|
||||
@@ -115,6 +119,7 @@ export function UnitControlMenu(props: { open: boolean; onClose: () => void }) {
|
||||
const [filterString, setFilterString] = useState("");
|
||||
const [showAdvancedSettings, setShowAdvancedSettings] = useState(false);
|
||||
const [activeAdvancedSettings, setActiveAdvancedSettings] = useState(null as null | { radio: Radio; TACAN: TACAN });
|
||||
const [lastUpdateTime, setLastUpdateTime] = useState(0);
|
||||
|
||||
var searchBarRef = useRef(null);
|
||||
|
||||
@@ -122,6 +127,7 @@ export function UnitControlMenu(props: { open: boolean; onClose: () => void }) {
|
||||
SelectedUnitsChangedEvent.on((units) => setSelectedUnits(units));
|
||||
SelectionClearedEvent.on(() => setSelectedUnits([]));
|
||||
AudioManagerStateChangedEvent.on((state) => setAudioManagerState(state));
|
||||
UnitsUpdatedEvent.on((units) => units.find((unit) => unit.getSelected()) && setLastUpdateTime(Date.now()));
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
@@ -130,7 +136,7 @@ export function UnitControlMenu(props: { open: boolean; onClose: () => void }) {
|
||||
if (!props.open && filterString !== "") setFilterString("");
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
const updateData = useCallback(() => {
|
||||
setShowAdvancedSettings(false);
|
||||
|
||||
const getters = {
|
||||
@@ -169,10 +175,24 @@ export function UnitControlMenu(props: { open: boolean; onClose: () => void }) {
|
||||
} as { [key in keyof typeof selectedUnitsData]: (unit: Unit) => void };
|
||||
|
||||
var updatedData = {};
|
||||
let anyForcedDataUpdated = false;
|
||||
Object.entries(getters).forEach(([key, getter]) => {
|
||||
updatedData[key] = getApp()?.getUnitsManager()?.getSelectedUnitsVariable(getter);
|
||||
let newDatum = getApp()?.getUnitsManager()?.getSelectedUnitsVariable(getter);
|
||||
if (forcedUnitsData[key] !== undefined) {
|
||||
if (newDatum === updatedData[key]) {
|
||||
anyForcedDataUpdated = true;
|
||||
forcedUnitsData[key] === undefined;
|
||||
}
|
||||
updatedData[key] = forcedUnitsData[key];
|
||||
} else updatedData[key] = newDatum;
|
||||
});
|
||||
setSelectedUnitsData(updatedData as typeof selectedUnitsData);
|
||||
if (anyForcedDataUpdated) setForcedUnitsData({...forcedUnitsData})
|
||||
}, [forcedUnitsData])
|
||||
useEffect(updateData, [selectedUnits, lastUpdateTime, forcedUnitsData]);
|
||||
|
||||
useEffect(() => {
|
||||
setForcedUnitsData(initializeUnitsData);
|
||||
}, [selectedUnits]);
|
||||
|
||||
/* Count how many units are selected of each type, divided by coalition */
|
||||
@@ -538,8 +558,8 @@ export function UnitControlMenu(props: { open: boolean; onClose: () => void }) {
|
||||
onClick={() => {
|
||||
selectedUnits.forEach((unit) => {
|
||||
unit.setAltitudeType(selectedUnitsData.desiredAltitudeType === "ASL" ? "AGL" : "ASL");
|
||||
setSelectedUnitsData({
|
||||
...selectedUnitsData,
|
||||
setForcedUnitsData({
|
||||
...forcedUnitsData,
|
||||
desiredAltitudeType: selectedUnitsData.desiredAltitudeType === "ASL" ? "AGL" : "ASL",
|
||||
});
|
||||
});
|
||||
@@ -550,8 +570,8 @@ export function UnitControlMenu(props: { open: boolean; onClose: () => void }) {
|
||||
onChange={(ev) => {
|
||||
selectedUnits.forEach((unit) => {
|
||||
unit.setAltitude(ftToM(Number(ev.target.value)));
|
||||
setSelectedUnitsData({
|
||||
...selectedUnitsData,
|
||||
setForcedUnitsData({
|
||||
...forcedUnitsData,
|
||||
desiredAltitude: Number(ev.target.value),
|
||||
});
|
||||
});
|
||||
@@ -600,8 +620,8 @@ export function UnitControlMenu(props: { open: boolean; onClose: () => void }) {
|
||||
onClick={() => {
|
||||
selectedUnits.forEach((unit) => {
|
||||
unit.setSpeedType(selectedUnitsData.desiredSpeedType === "CAS" ? "GS" : "CAS");
|
||||
setSelectedUnitsData({
|
||||
...selectedUnitsData,
|
||||
setForcedUnitsData({
|
||||
...forcedUnitsData,
|
||||
desiredSpeedType: selectedUnitsData.desiredSpeedType === "CAS" ? "GS" : "CAS",
|
||||
});
|
||||
});
|
||||
@@ -613,8 +633,8 @@ export function UnitControlMenu(props: { open: boolean; onClose: () => void }) {
|
||||
onChange={(ev) => {
|
||||
selectedUnits.forEach((unit) => {
|
||||
unit.setSpeed(knotsToMs(Number(ev.target.value)));
|
||||
setSelectedUnitsData({
|
||||
...selectedUnitsData,
|
||||
setForcedUnitsData({
|
||||
...forcedUnitsData,
|
||||
desiredSpeed: Number(ev.target.value),
|
||||
});
|
||||
});
|
||||
@@ -645,8 +665,8 @@ export function UnitControlMenu(props: { open: boolean; onClose: () => void }) {
|
||||
onClick={() => {
|
||||
selectedUnits.forEach((unit) => {
|
||||
unit.setROE(ROEs[convertROE(idx)]);
|
||||
setSelectedUnitsData({
|
||||
...selectedUnitsData,
|
||||
setForcedUnitsData({
|
||||
...forcedUnitsData,
|
||||
ROE: ROEs[convertROE(idx)],
|
||||
});
|
||||
});
|
||||
@@ -682,8 +702,8 @@ export function UnitControlMenu(props: { open: boolean; onClose: () => void }) {
|
||||
onClick={() => {
|
||||
selectedUnits.forEach((unit) => {
|
||||
unit.setReactionToThreat(reactionsToThreat[idx]);
|
||||
setSelectedUnitsData({
|
||||
...selectedUnitsData,
|
||||
setForcedUnitsData({
|
||||
...forcedUnitsData,
|
||||
reactionToThreat: reactionsToThreat[idx],
|
||||
});
|
||||
});
|
||||
@@ -714,8 +734,8 @@ export function UnitControlMenu(props: { open: boolean; onClose: () => void }) {
|
||||
onClick={() => {
|
||||
selectedUnits.forEach((unit) => {
|
||||
unit.setEmissionsCountermeasures(emissionsCountermeasures[idx]);
|
||||
setSelectedUnitsData({
|
||||
...selectedUnitsData,
|
||||
setForcedUnitsData({
|
||||
...forcedUnitsData,
|
||||
emissionsCountermeasures: emissionsCountermeasures[idx],
|
||||
});
|
||||
});
|
||||
@@ -756,8 +776,8 @@ export function UnitControlMenu(props: { open: boolean; onClose: () => void }) {
|
||||
unit.getRadio(),
|
||||
unit.getGeneralSettings()
|
||||
);
|
||||
setSelectedUnitsData({
|
||||
...selectedUnitsData,
|
||||
setForcedUnitsData({
|
||||
...forcedUnitsData,
|
||||
isActiveTanker: !selectedUnitsData.isActiveTanker,
|
||||
});
|
||||
});
|
||||
@@ -790,8 +810,8 @@ export function UnitControlMenu(props: { open: boolean; onClose: () => void }) {
|
||||
unit.getRadio(),
|
||||
unit.getGeneralSettings()
|
||||
);
|
||||
setSelectedUnitsData({
|
||||
...selectedUnitsData,
|
||||
setForcedUnitsData({
|
||||
...forcedUnitsData,
|
||||
isActiveAWACS: !selectedUnitsData.isActiveAWACS,
|
||||
});
|
||||
});
|
||||
@@ -848,8 +868,8 @@ export function UnitControlMenu(props: { open: boolean; onClose: () => void }) {
|
||||
onClick={() => {
|
||||
selectedUnits.forEach((unit) => {
|
||||
selectedUnitsData.scenicAAA ? unit.changeSpeed("stop") : unit.scenicAAA();
|
||||
setSelectedUnitsData({
|
||||
...selectedUnitsData,
|
||||
setForcedUnitsData({
|
||||
...forcedUnitsData,
|
||||
scenicAAA: !selectedUnitsData.scenicAAA,
|
||||
missOnPurpose: false,
|
||||
});
|
||||
@@ -873,8 +893,8 @@ export function UnitControlMenu(props: { open: boolean; onClose: () => void }) {
|
||||
onClick={() => {
|
||||
selectedUnits.forEach((unit) => {
|
||||
selectedUnitsData.missOnPurpose ? unit.changeSpeed("stop") : unit.missOnPurpose();
|
||||
setSelectedUnitsData({
|
||||
...selectedUnitsData,
|
||||
setForcedUnitsData({
|
||||
...forcedUnitsData,
|
||||
scenicAAA: false,
|
||||
missOnPurpose: !selectedUnitsData.missOnPurpose,
|
||||
});
|
||||
@@ -902,8 +922,8 @@ export function UnitControlMenu(props: { open: boolean; onClose: () => void }) {
|
||||
onClick={() => {
|
||||
selectedUnits.forEach((unit) => {
|
||||
unit.setShotsScatter(idx + 1);
|
||||
setSelectedUnitsData({
|
||||
...selectedUnitsData,
|
||||
setForcedUnitsData({
|
||||
...forcedUnitsData,
|
||||
shotsScatter: idx + 1,
|
||||
});
|
||||
});
|
||||
@@ -934,8 +954,8 @@ export function UnitControlMenu(props: { open: boolean; onClose: () => void }) {
|
||||
onClick={() => {
|
||||
selectedUnits.forEach((unit) => {
|
||||
unit.setShotsIntensity(idx + 1);
|
||||
setSelectedUnitsData({
|
||||
...selectedUnitsData,
|
||||
setForcedUnitsData({
|
||||
...forcedUnitsData,
|
||||
shotsIntensity: idx + 1,
|
||||
});
|
||||
});
|
||||
@@ -964,8 +984,8 @@ export function UnitControlMenu(props: { open: boolean; onClose: () => void }) {
|
||||
onClick={() => {
|
||||
selectedUnits.forEach((unit) => {
|
||||
unit.setOperateAs(selectedUnitsData.operateAs === "blue" ? "red" : "blue");
|
||||
setSelectedUnitsData({
|
||||
...selectedUnitsData,
|
||||
setForcedUnitsData({
|
||||
...forcedUnitsData,
|
||||
operateAs: selectedUnitsData.operateAs === "blue" ? "red" : "blue",
|
||||
});
|
||||
});
|
||||
@@ -989,8 +1009,8 @@ export function UnitControlMenu(props: { open: boolean; onClose: () => void }) {
|
||||
onClick={() => {
|
||||
selectedUnits.forEach((unit) => {
|
||||
unit.setFollowRoads(!selectedUnitsData.followRoads);
|
||||
setSelectedUnitsData({
|
||||
...selectedUnitsData,
|
||||
setForcedUnitsData({
|
||||
...forcedUnitsData,
|
||||
followRoads: !selectedUnitsData.followRoads,
|
||||
});
|
||||
});
|
||||
@@ -1013,8 +1033,8 @@ export function UnitControlMenu(props: { open: boolean; onClose: () => void }) {
|
||||
onClick={() => {
|
||||
selectedUnits.forEach((unit) => {
|
||||
unit.setOnOff(!selectedUnitsData.onOff);
|
||||
setSelectedUnitsData({
|
||||
...selectedUnitsData,
|
||||
setForcedUnitsData({
|
||||
...forcedUnitsData,
|
||||
onOff: !selectedUnitsData.onOff,
|
||||
});
|
||||
});
|
||||
@@ -1041,8 +1061,8 @@ export function UnitControlMenu(props: { open: boolean; onClose: () => void }) {
|
||||
selectedUnits.forEach((unit) => {
|
||||
if (!selectedUnitsData.isAudioSink) {
|
||||
getApp()?.getAudioManager().addUnitSink(unit);
|
||||
setSelectedUnitsData({
|
||||
...selectedUnitsData,
|
||||
setForcedUnitsData({
|
||||
...forcedUnitsData,
|
||||
isAudioSink: true,
|
||||
});
|
||||
} else {
|
||||
@@ -1054,8 +1074,8 @@ export function UnitControlMenu(props: { open: boolean; onClose: () => void }) {
|
||||
});
|
||||
if (sink !== undefined) getApp()?.getAudioManager().removeSink(sink);
|
||||
|
||||
setSelectedUnitsData({
|
||||
...selectedUnitsData,
|
||||
setForcedUnitsData({
|
||||
...forcedUnitsData,
|
||||
isAudioSink: false,
|
||||
});
|
||||
}
|
||||
@@ -1178,10 +1198,9 @@ export function UnitControlMenu(props: { open: boolean; onClose: () => void }) {
|
||||
value={activeAdvancedSettings ? activeAdvancedSettings.TACAN.channel : 1}
|
||||
></OlNumberInput>
|
||||
|
||||
<OlDropdown
|
||||
label={activeAdvancedSettings ? activeAdvancedSettings.TACAN.XY : "X"}
|
||||
className={`my-auto w-20`}
|
||||
>
|
||||
<OlDropdown label={activeAdvancedSettings ? activeAdvancedSettings.TACAN.XY : "X"} className={`
|
||||
my-auto w-20
|
||||
`}>
|
||||
<OlDropdownItem
|
||||
key={"X"}
|
||||
onClick={() => {
|
||||
@@ -1300,11 +1319,9 @@ export function UnitControlMenu(props: { open: boolean; onClose: () => void }) {
|
||||
className={`
|
||||
flex content-center gap-2 rounded-full
|
||||
${selectedUnits[0].getFuel() > 40 && `bg-green-700`}
|
||||
${
|
||||
selectedUnits[0].getFuel() > 10 &&
|
||||
selectedUnits[0].getFuel() <= 40 &&
|
||||
`bg-yellow-700`
|
||||
}
|
||||
${selectedUnits[0].getFuel() > 10 && selectedUnits[0].getFuel() <= 40 && `
|
||||
bg-yellow-700
|
||||
`}
|
||||
${selectedUnits[0].getFuel() <= 10 && `bg-red-700`}
|
||||
px-2 py-1 text-sm font-bold text-white
|
||||
`}
|
||||
|
||||
Reference in New Issue
Block a user