diff --git a/frontend/react/src/constants/constants.ts b/frontend/react/src/constants/constants.ts
index e6b81668..ce5c1728 100644
--- a/frontend/react/src/constants/constants.ts
+++ b/frontend/react/src/constants/constants.ts
@@ -53,25 +53,46 @@ export const IRST = 8;
export const RWR = 16;
export const DLINK = 32;
+export enum UnitState {
+ NONE = "none",
+ IDLE = "idle",
+ REACH_DESTINATION = "reach-destination",
+ ATTACK = "attack",
+ FOLLOW = "follow",
+ LAND = "land",
+ REFUEL = "refuel",
+ AWACS = "AWACS",
+ TANKER = "tanker",
+ BOMB_POINT = "bomb-point",
+ CARPET_BOMB = "carpet-bomb",
+ BOMB_BUILDING = "bomb-building",
+ FIRE_AT_AREA = "fire-at-area",
+ SIMULATE_FIRE_FIGHT = "simulate-fire-fight",
+ SCENIC_AAA = "scenic-aaa",
+ MISS_ON_PURPOSE = "miss-on-purpose",
+ LAND_AT_POINT = "land-at-point",
+}
+
export const states: string[] = [
- "none",
- "idle",
- "reach-destination",
- "attack",
- "follow",
- "land",
- "refuel",
- "AWACS",
- "tanker",
- "bomb-point",
- "carpet-bomb",
- "bomb-building",
- "fire-at-area",
- "simulate-fire-fight",
- "scenic-aaa",
- "miss-on-purpose",
- "land-at-point",
+ UnitState.NONE,
+ UnitState.IDLE,
+ UnitState.REACH_DESTINATION,
+ UnitState.ATTACK,
+ UnitState.FOLLOW,
+ UnitState.LAND,
+ UnitState.REFUEL,
+ UnitState.AWACS,
+ UnitState.TANKER,
+ UnitState.BOMB_POINT,
+ UnitState.CARPET_BOMB,
+ UnitState.BOMB_BUILDING,
+ UnitState.FIRE_AT_AREA,
+ UnitState.SIMULATE_FIRE_FIGHT,
+ UnitState.SCENIC_AAA,
+ UnitState.MISS_ON_PURPOSE,
+ UnitState.LAND_AT_POINT,
];
+
export const ROEs: string[] = ["free", "designated", "", "return", "hold"];
export const reactionsToThreat: string[] = ["none", "manoeuvre", "passive", "evade"];
export const emissionsCountermeasures: string[] = ["silent", "attack", "defend", "free"];
@@ -284,7 +305,6 @@ export const formationTypes = {
custom: "Custom",
};
-
export enum OlympusState {
NOT_INITIALIZED = "Not initialized",
SERVER = "Server",
@@ -302,7 +322,7 @@ export enum OlympusState {
AIRBASE = "Airbase",
GAME_MASTER = "Game master",
IMPORT_EXPORT = "Import/export",
- WARNING = "Warning modal"
+ WARNING = "Warning modal",
}
export const NO_SUBSTATE = "No substate";
@@ -351,16 +371,15 @@ export enum OptionsSubstate {
export enum ImportExportSubstate {
NO_SUBSTATE = "No substate",
IMPORT = "IMPORT",
- EXPORT = "EXPORT"
+ EXPORT = "EXPORT",
}
export enum WarningSubstate {
NO_SUBSTATE = "No substate",
NOT_CHROME = "Not chrome",
- NOT_SECURE = "Not secure"
+ NOT_SECURE = "Not secure",
}
-
export type OlympusSubState = DrawSubState | JTACSubState | SpawnSubState | OptionsSubstate | string;
export const IADSTypes = ["AAA", "SAM Site", "Radar (EWR)"];
@@ -389,7 +408,7 @@ export const MAP_OPTIONS_DEFAULTS: MapOptions = {
AWACSMode: false,
AWACSCoalition: "blue",
hideChromeWarning: false,
- hideSecureWarning: false
+ hideSecureWarning: false,
};
export const MAP_HIDDEN_TYPES_DEFAULTS = {
@@ -542,7 +561,7 @@ export namespace ContextActions {
.getUnitsManager()
.addDestination(targetPosition, getApp().getMap().getKeepRelativePositions(), getApp().getMap().getDestinationRotation(), units);
},
- { type: ContextActionType.MOVE, code: null}
+ { type: ContextActionType.MOVE, code: null }
);
export const DELETE = new ContextAction(
diff --git a/frontend/react/src/other/utils.ts b/frontend/react/src/other/utils.ts
index 8f0cbd49..c519c991 100644
--- a/frontend/react/src/other/utils.ts
+++ b/frontend/react/src/other/utils.ts
@@ -447,22 +447,25 @@ export function deepCopyTable(table) {
}
export function computeStandardFormationOffset(formation, idx) {
- let offset = { x: 0, y: 0 };
+ let offset = { x: 0, y: 0, z: 0 };
if (formation === "trail") {
- offset.y = 50 * idx;
+ offset.y = 75 * idx;
offset.x = 0;
+ offset.z = -Math.sqrt(offset.x * offset.x + offset.y * offset.y) * 0.1;
} else if (formation === "echelon-lh" || formation == "custom" /* default fallback if needed */) {
offset.y = 50 * idx;
offset.x = -50 * idx;
+ offset.z = -Math.sqrt(offset.x * offset.x + offset.y * offset.y) * 0.1;
} else if (formation === "echelon-rh") {
offset.y = 50 * idx;
offset.x = 50 * idx;
+ offset.z = -Math.sqrt(offset.x * offset.x + offset.y * offset.y) * 0.1;
} else if (formation === "line-abreast-lh") {
offset.y = 0;
- offset.x = -50 * idx;
+ offset.x = -75 * idx;
} else if (formation === "line-abreast-rh") {
offset.y = 0;
- offset.x = 50 * idx;
+ offset.x = 75 * idx;
} else if (formation === "front") {
offset.y = -100 * idx;
offset.x = 0;
@@ -475,7 +478,8 @@ export function computeStandardFormationOffset(formation, idx) {
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: xl * 50, y: yl * 50 };
+ offset = { x: xl * 50, y: yl * 50, z: 0 };
+ offset.z = -Math.sqrt(offset.x * offset.x + offset.y * offset.y) * 0.1
if (yr == 0) {
layer++;
xr = 0;
@@ -494,3 +498,31 @@ export function computeStandardFormationOffset(formation, idx) {
}
return offset;
}
+
+export function nearestNiceNumber(number) {
+ let niceNumbers = [1, 2, 5, 10, 20, 50, 100, 200, 500, 1000, 2000, 5000, 10000];
+ let niceNumber = niceNumbers[0];
+ for (let i = 0; i < niceNumbers.length; i++) {
+ if (niceNumbers[i] >= number) {
+ niceNumber = niceNumbers[i];
+ break;
+ }
+ }
+ return niceNumber;
+}
+
+export function roundToNearestFive(number) {
+ return Math.round(number / 5) * 5;
+}
+
+export function toDCSFormationOffset(offset: {x: number, y: number, z: number}) {
+ // X: front-rear, positive front
+ // Y: top-bottom, positive top
+ // Z: left-right, positive right
+
+ return { x: -offset.y, y: offset.z, z: offset.x };
+}
+
+export function fromDCSFormationOffset(offset: {x: number, y: number, z: number}) {
+ return { x: offset.z, y: -offset.x, z: offset.y };
+}
\ No newline at end of file
diff --git a/frontend/react/src/ui/panels/components/draggable.tsx b/frontend/react/src/ui/panels/components/draggable.tsx
index 4710a79c..07e363a8 100644
--- a/frontend/react/src/ui/panels/components/draggable.tsx
+++ b/frontend/react/src/ui/panels/components/draggable.tsx
@@ -1,10 +1,10 @@
import React, { useCallback, useEffect, useRef, useState } from "react";
export function Draggable(props: {
- position: { x: number; y: number };
+ position: { x: number; y: number, z: number };
children: JSX.Element | JSX.Element[];
disabled: boolean;
- onPositionChange: (position: { x: number; y: number }) => void;
+ onPositionChange: (position: { x: number; y: number, z: number }) => void;
}) {
const [dragging, setDragging] = useState(false);
const [refPosition, setRefPosition] = useState({ x: 0, y: 0 });
@@ -15,7 +15,7 @@ export function Draggable(props: {
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 });
+ if (!props.disabled) props.onPositionChange({ x: props.position.x + e.clientX - refPosition.x, y: props.position.y + e.clientY - refPosition.y, z: props.position.z });
}
},
[dragging, refPosition]
diff --git a/frontend/react/src/ui/panels/components/draggablesilhouette.tsx b/frontend/react/src/ui/panels/components/draggablesilhouette.tsx
index 3dcbbdb6..0ea2d53e 100644
--- a/frontend/react/src/ui/panels/components/draggablesilhouette.tsx
+++ b/frontend/react/src/ui/panels/components/draggablesilhouette.tsx
@@ -1,32 +1,60 @@
import React, { useCallback, useEffect, useRef, useState } from "react";
import { Draggable } from "./draggable";
import { Unit } from "../../../unit/unit";
+import { FaArrowDown, FaArrowUp } from "react-icons/fa";
+import { nearestNiceNumber } from "../../../other/utils";
export function DraggableSilhouette(props: {
- position: { x: number; y: number };
+ position: { x: number; y: number; z: number };
unit: Unit;
zoom: number;
scale: number;
disabled: boolean;
angle: number;
- onPositionChange: (position: { x: number; y: number }) => void;
+ showVerticalOffset: boolean;
+ onPositionChange: (position: { x: number; y: number; z: number }) => void;
src?: string;
}) {
+ const imgHeight = Math.round((props.scale * (props.disabled ? 20 : (props.unit?.getBlueprint()?.length ?? 50))) / Math.min(3, props.disabled ? 1 : props.zoom));
return (
+ onWheel={(e) => {
+ e.stopPropagation();
+ let delta = nearestNiceNumber(Math.max(1, 0.1 * Math.abs(props.position.z)));
+ let newZ = props.position.z + (e.deltaY > 0 ? -delta : delta);
+ newZ = Math.round(newZ / delta) * delta;
+ props.onPositionChange({ x: props.position.x, y: props.position.y, z: newZ });
+ }}
+ />
+ {props.showVerticalOffset ? (
+
+
+ {Math.round(props.position.z) > 0 ? (
+
+ ) : Math.round(props.position.z) < 0 ? (
+
+ ) : (
+ <>>
+ )}
+ {Math.round(Math.abs(props.position.z))}
ft
+
+
+ ) : (
+ <>>
+ )}
);
}
diff --git a/frontend/react/src/ui/panels/components/formationcanvas.tsx b/frontend/react/src/ui/panels/components/formationcanvas.tsx
index b5421299..4493daed 100644
--- a/frontend/react/src/ui/panels/components/formationcanvas.tsx
+++ b/frontend/react/src/ui/panels/components/formationcanvas.tsx
@@ -1,19 +1,21 @@
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";
+import { FaArrowDown, FaArrowUp, FaCompressArrowsAlt, FaExclamationTriangle, FaExpand, FaExpandArrowsAlt, FaQuestionCircle } from "react-icons/fa";
+import { OlToggle } from "../../components/oltoggle";
const FT_TO_PX = 1;
export function FormationCanvas(props: {
units: Unit[];
- unitPositions: { x: number; y: number }[];
- setUnitPositions: (positions: { x: number; y: number }[]) => void;
+ unitPositions: { x: number; y: number; z: number }[];
+ setUnitPositions: (positions: { x: number; y: number; z: 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);
+ const [showVerticalOffset, setShowVerticalOffset] = useState(true);
/* Init references and hooks */
const containerRef = useRef(null);
@@ -79,55 +81,118 @@ export function FormationCanvas(props: {
return (
<>
-
-
-
+
+
+
+
+
+
+
+
+
+
+
Show units vertical offset
{setShowVerticalOffset(!showVerticalOffset)}} toggled={showVerticalOffset} />
+
{
+ showVerticalOffset={showVerticalOffset}
+ onPositionChange={({ x, y, z }) => {
+ if (idx === 0) return;
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,
+ z: z,
};
props.setUnitPositions([...props.unitPositions]);
}}
@@ -226,12 +295,12 @@ export function FormationCanvas(props: {
);
})}
- {zoom > 3 && (
+
-
-
Silhouettes not to scale!
+
+
Double click to reset view
- )}
+
{referenceDistance === 5280 &&
1 NM
}
- {referenceDistance === 5280 * 2 && (
-
- 2 NM
-
- )}
+ {referenceDistance === 5280 * 2 &&
2 NM
}
{referenceDistance < 5280 &&
{referenceDistance} ft
}
diff --git a/frontend/react/src/ui/panels/formationmenu.tsx b/frontend/react/src/ui/panels/formationmenu.tsx
index da7cc8f5..081d9d73 100644
--- a/frontend/react/src/ui/panels/formationmenu.tsx
+++ b/frontend/react/src/ui/panels/formationmenu.tsx
@@ -1,11 +1,10 @@
-import React, { useCallback, useEffect, useRef, useState } from "react";
+import React, { useCallback, useEffect, useState } from "react";
import { Menu } from "./components/menu";
import { OlDropdown, OlDropdownItem } from "../components/oldropdown";
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 { computeStandardFormationOffset, fromDCSFormationOffset, toDCSFormationOffset } from "../../other/utils";
+import { formationTypes, UnitState } from "../../constants/constants";
import { FormationCanvas } from "./components/formationcanvas";
export function FormationMenu(props: { open: boolean; onClose: () => void; children?: JSX.Element | JSX.Element[] }) {
@@ -14,10 +13,11 @@ export function FormationMenu(props: { open: boolean; onClose: () => void; child
/* Init state variables */
const [formationType, setFormationType] = useState("echelon-lh");
- const [verticalScale, setVerticalScale] = useState(30);
- const [unitPositions, setUnitPositions] = useState([] as { x: number; y: number }[]);
+ const [unitPositions, setUnitPositions] = useState([] as { x: number; y: number, z: number }[]);
- const verticalRatio = (verticalScale - 50) / 50;
+ useEffect(() => {
+ setFormationType("echelon-lh");
+ }, [props.open])
/* Listen for the setting of a new leader and wingmen and check if the formation is too big */
useEffect(() => {
@@ -40,6 +40,23 @@ export function FormationMenu(props: { open: boolean; onClose: () => void; child
}, [formationType]);
useEffect(setStandardFormation, [formationType]);
+ const setCurrentFormation = useCallback(() => {
+ if (
+ wingmen.every((unit: Unit) => {
+ return unit.getState() === UnitState.FOLLOW && unit.getLeader() === leader;
+ })
+ ) {
+ setUnitPositions([
+ { x: 0, y: 0, z: 0 },
+ ...wingmen.map((unit, idx) => {
+ return fromDCSFormationOffset(unit.getFormationOffset());
+ }),
+ ]);
+ setFormationType("custom");
+ }
+ }, [leader, wingmen]);
+ useEffect(setCurrentFormation, [leader, wingmen]);
+
if (leader && unitPositions.length < [leader, ...wingmen].length) {
/* If more units are added to the group keep the existing positions */
setUnitPositions(
@@ -120,18 +137,6 @@ export function FormationMenu(props: { open: boolean; onClose: () => void; child
Load
- Vertical separation
-
-
Down
-
{
- setVerticalScale(Number(ev.target.value));
- }}
- />
- Up
-
void; child
.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 = {
+ const [dx, dy] = [unitPositions[idx].x - unitPositions[0].x, unitPositions[idx].y - unitPositions[0].y];
+
+ unit.followUnit(leader.ID, toDCSFormationOffset({
x: dx,
- y: distance * verticalRatio,
- z: dz,
- };
- unit.followUnit(leader.ID, offset);
+ y: dy,
+ z: unitPositions[idx].z,
+ }));
}
});
}
diff --git a/frontend/react/src/unit/unitsmanager.ts b/frontend/react/src/unit/unitsmanager.ts
index 7f048341..aff29e9b 100644
--- a/frontend/react/src/unit/unitsmanager.ts
+++ b/frontend/react/src/unit/unitsmanager.ts
@@ -807,86 +807,17 @@ export class UnitsManager {
* @param formation Optional parameter, defines a predefined formation type. Values are: "trail", "echelon-lh", "echelon-rh", "line-abreast-lh", "line-abreast-rh", "front", "diamond"
* @param units (Optional) Array of units to apply the control to. If not provided, the operation will be completed on all selected units.
*/
- followUnit(ID: number, offset?: { x: number; y: number; z: number }, formation?: string, units: Unit[] | null = null) {
+ followUnit(ID: number, offset?: { x: number; y: number; z: number }, units: Unit[] | null = null) {
if (units === null) units = this.getSelectedUnits();
units = units.filter((unit) => !unit.getHuman());
let callback = (units) => {
- if (offset == undefined) {
- /* Simple formations with fixed offsets */
- offset = { x: 0, y: 0, z: 0 };
- if (formation === "trail") {
- offset.x = -50;
- offset.y = -30;
- offset.z = 0;
- } else if (formation === "echelon-lh") {
- offset.x = -50;
- offset.y = -10;
- offset.z = -50;
- } else if (formation === "echelon-rh") {
- offset.x = -50;
- offset.y = -10;
- offset.z = 50;
- } else if (formation === "line-abreast-lh") {
- offset.x = 0;
- offset.y = 0;
- offset.z = -50;
- } else if (formation === "line-abreast-rh") {
- offset.x = 0;
- offset.y = 0;
- offset.z = 50;
- } else if (formation === "front") {
- offset.x = 100;
- offset.y = 0;
- offset.z = 0;
- } else offset = undefined;
- }
-
if (getApp().getMap().getOptions().protectDCSUnits && !units.every((unit) => unit.isControlledByOlympus())) {
getApp().setState(OlympusState.UNIT_CONTROL, UnitControlSubState.PROTECTION);
this.#protectionCallback = callback;
} else callback(units);
};
- var count = 1;
- var xr = 0;
- var yr = 1;
- var zr = -1;
- var layer = 1;
- units.forEach((unit: Unit) => {
- if (unit.ID !== ID) {
- if (offset != undefined)
- /* Offset is set, apply it */
- unit.followUnit(ID, {
- x: offset.x * count,
- y: offset.y * count,
- z: offset.z * count,
- });
- else {
- /* More complex formations with variable offsets */
- if (formation === "diamond") {
- 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);
- unit.followUnit(ID, { x: -yl * 50, y: zr * 10, z: xl * 50 });
- if (yr == 0) {
- layer++;
- xr = 0;
- yr = layer;
- zr = -layer;
- } else {
- if (xr < layer) {
- xr++;
- zr--;
- } else {
- yr--;
- zr++;
- }
- }
- }
- }
- count++;
- }
- });
this.#showActionMessage(units, `following unit ${this.getUnitByID(ID)?.getUnitName()}`);
}
@@ -1332,7 +1263,7 @@ export class UnitsManager {
getApp().getMap().getMouseCoordinates().lat + unit.position.lat - avgLat,
getApp().getMap().getMouseCoordinates().lng + unit.position.lng - avgLng
);
- markers.push(getApp().getMap().addTemporaryMarker(position, unit.name, unit.coalition));
+ markers.push(getApp().getMap().addTemporaryMarker(position, unit.name, unit.coalition, false));
units.push({ ID: unit.ID, location: position });
});