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