mirror of
https://github.com/Pax1601/DCSOlympus.git
synced 2025-10-29 16:56:34 +00:00
feat: completed formation menu
This commit is contained in:
parent
9bd206a750
commit
5b6f58a38e
@ -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(
|
||||
|
||||
@ -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 };
|
||||
}
|
||||
@ -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]
|
||||
|
||||
@ -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 (
|
||||
<Draggable position={props.position} onPositionChange={props.onPositionChange} disabled={props.disabled}>
|
||||
<img
|
||||
data-disabled = {props.disabled}
|
||||
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`,
|
||||
maxWidth: `${imgHeight}px`,
|
||||
minWidth: `${imgHeight}px`,
|
||||
rotate: `${props.disabled ? props.angle : 90}deg`,
|
||||
}}
|
||||
></img>
|
||||
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 ? (
|
||||
<div className={`absolute flex w-full justify-center text-gray-400`}
|
||||
style={{top: `calc(50% + ${imgHeight / 2}px)`}}>
|
||||
<div className="flex gap-1">
|
||||
{Math.round(props.position.z) > 0 ? (
|
||||
<FaArrowUp className={`my-auto text-xs`} />
|
||||
) : Math.round(props.position.z) < 0 ? (
|
||||
<FaArrowDown className={`my-auto text-xs`} />
|
||||
) : (
|
||||
<></>
|
||||
)}
|
||||
{Math.round(Math.abs(props.position.z))} <div>ft</div>
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
<></>
|
||||
)}
|
||||
</Draggable>
|
||||
);
|
||||
}
|
||||
|
||||
@ -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 (
|
||||
<>
|
||||
<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 className="flex">
|
||||
<div className="flex w-fit 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,
|
||||
z: position.z,
|
||||
};
|
||||
})
|
||||
);
|
||||
}}
|
||||
className={`
|
||||
rounded-l-lg px-3 py-2 text-md flex content-center justify-center
|
||||
gap-2 border-r-2 border-gray-400 bg-gray-600 font-medium
|
||||
text-white
|
||||
hover:bg-gray-700
|
||||
`}
|
||||
>
|
||||
<FaExpandArrowsAlt className="my-auto" /> <div> Loose </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,
|
||||
z: position.z,
|
||||
};
|
||||
})
|
||||
);
|
||||
}}
|
||||
className={`
|
||||
rounded-r-lg px-3 py-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 className="">Tight</div>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div className="flex w-fit p-1">
|
||||
<button
|
||||
type="button"
|
||||
onDoubleClick={(e) => {
|
||||
e.stopPropagation();
|
||||
}}
|
||||
onClick={() => {
|
||||
props.setUnitPositions(
|
||||
props.unitPositions.map((position, idx) => {
|
||||
const [dx, dz] = [-(props.unitPositions[idx].y - props.unitPositions[0].y), props.unitPositions[idx].x - props.unitPositions[0].x];
|
||||
const distance = Math.sqrt(dx ** 2 + dz ** 2);
|
||||
return {
|
||||
x: position.x,
|
||||
y: position.y,
|
||||
z: position.z + 0.1 * distance,
|
||||
};
|
||||
})
|
||||
);
|
||||
}}
|
||||
className={`
|
||||
rounded-l-lg px-3 py-2 text-md flex content-center justify-center
|
||||
gap-2 border-r-2 border-gray-400 bg-gray-600 font-medium
|
||||
text-white
|
||||
hover:bg-gray-700
|
||||
`}
|
||||
>
|
||||
<FaArrowUp className="my-auto" /> <div> Up </div>
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
onDoubleClick={(e) => {
|
||||
e.stopPropagation();
|
||||
}}
|
||||
onClick={() => {
|
||||
props.setUnitPositions(
|
||||
props.unitPositions.map((position, idx) => {
|
||||
const [dx, dz] = [-(props.unitPositions[idx].y - props.unitPositions[0].y), props.unitPositions[idx].x - props.unitPositions[0].x];
|
||||
const distance = Math.sqrt(dx ** 2 + dz ** 2);
|
||||
return {
|
||||
x: position.x,
|
||||
y: position.y,
|
||||
z: position.z - 0.1 * distance,
|
||||
};
|
||||
})
|
||||
);
|
||||
}}
|
||||
className={`
|
||||
rounded-r-lg px-3 py-2 text-md flex content-center justify-center
|
||||
gap-2 bg-gray-600 font-medium text-white
|
||||
hover:bg-gray-700
|
||||
`}
|
||||
>
|
||||
<FaArrowDown className="my-auto" /> <div className="">Down</div>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex justify-between"><div className="text-white">Show units vertical offset</div> <OlToggle onClick={() => {setShowVerticalOffset(!showVerticalOffset)}} toggled={showVerticalOffset} /></div>
|
||||
|
||||
<div
|
||||
data-dragging={dragging}
|
||||
className={`
|
||||
@ -164,8 +229,9 @@ export function FormationCanvas(props: {
|
||||
(((props.unitPositions[idx].y - props.unitPositions[0].y) * 1) / zoom) * FT_TO_PX +
|
||||
dragDelta.y +
|
||||
containerCenter.y,
|
||||
z: props.unitPositions[idx].z,
|
||||
}
|
||||
: { x: 0, y: 0 };
|
||||
: { x: 0, y: 0, z: 0 };
|
||||
|
||||
let disabled = false;
|
||||
let overflowX = null as null | string;
|
||||
@ -213,10 +279,13 @@ export function FormationCanvas(props: {
|
||||
unit={unit}
|
||||
scale={FT_TO_PX}
|
||||
disabled={disabled}
|
||||
onPositionChange={({ x, y }) => {
|
||||
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: {
|
||||
);
|
||||
})}
|
||||
</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>
|
||||
<FaQuestionCircle className={`text-xl text-gray-400`} />
|
||||
<div className="text-gray-400">Double click to reset view</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="absolute left-0 top-2 m-[-0.75rem] h-0">
|
||||
<div
|
||||
className={`
|
||||
@ -243,13 +312,9 @@ export function FormationCanvas(props: {
|
||||
}}
|
||||
>
|
||||
{referenceDistance === 5280 && <div className="translate-y-[-8px]">1 NM</div>}
|
||||
{referenceDistance === 5280 * 2 && (
|
||||
<div
|
||||
className={`translate-y-[-8px]`}
|
||||
>
|
||||
2 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>
|
||||
|
||||
@ -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
|
||||
</button>
|
||||
</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>
|
||||
<OlRangeSlider
|
||||
className="my-auto"
|
||||
value={verticalScale}
|
||||
onChange={(ev) => {
|
||||
setVerticalScale(Number(ev.target.value));
|
||||
}}
|
||||
/>
|
||||
<span className="my-auto min-w-16 text-center text-sm text-white">Up</span>
|
||||
</div>
|
||||
<FormationCanvas
|
||||
units={leader ? [leader, ...wingmen] : []}
|
||||
unitPositions={unitPositions}
|
||||
@ -148,14 +153,13 @@ export function FormationMenu(props: { open: boolean; onClose: () => 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,
|
||||
}));
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@ -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 });
|
||||
});
|
||||
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user