Added custom formation tool

This commit is contained in:
Davide Passoni
2024-10-09 18:30:13 +02:00
parent b282e5d676
commit 10a76c47ff
17 changed files with 322 additions and 72 deletions

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

View File

@@ -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}>

View File

@@ -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)]
`}
>

View File

@@ -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}

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

View File

@@ -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

View File

@@ -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 />