Merge pull request #919 from Pax1601/904-add-unit-loadoutfuelhealth-panel-to-unit-control-panel

904 add unit loadoutfuelhealth panel to unit control panel
This commit is contained in:
Pax1601 2024-08-05 16:07:14 +02:00 committed by GitHub
commit 040476107a
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
13 changed files with 1119 additions and 684 deletions

View File

@ -135,7 +135,6 @@ export interface Offset {
export interface UnitData {
category: string;
categoryDisplayName: string;
ID: number;
alive: boolean;
human: boolean;

View File

@ -601,3 +601,55 @@ export function getFunctionArguments(func) {
if (result === null) result = [];
return result;
}
export function filterBlueprintsByLabel(
blueprints: { [key: string]: UnitBlueprint },
filterString: string
) {
var filteredBlueprints: { [key: string]: UnitBlueprint } = {};
if (blueprints) {
Object.entries(blueprints).forEach(([key, value]) => {
if (
value.enabled &&
(filterString === "" || value.label.includes(filterString))
)
filteredBlueprints[key] = value;
});
}
return filteredBlueprints;
}
export function getUnitsByLabel(filterString: string) {
/* Filter aircrafts, helicopters, and navyunits */
const filteredAircraft = filterBlueprintsByLabel(
getApp()?.getAircraftDatabase()?.blueprints,
filterString
);
const filteredHelicopters = filterBlueprintsByLabel(
getApp()?.getHelicopterDatabase()?.blueprints,
filterString
);
const filteredNavyUnits = filterBlueprintsByLabel(
getApp()?.getNavyUnitDatabase()?.blueprints,
filterString
);
/* Split ground units between air defence and all others */
var filteredAirDefense: { [key: string]: UnitBlueprint } = {};
var filteredGroundUnits: { [key: string]: UnitBlueprint } = {};
Object.keys(getApp()?.getGroundUnitDatabase()?.blueprints ?? {}).forEach(
(key) => {
var blueprint = getApp()?.getGroundUnitDatabase()?.blueprints[key];
var type = blueprint.label;
if (/\bAAA|SAM\b/.test(type) || /\bmanpad|stinger\b/i.test(type)) {
filteredAirDefense[key] = blueprint;
} else {
filteredGroundUnits[key] = blueprint;
}
}
);
filteredAirDefense = filterBlueprintsByLabel(filteredAirDefense, filterString);
filteredGroundUnits = filterBlueprintsByLabel(filteredGroundUnits, filterString);
return [filteredAircraft, filteredHelicopters, filteredAirDefense, filteredGroundUnits, filteredNavyUnits]
}

View File

@ -2,6 +2,8 @@ import React, { ChangeEvent } from "react";
export function OlCheckbox(props: {
checked: boolean;
className?: string;
disabled?: boolean;
onChange: (e: ChangeEvent<HTMLInputElement>) => void;
}) {
return (
@ -10,7 +12,9 @@ export function OlCheckbox(props: {
type="checkbox"
checked={props.checked}
value=""
disabled={props.disabled ?? false}
className={`
${props.className ?? ""}
my-auto h-4 w-4 cursor-pointer rounded border-gray-300 bg-gray-100
text-blue-600
dark:border-gray-600 dark:bg-gray-700 dark:ring-offset-gray-800

View File

@ -1,17 +1,21 @@
import React, { useState, useEffect, useRef } from "react";
import React, { useState, useEffect, useRef, MutableRefObject } from "react";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { IconProp } from "@fortawesome/fontawesome-svg-core";
export function OlDropdown(props: {
className: string;
className?: string;
leftIcon?: IconProp;
rightIcon?: IconProp;
label: string;
label?: string;
children?: JSX.Element | JSX.Element[];
buttonRef?: MutableRefObject<null> | null;
open?: boolean;
}) {
var [open, setOpen] = useState(false);
var [open, setOpen] =
props.open !== undefined ? [props.open, () => {}] : useState(false);
var contentRef = useRef(null);
var buttonRef = useRef(null);
var buttonRef =
props.buttonRef !== undefined ? props.buttonRef : useRef(null);
function setPosition(content: HTMLDivElement, button: HTMLButtonElement) {
/* Reset the position of the content */
@ -19,21 +23,7 @@ export function OlDropdown(props: {
content.style.top = "0px";
content.style.height = "";
/* Get the position and size of the button */
var [bxl, byt, bxr, byb, bw, bh] = [
button.getBoundingClientRect().x,
button.getBoundingClientRect().y,
button.getBoundingClientRect().x + button.clientWidth,
button.getBoundingClientRect().y + button.clientHeight,
button.clientWidth,
button.clientHeight,
];
/* Set the minimum and maximum width to be equal to the button width */
content.style.minWidth = `${bw}px`;
content.style.maxWidth = `${bw}px`;
/* Get the position and size of the content element */
/* Get the position and size of the button and the content elements */
var [cxl, cyt, cxr, cyb, cw, ch] = [
content.getBoundingClientRect().x,
content.getBoundingClientRect().y,
@ -42,6 +32,14 @@ export function OlDropdown(props: {
content.clientWidth,
content.clientHeight,
];
var [bxl, byt, bxr, byb, bbw, bh] = [
button.getBoundingClientRect().x,
button.getBoundingClientRect().y,
button.getBoundingClientRect().x + button.clientWidth,
button.getBoundingClientRect().y + button.clientHeight,
button.clientWidth,
button.clientHeight,
];
/* Limit the maximum height */
if (ch > 400) {
@ -70,10 +68,11 @@ export function OlDropdown(props: {
/* Apply the offset */
content.style.left = `${offsetX}px`;
content.style.top = `${offsetY}px`;
content.style.width = `${bbw}px`;
}
useEffect(() => {
if (contentRef.current && buttonRef.current) {
if (contentRef.current && buttonRef?.current) {
const content = contentRef.current as HTMLDivElement;
const button = buttonRef.current as HTMLButtonElement;
@ -94,53 +93,57 @@ export function OlDropdown(props: {
});
return (
<div className={(props.className ?? "") + "relative"}>
<button
ref={buttonRef}
onClick={() => {
setOpen(!open);
}}
className={`
inline-flex w-full items-center justify-between rounded-lg border
bg-blue-700 px-5 py-2.5 text-center text-sm font-medium text-white
dark:border-gray-700 dark:bg-gray-700 dark:text-gray-100
dark:hover:bg-gray-600
hover:bg-blue-800
`}
type="button"
>
{props.leftIcon && (
<FontAwesomeIcon icon={props.leftIcon} className={`mr-3`} />
)}
<span className="overflow-hidden text-ellipsis text-nowrap">
{props.label}
</span>
<svg
<div
className={props.className ?? ""}
>
{props.buttonRef === undefined && (
<button
ref={buttonRef}
onClick={() => {
setOpen(!open);
}}
className={`
ml-auto ms-3 h-2.5 w-2.5 flex-none transition-transform
data-[open='true']:-scale-y-100
inline-flex w-full items-center justify-between rounded-lg border
bg-blue-700 px-5 py-2.5 text-center text-sm font-medium text-white
dark:border-gray-700 dark:bg-gray-700 dark:text-gray-100
dark:hover:bg-gray-600
hover:bg-blue-800
`}
data-open={open}
xmlns="http://www.w3.org/2000/svg"
fill="none"
viewBox="0 0 10 6"
type="button"
>
<path
stroke="currentColor"
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth="2"
d="m1 1 4 4 4-4"
/>
</svg>
</button>
{props.leftIcon && (
<FontAwesomeIcon icon={props.leftIcon} className={`mr-3`} />
)}
<span className="overflow-hidden text-ellipsis text-nowrap">
{props.label ?? ""}
</span>
<svg
className={`
ml-auto ms-3 h-2.5 w-2.5 flex-none transition-transform
data-[open='true']:-scale-y-100
`}
data-open={open}
xmlns="http://www.w3.org/2000/svg"
fill="none"
viewBox="0 0 10 6"
>
<path
stroke="currentColor"
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth="2"
d="m1 1 4 4 4-4"
/>
</svg>
</button>
)}
<div
ref={contentRef}
data-open={open}
className={`
absolute z-ui-4 divide-y divide-gray-100 overflow-y-scroll
no-scrollbar rounded-lg bg-white p-2 shadow
absolute divide-y divide-gray-100 overflow-y-scroll no-scrollbar
rounded-lg bg-white p-2 shadow
dark:bg-gray-700
data-[open='false']:hidden
`}
@ -165,8 +168,8 @@ export function OlDropdownItem(props) {
onClick={props.onClick ?? (() => {})}
className={`
${props.className ?? ""}
flex cursor-pointer select-none flex-row content-center rounded-md px-4
py-2
flex w-full cursor-pointer select-none flex-row content-center
rounded-md px-4 py-2
dark:hover:bg-gray-600 dark:hover:text-white
hover:bg-gray-100
`}

View File

@ -1,15 +1,17 @@
import { faMultiply, faSearch } from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import React, { ChangeEvent, useId, useRef } from "react";
import React, { useId, useRef } from "react";
export function OlSearchBar(props: {
onChange: (e: ChangeEvent<HTMLInputElement>) => void;
onChange: (e: string) => void;
text: string;
}) {
const searchId = useId();
const inputRef = useRef(null);
function resetSearch() {
inputRef.current && ((inputRef.current as HTMLInputElement).value = "");
props.onChange("");
}
return (
@ -36,7 +38,7 @@ export function OlSearchBar(props: {
type="search"
ref={inputRef}
id={searchId}
onChange={props.onChange}
onChange={(e) => props.onChange(e.currentTarget.value)}
className={`
mb-2 block w-full rounded-full border border-gray-300 bg-gray-50 p-3
ps-10 text-sm text-gray-900
@ -46,6 +48,7 @@ export function OlSearchBar(props: {
focus:border-blue-500 focus:ring-blue-500
`}
placeholder="Search"
value={props.text}
required
/>
<FontAwesomeIcon

View File

@ -70,7 +70,7 @@ export function Menu(props: {
</div>
{props.canBeHidden == true && (
<div className={`
flex h-8 justify-center z-ui-6 pointer-events-auto backdrop-blur-lg
flex h-8 justify-center z-ui-4 pointer-events-auto backdrop-blur-lg
backdrop-grayscale
dark:bg-olympus-800/90
`} onClick={() => setHide(!hide)}>

View File

@ -35,18 +35,17 @@ export function Header() {
const [scrolledLeft, setScrolledLeft] = useState(true);
const [scrolledRight, setScrolledRight] = useState(false);
/* Initialize the "scroll" position of the element */
var scrollRef = useRef(null);
useEffect(() => {
if (scrollRef.current) {
onScroll(scrollRef.current);
}
});
/* Initialize the "scroll" position of the element */
var scrollRef = useRef(null);
useEffect(() => {
if (scrollRef.current) {
onScroll(scrollRef.current);
}
});
function onScroll(el) {
const sl = el.scrollLeft;
const sr =
el.scrollWidth - el.scrollLeft - el.clientWidth;
const sr = el.scrollWidth - el.scrollLeft - el.clientWidth;
sl < 1 && !scrolledLeft && setScrolledLeft(true);
sl > 1 && scrolledLeft && setScrolledLeft(false);
@ -249,29 +248,28 @@ export function Header() {
onClick={() => {}}
tooltip="Activate/deactivate camera plugin"
/>
<OlDropdown label={appState.activeMapSource} className="w-40">
<OlDropdown label={appState.activeMapSource} className="w-60">
{appState.mapSources.map((source) => {
return (
<OlDropdownItem
key={source}
className="w-52"
onClick={() => getApp().getMap().setLayerName(source)}
>
{source}
<div className="truncate">{source}</div>
</OlDropdownItem>
);
})}
</OlDropdown>
</div>
{!scrolledRight && (
<FaChevronRight
className={`
absolute right-0 h-full w-6 rounded-lg px-2 py-3.5
text-gray-200 z-ui-1
dark:bg-olympus-900
`}
/>
)}
<FaChevronRight
className={`
absolute right-0 h-full w-6 rounded-lg px-2 py-3.5
text-gray-200 z-ui-1
dark:bg-olympus-900
`}
/>
)}
</nav>
)}
</EventsConsumer>

View File

@ -11,15 +11,8 @@ import {
} from "@fortawesome/free-solid-svg-icons";
import { EventsConsumer } from "../../eventscontext";
import { StateConsumer } from "../../statecontext";
import { IDLE, SPAWN_UNIT } from "../../constants/constants";
export function SideBar() {
const [mapState, setMapState] = useState(IDLE);
document.addEventListener("mapStateChanged", (ev) => {
setMapState((ev as CustomEvent).detail);
});
return (
<StateConsumer>
{(appState) => (

View File

@ -16,26 +16,10 @@ import {
olButtonsVisibilityNavyunit,
} from "../components/olicons";
import { IDLE, SPAWN_UNIT } from "../../constants/constants";
import { getUnitsByLabel } from "../../other/utils";
library.add(faPlus);
function filterUnits(
blueprints: { [key: string]: UnitBlueprint },
filterString: string
) {
var filteredUnits = {};
if (blueprints) {
Object.entries(blueprints).forEach(([key, value]) => {
if (
value.enabled &&
(filterString === "" || value.label.includes(filterString))
)
filteredUnits[key] = value;
});
}
return filteredUnits;
}
export function SpawnMenu(props: {
open: boolean;
onClose: () => void;
@ -44,36 +28,13 @@ export function SpawnMenu(props: {
var [blueprint, setBlueprint] = useState(null as null | UnitBlueprint);
var [filterString, setFilterString] = useState("");
/* Filter aircrafts, helicopters, and navyunits */
const filteredAircraft = filterUnits(
getApp()?.getAircraftDatabase()?.blueprints,
filterString
);
const filteredHelicopters = filterUnits(
getApp()?.getHelicopterDatabase()?.blueprints,
filterString
);
const filteredNavyUnits = filterUnits(
getApp()?.getNavyUnitDatabase()?.blueprints,
filterString
);
/* Split ground units between air defence and all others */
var filteredAirDefense = {};
var filteredGroundUnits = {};
Object.keys(getApp()?.getGroundUnitDatabase()?.blueprints ?? {}).forEach(
(key) => {
var blueprint = getApp()?.getGroundUnitDatabase()?.blueprints[key];
var type = blueprint.label;
if (/\bAAA|SAM\b/.test(type) || /\bmanpad|stinger\b/i.test(type)) {
filteredAirDefense[key] = blueprint;
} else {
filteredGroundUnits[key] = blueprint;
}
}
);
filteredAirDefense = filterUnits(filteredAirDefense, filterString);
filteredGroundUnits = filterUnits(filteredGroundUnits, filterString);
const [
filteredAircraft,
filteredHelicopters,
filteredAirDefense,
filteredGroundUnits,
filteredNavyUnits,
] = getUnitsByLabel(filterString);
useEffect(() => {
if (!props.open) {
@ -97,22 +58,23 @@ export function SpawnMenu(props: {
<>
{blueprint === null && (
<div className="p-5">
<OlSearchBar onChange={(ev) => setFilterString(ev.target.value)} />
<OlSearchBar
onChange={(value) => setFilterString(value)}
text={filterString}
/>
<OlAccordion title={`Aircraft`}>
<div
className={`
flex max-h-80 flex-col gap-1 overflow-y-scroll no-scrollbar
`}
>
{Object.keys(filteredAircraft).map((key) => {
const blueprint =
getApp().getAircraftDatabase().blueprints[key];
{Object.entries(filteredAircraft).map((entry) => {
return (
<OlUnitEntryList
key={key}
key={entry[0]}
icon={olButtonsVisibilityAircraft}
blueprint={blueprint}
onClick={() => setBlueprint(blueprint)}
blueprint={entry[1]}
onClick={() => setBlueprint(entry[1])}
/>
);
})}
@ -124,15 +86,13 @@ export function SpawnMenu(props: {
flex max-h-80 flex-col gap-1 overflow-y-scroll no-scrollbar
`}
>
{Object.keys(filteredHelicopters).map((key) => {
const blueprint =
getApp().getHelicopterDatabase().blueprints[key];
{Object.entries(filteredHelicopters).map((entry) => {
return (
<OlUnitEntryList
key={key}
key={entry[0]}
icon={olButtonsVisibilityHelicopter}
blueprint={blueprint}
onClick={() => setBlueprint(blueprint)}
blueprint={entry[1]}
onClick={() => setBlueprint(entry[1])}
/>
);
})}
@ -144,15 +104,13 @@ export function SpawnMenu(props: {
flex max-h-80 flex-col gap-1 overflow-y-scroll no-scrollbar
`}
>
{Object.keys(filteredAirDefense).map((key) => {
const blueprint =
getApp().getGroundUnitDatabase().blueprints[key];
{Object.entries(filteredAirDefense).map((entry) => {
return (
<OlUnitEntryList
key={key}
key={entry[0]}
icon={olButtonsVisibilityGroundunitSam}
blueprint={blueprint}
onClick={() => setBlueprint(blueprint)}
blueprint={entry[1]}
onClick={() => setBlueprint(entry[1])}
/>
);
})}
@ -164,15 +122,13 @@ export function SpawnMenu(props: {
flex max-h-80 flex-col gap-1 overflow-y-scroll no-scrollbar
`}
>
{Object.keys(filteredGroundUnits).map((key) => {
const blueprint =
getApp().getGroundUnitDatabase().blueprints[key];
{Object.entries(filteredGroundUnits).map((entry) => {
return (
<OlUnitEntryList
key={key}
key={entry[0]}
icon={olButtonsVisibilityGroundunit}
blueprint={blueprint}
onClick={() => setBlueprint(blueprint)}
blueprint={entry[1]}
onClick={() => setBlueprint(entry[1])}
/>
);
})}
@ -184,15 +140,13 @@ export function SpawnMenu(props: {
flex max-h-80 flex-col gap-1 overflow-y-scroll no-scrollbar
`}
>
{Object.keys(filteredNavyUnits).map((key) => {
const blueprint =
getApp().getNavyUnitDatabase().blueprints[key];
{Object.entries(filteredNavyUnits).map((entry) => {
return (
<OlUnitEntryList
key={key}
key={entry[0]}
icon={olButtonsVisibilityNavyunit}
blueprint={blueprint}
onClick={() => setBlueprint(blueprint)}
blueprint={entry[1]}
onClick={() => setBlueprint(entry[1])}
/>
);
})}

File diff suppressed because it is too large Load Diff

View File

@ -54,6 +54,7 @@ export function UI() {
var [mapSources, setMapSources] = useState([] as string[]);
var [activeMapSource, setActiveMapSource] = useState("");
var [mapBoxSelection, setMapBoxSelection] = useState(false);
var [mapState, setMapState] = useState(IDLE);
document.addEventListener("hiddenTypesChanged", (ev) => {
setMapHiddenTypes({ ...getApp().getMap().getHiddenTypes() });
@ -64,9 +65,10 @@ export function UI() {
});
document.addEventListener("mapStateChanged", (ev) => {
if ((ev as CustomEvent).detail == IDLE) {
if ((ev as CustomEvent).detail === IDLE && mapState !== IDLE)
hideAllMenus();
}
setMapState(String((ev as CustomEvent).detail))
});
document.addEventListener("mapSourceChanged", (ev) => {

View File

@ -443,18 +443,6 @@ export abstract class Unit extends CustomMarker {
*/
abstract getDefaultMarker(): string;
/** Get the category but for display use - for the user. (i.e. has spaces in it)
*
* @returns string
*/
getCategoryLabel() {
return (
GROUND_UNIT_AIR_DEFENCE_REGEX.test(this.getType())
? "Air Defence"
: this.getCategory()
).replace(/([a-z])([A-Z])/g, "$1 $2");
}
/********************** Unit data *************************/
/** This function is called by the units manager to update all the data coming from the backend. It reads the binary raw data using a DataExtractor
*
@ -657,7 +645,6 @@ export abstract class Unit extends CustomMarker {
getData(): UnitData {
return {
category: this.getCategory(),
categoryDisplayName: this.getCategoryLabel(),
ID: this.ID,
alive: this.#alive,
human: this.#human,

View File

@ -66,7 +66,7 @@ module.exports = function (configLocation) {
isActiveAWACS: false,
onOff: true,
followRoads: false,
fuel: 50,
fuel: 10,
desiredSpeed: 300,
desiredSpeedType: 1,
desiredAltitude: 1000,
@ -87,7 +87,19 @@ module.exports = function (configLocation) {
prohibitAirWpn: false,
prohibitJettison: false,
},
ammo: [],
ammo: [{
quantity: 2,
name: "A super nice missile",
guidance: 1,
category: 1,
missileCategory: 1
}, {
quantity: 4,
name: "A less nice missile",
guidance: 1,
category: 1,
missileCategory: 1
}],
contacts: [],
activePath: [{ lat: 37.1, lng: -116.1 }],
isLeader: true,