mirror of
https://github.com/Pax1601/DCSOlympus.git
synced 2025-10-29 16:56:34 +00:00
Added custom formation tool
This commit is contained in:
parent
b282e5d676
commit
10a76c47ff
8
frontend/react/.vscode/launch.json
vendored
8
frontend/react/.vscode/launch.json
vendored
@ -10,7 +10,11 @@
|
||||
"name": "Launch Chrome against localhost",
|
||||
"url": "http://localhost:3000/vite/",
|
||||
"webRoot": "${workspaceFolder}",
|
||||
"preLaunchTask": "npm: dev"
|
||||
}
|
||||
"preLaunchTask": "npm: dev",
|
||||
"resolveSourceMapLocations": [
|
||||
"${workspaceFolder}/**",
|
||||
"!**/node_modules/**"
|
||||
]
|
||||
},
|
||||
]
|
||||
}
|
||||
@ -3,7 +3,7 @@ import { getApp } from "../olympusapp";
|
||||
import { BoxSelect } from "./boxselect";
|
||||
import { Airbase } from "../mission/airbase";
|
||||
import { Unit } from "../unit/unit";
|
||||
import { areaContains, deg2rad, getFunctionArguments, getGroundElevation } from "../other/utils";
|
||||
import { areaContains, deg2rad, getGroundElevation } from "../other/utils";
|
||||
import { TemporaryUnitMarker } from "./markers/temporaryunitmarker";
|
||||
import { ClickableMiniMap } from "./clickableminimap";
|
||||
import {
|
||||
@ -24,7 +24,7 @@ import {
|
||||
import { CoalitionPolygon } from "./coalitionarea/coalitionpolygon";
|
||||
import { MapHiddenTypes, MapOptions } from "../types/types";
|
||||
import { SpawnRequestTable } from "../interfaces";
|
||||
import { ContextAction, ContextActionCallback } from "../unit/contextaction";
|
||||
import { ContextAction } from "../unit/contextaction";
|
||||
|
||||
/* Stylesheets */
|
||||
import "./markers/stylesheets/airbase.css";
|
||||
@ -109,6 +109,7 @@ export class Map extends L.Map {
|
||||
/* Unit spawning */
|
||||
#spawnRequestTable: SpawnRequestTable | null = null;
|
||||
#temporaryMarkers: TemporaryUnitMarker[] = [];
|
||||
#currentSpawnMarker: TemporaryUnitMarker | null = null;
|
||||
|
||||
/**
|
||||
*
|
||||
@ -349,6 +350,8 @@ export class Map extends L.Map {
|
||||
|
||||
/* Operations to perform when leaving a state */
|
||||
if (this.#state === COALITIONAREA_DRAW_POLYGON || this.#state === COALITIONAREA_DRAW_CIRCLE) this.getSelectedCoalitionArea()?.setEditing(false);
|
||||
this.#currentSpawnMarker?.removeFrom(this);
|
||||
this.#currentSpawnMarker = null;
|
||||
|
||||
this.#state = state;
|
||||
|
||||
@ -361,6 +364,8 @@ export class Map extends L.Map {
|
||||
this.#spawnRequestTable = options?.spawnRequestTable ?? null;
|
||||
console.log(`Spawn request table:`);
|
||||
console.log(this.#spawnRequestTable);
|
||||
this.#currentSpawnMarker = new TemporaryUnitMarker(new L.LatLng(0, 0), this.#spawnRequestTable?.unit.unitType ?? "", this.#spawnRequestTable?.coalition ?? "neutral")
|
||||
this.#currentSpawnMarker.addTo(this);
|
||||
} else if (this.#state === CONTEXT_ACTION) {
|
||||
this.deselectAllCoalitionAreas();
|
||||
this.#contextAction = options?.contextAction ?? null;
|
||||
@ -840,7 +845,7 @@ export class Map extends L.Map {
|
||||
/* Execute the short click action */
|
||||
if (this.#state === IDLE) {
|
||||
} else if (this.#state === SPAWN_UNIT) {
|
||||
if (this.#spawnRequestTable !== null) {
|
||||
if (e.originalEvent.button != 2 && this.#spawnRequestTable !== null) {
|
||||
this.#spawnRequestTable.unit.location = pressLocation;
|
||||
getApp()
|
||||
.getUnitsManager()
|
||||
@ -920,6 +925,10 @@ export class Map extends L.Map {
|
||||
this.#lastMousePosition.x = e.originalEvent.x;
|
||||
this.#lastMousePosition.y = e.originalEvent.y;
|
||||
this.#lastMouseCoordinates = e.latlng;
|
||||
|
||||
if (this.#currentSpawnMarker) {
|
||||
this.#currentSpawnMarker.setLatLng(e.latlng);
|
||||
}
|
||||
}
|
||||
|
||||
#onMapMove(e: any) {
|
||||
|
||||
@ -503,7 +503,7 @@ export function filterBlueprintsByLabel(blueprints: { [key: string]: UnitBluepri
|
||||
var filteredBlueprints: { [key: string]: UnitBlueprint } = {};
|
||||
if (blueprints) {
|
||||
Object.entries(blueprints).forEach(([key, value]) => {
|
||||
if (value.enabled && (filterString === "" || value.label.includes(filterString))) filteredBlueprints[key] = value;
|
||||
if (value.enabled && (filterString === "" || value.label.toLowerCase().includes(filterString.toLowerCase()))) filteredBlueprints[key] = value;
|
||||
});
|
||||
}
|
||||
return filteredBlueprints;
|
||||
|
||||
@ -59,7 +59,7 @@ export class ServerManager {
|
||||
/* If we are forcing the request we don't care if one already exists, just send it. CAREFUL: this makes sense only for low frequency requests, like refreshes, when we
|
||||
are reasonably confident any previous request will be done before we make a new one on the same URI. */
|
||||
if (uri in this.#requests && this.#requests[uri].readyState !== 4 && !force) {
|
||||
console.warn(`GET request on ${uri} URI still pending, skipping...`);
|
||||
//console.warn(`GET request on ${uri} URI still pending, skipping...`);
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
68
frontend/react/src/ui/libs/useDrag.ts
Normal file
68
frontend/react/src/ui/libs/useDrag.ts
Normal file
@ -0,0 +1,68 @@
|
||||
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 [count, setCount] = useState(0)
|
||||
|
||||
if (count !== props.count) {
|
||||
setCount(props.count)
|
||||
setFinalPosition({ x: props.initialPosition.x, y: props.initialPosition.y })
|
||||
}
|
||||
|
||||
const handleMouseUp = (evt) => {
|
||||
evt.preventDefault();
|
||||
|
||||
setIsDragging(false);
|
||||
};
|
||||
|
||||
const handleMouseDown = (evt) => {
|
||||
evt.preventDefault();
|
||||
|
||||
const { current: draggableElement } = props.ref;
|
||||
|
||||
if (!draggableElement) {
|
||||
return;
|
||||
}
|
||||
|
||||
setIsDragging(true);
|
||||
};
|
||||
|
||||
const handleMouseMove = useCallback(
|
||||
(evt) => {
|
||||
const { current: draggableElement } = props.ref;
|
||||
|
||||
if (!isDragging || !draggableElement) return;
|
||||
|
||||
evt.preventDefault();
|
||||
|
||||
const parentRect = draggableElement.parentElement.getBoundingClientRect();
|
||||
const rect = draggableElement.getBoundingClientRect();
|
||||
|
||||
const [width, height] = [rect.width, rect.height];
|
||||
const [mouseX, mouseY] = [evt.clientX, evt.clientY];
|
||||
const [parentTop, parentLeft, parentWidth, parentHeight] = [parentRect.top, parentRect.left, parentRect.width, parentRect.height];
|
||||
|
||||
setFinalPosition({
|
||||
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]
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
document.addEventListener("mousemove", handleMouseMove);
|
||||
document.addEventListener("mouseup", handleMouseUp);
|
||||
|
||||
return () => {
|
||||
document.removeEventListener("mousemove", handleMouseMove);
|
||||
document.removeEventListener("mouseup", handleMouseUp);
|
||||
};
|
||||
}, [handleMouseMove]);
|
||||
|
||||
return {
|
||||
position: finalPosition,
|
||||
handleMouseDown
|
||||
};
|
||||
};
|
||||
@ -1,15 +1,10 @@
|
||||
import React, { useState } from "react";
|
||||
import { Menu } from "./components/menu";
|
||||
import { OlCheckbox } from "../components/olcheckbox";
|
||||
import { OlRangeSlider } from "../components/olrangeslider";
|
||||
import { OlNumberInput } from "../components/olnumberinput";
|
||||
import { Coalition, MapOptions } from "../../types/types";
|
||||
import { getApp } from "../../olympusapp";
|
||||
import { Coalition } from "../../types/types";
|
||||
import { Airbase } from "../../mission/airbase";
|
||||
import { FaArrowLeft, FaCompass } from "react-icons/fa6";
|
||||
import { getUnitsByLabel } from "../../other/utils";
|
||||
import { UnitBlueprint } from "../../interfaces";
|
||||
import { IDLE } from "../../constants/constants";
|
||||
import { OlSearchBar } from "../components/olsearchbar";
|
||||
import { OlAccordion } from "../components/olaccordion";
|
||||
import { OlUnitEntryList } from "../components/olunitlistentry";
|
||||
@ -20,7 +15,7 @@ export function AirbaseMenu(props: { open: boolean; onClose: () => void; airbase
|
||||
const [blueprint, setBlueprint] = useState(null as null | UnitBlueprint);
|
||||
const [filterString, setFilterString] = useState("");
|
||||
|
||||
const [filteredAircraft, filteredHelicopters, filteredAirDefense, filteredGroundUnits, filteredNavyUnits] = getUnitsByLabel(filterString);
|
||||
const [filteredAircraft, filteredHelicopters, _1, _2, _3] = getUnitsByLabel(filterString);
|
||||
|
||||
return (
|
||||
<Menu title={props.airbase?.getName() ?? "No airbase selected"} open={props.open} onClose={props.onClose} showBackButton={false} canBeHidden={true}>
|
||||
|
||||
@ -29,13 +29,13 @@ export function Menu(props: {
|
||||
>
|
||||
<div
|
||||
data-hide={hide}
|
||||
data-canBeHidden={props.canBeHidden}
|
||||
data-canbehidden={props.canBeHidden}
|
||||
className={`
|
||||
pointer-events-auto h-[calc(100vh-58px)] overflow-y-auto
|
||||
overflow-x-hidden backdrop-blur-lg backdrop-grayscale
|
||||
transition-transform no-scrollbar
|
||||
dark:bg-olympus-800/90
|
||||
data-[canBeHidden='true']:h-[calc(100vh-58px-2rem)]
|
||||
data-[canbehidden='true']:h-[calc(100vh-58px-2rem)]
|
||||
data-[hide='true']:translate-y-[calc(100vh-58px)]
|
||||
`}
|
||||
>
|
||||
|
||||
@ -2,17 +2,18 @@ import React, { useEffect, useState } from "react";
|
||||
import { getApp } from "../../olympusapp";
|
||||
import { IconDefinition } from "@fortawesome/free-solid-svg-icons";
|
||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||
|
||||
export function ControlsPanel(props: {}) {
|
||||
const [controls, setControls] = useState(
|
||||
[] as {
|
||||
null as {
|
||||
actions: (string | number | IconDefinition)[];
|
||||
target: IconDefinition;
|
||||
text: string;
|
||||
}[]
|
||||
}[] | null
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
if (getApp() && controls.length === 0) {
|
||||
if (getApp() && controls === null) {
|
||||
setControls(getApp().getMap().getCurrentControls());
|
||||
}
|
||||
});
|
||||
@ -30,7 +31,7 @@ export function ControlsPanel(props: {}) {
|
||||
justify-between gap-1 p-3 text-sm
|
||||
`}
|
||||
>
|
||||
{controls.map((control) => {
|
||||
{controls?.map((control) => {
|
||||
return (
|
||||
<div
|
||||
key={control.text}
|
||||
|
||||
194
frontend/react/src/ui/panels/formationmenu.tsx
Normal file
194
frontend/react/src/ui/panels/formationmenu.tsx
Normal file
@ -0,0 +1,194 @@
|
||||
import React, { 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";
|
||||
|
||||
export function FormationMenu(props: {
|
||||
open: boolean;
|
||||
onClose: () => void;
|
||||
leader: Unit | null;
|
||||
wingmen: Unit[] | null;
|
||||
children?: JSX.Element | JSX.Element[];
|
||||
}) {
|
||||
const [formationType, setFormationType] = useState("echelon-lh");
|
||||
const [horizontalStep, setHorizontalStep] = useState(50);
|
||||
const [verticalStep, setVerticalStep] = useState(15);
|
||||
const [count, setCount] = useState(0);
|
||||
|
||||
let units = Array(128).fill(null) as (Unit | null)[];
|
||||
units[0] = props.leader;
|
||||
props.wingmen?.forEach((unit, idx) => (units[idx + 1] = unit));
|
||||
|
||||
const containerRef = useRef(null);
|
||||
const silhouetteReferences = units.map((unit) => useRef(null));
|
||||
const silhouetteHandles = units.map((unit, idx) => {
|
||||
let offset = computeFormationOffset(formationType, idx);
|
||||
let center = { x: 0, y: 0 };
|
||||
if (containerRef.current) {
|
||||
center.x = (containerRef.current as HTMLDivElement).getBoundingClientRect().width / 2;
|
||||
center.y = 150;
|
||||
}
|
||||
return useDrag({
|
||||
ref: silhouetteReferences[idx],
|
||||
initialPosition: { x: offset.z + center.x, y: -offset.x + center.y },
|
||||
count: count
|
||||
});
|
||||
});
|
||||
|
||||
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",
|
||||
};
|
||||
|
||||
return (
|
||||
<Menu title="Formation menu" open={props.open} showBackButton={false} onClose={props.onClose}>
|
||||
<div className="flex h-full flex-col gap-4 p-4">
|
||||
<span className="text-white">Formation type presets</span>
|
||||
<OlDropdown label={formationTypes[formationType]}>
|
||||
{Object.keys(formationTypes).map((optionFormationType) => {
|
||||
return (
|
||||
<OlDropdownItem
|
||||
onClick={() => {
|
||||
setCount(count + 1);
|
||||
setFormationType(optionFormationType);
|
||||
}}
|
||||
>
|
||||
{formationTypes[optionFormationType]}
|
||||
</OlDropdownItem>
|
||||
);
|
||||
})}
|
||||
</OlDropdown>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => {
|
||||
let center = { x: 0, y: 0 };
|
||||
|
||||
if (containerRef.current) {
|
||||
center.x = (containerRef.current as HTMLDivElement).getBoundingClientRect().width / 2;
|
||||
center.y = 150;
|
||||
}
|
||||
|
||||
units
|
||||
.filter((unit) => unit !== null)
|
||||
.forEach((unit, idx) => {
|
||||
if (idx != 0) {
|
||||
const ID = units[0].ID;
|
||||
const offset = {
|
||||
x: -(silhouetteHandles[idx].position.y - silhouetteHandles[0].position.y),
|
||||
y: 0,
|
||||
z: silhouetteHandles[idx].position.x - silhouetteHandles[0].position.x
|
||||
}
|
||||
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
|
||||
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
|
||||
`}
|
||||
>
|
||||
Apply
|
||||
</button>
|
||||
<div
|
||||
className={`
|
||||
relative h-full w-full rounded-md border-[1px] border-white/20
|
||||
bg-white/10
|
||||
`}
|
||||
ref={containerRef}
|
||||
>
|
||||
<>
|
||||
{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={silhouetteHandles[idx].handleMouseDown}
|
||||
>
|
||||
<img
|
||||
className={`
|
||||
h-10 min-h-10 w-10 min-w-10 translate-x-[-50%]
|
||||
translate-y-[-50%] rotate-90 opacity-80 invert
|
||||
`}
|
||||
src="public\images\units\general1.png"
|
||||
></img>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</>
|
||||
</div>
|
||||
</div>
|
||||
</Menu>
|
||||
);
|
||||
}
|
||||
|
||||
function computeFormationOffset(formation, idx) {
|
||||
let offset = { x: 0, y: 0, z: 0 };
|
||||
if (formation === "trail") {
|
||||
offset.x = -50 * idx;
|
||||
offset.y = -30 * idx;
|
||||
offset.z = 0;
|
||||
} else if (formation === "echelon-lh") {
|
||||
offset.x = -50 * idx;
|
||||
offset.y = -10 * idx;
|
||||
offset.z = -50 * idx;
|
||||
} else if (formation === "echelon-rh") {
|
||||
offset.x = -50 * idx;
|
||||
offset.y = -10 * idx;
|
||||
offset.z = 50 * idx;
|
||||
} else if (formation === "line-abreast-lh") {
|
||||
offset.x = 0;
|
||||
offset.y = 0;
|
||||
offset.z = -50 * idx;
|
||||
} else if (formation === "line-abreast-rh") {
|
||||
offset.x = 0;
|
||||
offset.y = 0;
|
||||
offset.z = 50 * idx;
|
||||
} else if (formation === "front") {
|
||||
offset.x = 100 * idx;
|
||||
offset.y = 0;
|
||||
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, 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++;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return offset;
|
||||
}
|
||||
@ -485,7 +485,7 @@ export function UnitControlMenu(props: { open: boolean; onClose: () => void }) {
|
||||
return Object.keys(unitOccurences[coalition]).map((name, idx) => {
|
||||
return (
|
||||
<div
|
||||
key={idx}
|
||||
key={`coalition-${idx}`}
|
||||
data-coalition={coalition}
|
||||
className={`
|
||||
flex content-center justify-between border-l-4
|
||||
@ -1344,9 +1344,9 @@ export function UnitControlMenu(props: { open: boolean; onClose: () => void }) {
|
||||
{/* ============== Payload section START ============== */}
|
||||
{!selectedUnits[0].isTanker() &&
|
||||
!selectedUnits[0].isAWACS() &&
|
||||
selectedUnits[0].getAmmo().map((ammo) => {
|
||||
selectedUnits[0].getAmmo().map((ammo, idx) => {
|
||||
return (
|
||||
<div className="flex content-center gap-2">
|
||||
<div className="flex content-center gap-2" key={idx}>
|
||||
<div
|
||||
className={`
|
||||
my-auto w-fit rounded-full px-2 py-0.5
|
||||
|
||||
@ -24,6 +24,8 @@ import { AirbaseMenu } from "./panels/airbasemenu";
|
||||
import { Airbase } from "../mission/airbase";
|
||||
import { RadioMenu } from "./panels/radiomenu";
|
||||
import { AudioMenu } from "./panels/audiomenu";
|
||||
import { FormationMenu } from "./panels/formationmenu";
|
||||
import { Unit } from "../unit/unit";
|
||||
|
||||
export type OlympusUIState = {
|
||||
mainMenuVisible: boolean;
|
||||
@ -48,6 +50,7 @@ export function UI() {
|
||||
const [audioMenuVisible, setAudioMenuVisible] = useState(false);
|
||||
const [optionsMenuVisible, setOptionsMenuVisible] = useState(false);
|
||||
const [airbaseMenuVisible, setAirbaseMenuVisible] = useState(false);
|
||||
const [formationMenuVisible, setFormationMenuVisible] = useState(false);
|
||||
const [mapHiddenTypes, setMapHiddenTypes] = useState(MAP_HIDDEN_TYPES_DEFAULTS);
|
||||
const [mapOptions, setMapOptions] = useState(MAP_OPTIONS_DEFAULTS);
|
||||
const [checkingPassword, setCheckingPassword] = useState(false);
|
||||
@ -57,6 +60,8 @@ export function UI() {
|
||||
const [activeMapSource, setActiveMapSource] = useState("");
|
||||
const [mapState, setMapState] = useState(IDLE);
|
||||
const [airbase, setAirbase] = useState(null as null | Airbase);
|
||||
const [formationLeader, setFormationLeader] = useState(null as null | Unit);
|
||||
const [formationWingmen, setFormationWingmen] = useState(null as null | Unit[]);
|
||||
|
||||
useEffect(() => {
|
||||
document.addEventListener("hiddenTypesChanged", (ev) => {
|
||||
@ -91,6 +96,12 @@ export function UI() {
|
||||
setAirbase((ev as CustomEvent).detail);
|
||||
setAirbaseMenuVisible(true);
|
||||
});
|
||||
|
||||
document.addEventListener("createFormation", (ev) => {
|
||||
setFormationMenuVisible(true);
|
||||
setFormationLeader((ev as CustomEvent).detail.leader);
|
||||
setFormationWingmen((ev as CustomEvent).detail.wingmen);
|
||||
});
|
||||
}, []);
|
||||
|
||||
function hideAllMenus() {
|
||||
@ -103,6 +114,7 @@ export function UI() {
|
||||
setAirbaseMenuVisible(false);
|
||||
setRadioMenuVisible(false);
|
||||
setAudioMenuVisible(false);
|
||||
setFormationMenuVisible(false);
|
||||
}
|
||||
|
||||
function checkPassword(password: string) {
|
||||
@ -243,6 +255,7 @@ export function UI() {
|
||||
<AirbaseMenu open={airbaseMenuVisible} onClose={() => setAirbaseMenuVisible(false)} airbase={airbase}/>
|
||||
<RadioMenu open={radioMenuVisible} onClose={() => setRadioMenuVisible(false)} />
|
||||
<AudioMenu open={audioMenuVisible} onClose={() => setAudioMenuVisible(false)} />
|
||||
<FormationMenu open={formationMenuVisible} leader={formationLeader} wingmen={formationWingmen} onClose={() => setFormationMenuVisible(false)} />
|
||||
|
||||
<MiniMapPanel />
|
||||
<ControlsPanel />
|
||||
|
||||
@ -849,7 +849,7 @@ export abstract class Unit extends CustomMarker {
|
||||
if (targetPosition) getApp().getUnitsManager().addDestination(targetPosition, false, 0, units);
|
||||
}
|
||||
);
|
||||
|
||||
|
||||
contextActionSet.addDefaultContextAction(this, "default", "Set destination", "", faRoute, null, (units: Unit[], targetUnit, targetPosition) => {
|
||||
if (targetPosition) {
|
||||
getApp().getUnitsManager().clearDestinations(units);
|
||||
@ -1294,48 +1294,6 @@ export abstract class Unit extends CustomMarker {
|
||||
this.#redrawMarker();
|
||||
}
|
||||
|
||||
showFollowOptions(units: Unit[]) {
|
||||
var contextActionSet = new ContextActionSet();
|
||||
|
||||
// TODO FIX
|
||||
contextActionSet.addContextAction(this, "trail", "Trail", "Follow unit in trail formation", olButtonsContextTrail, null, () =>
|
||||
this.applyFollowOptions("trail", units)
|
||||
);
|
||||
contextActionSet.addContextAction(this, "echelon-lh", "Echelon (LH)", "Follow unit in echelon left formation", olButtonsContextEchelonLh, null, () =>
|
||||
this.applyFollowOptions("echelon-lh", units)
|
||||
);
|
||||
contextActionSet.addContextAction(this, "echelon-rh", "Echelon (RH)", "Follow unit in echelon right formation", olButtonsContextEchelonRh, null, () =>
|
||||
this.applyFollowOptions("echelon-rh", units)
|
||||
);
|
||||
contextActionSet.addContextAction(
|
||||
this,
|
||||
"line-abreast-lh",
|
||||
"Line abreast (LH)",
|
||||
"Follow unit in line abreast left formation",
|
||||
olButtonsContextLineAbreast,
|
||||
null,
|
||||
() => this.applyFollowOptions("line-abreast-lh", units)
|
||||
);
|
||||
contextActionSet.addContextAction(
|
||||
this,
|
||||
"line-abreast-rh",
|
||||
"Line abreast (RH)",
|
||||
"Follow unit in line abreast right formation",
|
||||
olButtonsContextLineAbreast,
|
||||
null,
|
||||
() => this.applyFollowOptions("line-abreast-rh", units)
|
||||
);
|
||||
contextActionSet.addContextAction(this, "front", "Front", "Fly in front of unit", olButtonsContextFront, null, () =>
|
||||
this.applyFollowOptions("front", units)
|
||||
);
|
||||
contextActionSet.addContextAction(this, "diamond", "Diamond", "Follow unit in diamond formation", olButtonsContextDiamond, null, () =>
|
||||
this.applyFollowOptions("diamond", units)
|
||||
);
|
||||
contextActionSet.addContextAction(this, "custom", "Custom", "Set a custom formation position", faExclamation, null, () =>
|
||||
this.applyFollowOptions("custom", units)
|
||||
);
|
||||
}
|
||||
|
||||
applyFollowOptions(formation: string, units: Unit[]) {
|
||||
if (formation === "custom") {
|
||||
document.getElementById("custom-formation-dialog")?.classList.remove("hide");
|
||||
@ -1887,7 +1845,16 @@ export abstract class AirUnit extends Unit {
|
||||
olButtonsContextFollow,
|
||||
"unit",
|
||||
(units: Unit[], targetUnit: Unit | null, _) => {
|
||||
if (targetUnit) targetUnit.showFollowOptions(units);
|
||||
if (targetUnit) {
|
||||
document.dispatchEvent(
|
||||
new CustomEvent("createFormation", {
|
||||
detail: {
|
||||
leader: targetUnit,
|
||||
wingmen: units.filter((unit) => unit !== targetUnit),
|
||||
},
|
||||
})
|
||||
);
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
@ -2243,7 +2210,7 @@ export class NavyUnit extends Unit {
|
||||
this.#carrier.setLatLng(this.getPosition());
|
||||
this.#carrier.setHeading(this.getHeading());
|
||||
this.#carrier.updateSize();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
onAdd(map: Map): this {
|
||||
@ -2258,8 +2225,7 @@ export class NavyUnit extends Unit {
|
||||
|
||||
onRemove(map: Map): this {
|
||||
super.onRemove(map);
|
||||
if (this.#carrier)
|
||||
this.#carrier.removeFrom(getApp().getMap())
|
||||
if (this.#carrier) this.#carrier.removeFrom(getApp().getMap());
|
||||
return this;
|
||||
}
|
||||
}
|
||||
|
||||
@ -1463,9 +1463,9 @@ export class UnitsManager {
|
||||
getApp()
|
||||
.getServerManager()
|
||||
.cloneUnits(units, false, spawnPoints, (res: any) => {
|
||||
if (res.commandHash !== undefined) {
|
||||
if (res !== undefined) {
|
||||
markers.forEach((marker: TemporaryUnitMarker) => {
|
||||
marker.setCommandHash(res.commandHash);
|
||||
marker.setCommandHash(res);
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Loading…
x
Reference in New Issue
Block a user