Large rework of events and state

This commit is contained in:
Davide Passoni
2024-11-06 17:32:31 +01:00
parent 7f5873b5b8
commit 636803b2ac
57 changed files with 110475 additions and 119636 deletions

View File

@@ -41,7 +41,7 @@ export function OlAccordion(props: { title: string; open: boolean, onClick: () =
>
<span className="font-normal">{props.title}</span>
<svg
data-open={open}
data-open={props.open}
className={`
h-3 w-3 shrink-0 -rotate-180 transition-transform
dark:text-olympus-50

View File

@@ -7,7 +7,7 @@ export function ErrorCallout(props: { title?: string; description?: string }) {
return (
<div
className={`
flex w-fit w-full flex-row gap-2 rounded-md border-[1px] border-red-800
flex w-full flex-row gap-2 rounded-md border-[1px] border-red-800
bg-gray-300 p-4 text-red-700
dark:bg-gray-800 dark:text-red-500
`}
@@ -19,13 +19,9 @@ export function ErrorCallout(props: { title?: string; description?: string }) {
`}
>
{props.title}
<div
className={`
flex whitespace-pre-line text-xs font-medium text-red-500
`}
>
{props.description}
</div>
<div className={`
flex whitespace-pre-line text-xs font-medium text-red-500
`}>{props.description}</div>
</div>
</div>
);
@@ -36,7 +32,7 @@ export function InfoCallout(props: { title?: string; description?: string }) {
return (
<div
className={`
flex w-fit w-full flex-row gap-2 rounded-md border-[1px] border-blue-800
flex w-full flex-row gap-2 rounded-md border-[1px] border-blue-800
bg-gray-300 p-4 text-blue-400
dark:bg-gray-800 dark:text-blue-400
`}
@@ -48,15 +44,9 @@ export function InfoCallout(props: { title?: string; description?: string }) {
`}
>
{props.title}
{props.description && (
<div
className={`
flex whitespace-pre-line text-xs font-medium
`}
>
{props.description}
</div>
)}
{props.description && <div className={`
flex whitespace-pre-line text-xs font-medium
`}>{props.description}</div>}
</div>
</div>
);
@@ -67,7 +57,7 @@ export function CommandCallout(props: { coalition?: string }) {
return (
<div
className={`
flex w-fit w-full flex-row gap-2 rounded-md border-[1px] border-gray-700
flex w-full flex-row gap-2 rounded-md border-[1px] border-gray-700
bg-gray-300 p-4 text-gray-400
dark:bg-gray-800 dark:text-gray-400
`}

View File

@@ -12,7 +12,6 @@ import { SelectionClearedEvent } from "../../events";
export function MapContextMenu(props: {}) {
const [open, setOpen] = useState(false);
const [contextActionsSet, setContextActionsSet] = useState(new ContextActionSet());
const [activeContextAction, setActiveContextAction] = useState(null as null | ContextAction);
const [xPosition, setXPosition] = useState(0);
const [yPosition, setYPosition] = useState(0);
const [latLng, setLatLng] = useState(null as null | LatLng);
@@ -107,9 +106,7 @@ export function MapContextMenu(props: {}) {
<>
<div
ref={contentRef}
className={`
absolute flex min-w-80 gap-2 rounded-md bg-olympus-600
`}
className={`absolute flex min-w-80 gap-2 rounded-md bg-olympus-600`}
>
<div
className={`

View File

@@ -14,6 +14,7 @@ export function LoginModal(props: {
onContinue: (username: string) => void;
onBack: () => void;
}) {
// TODO: add warning if not in secure context and some features are disabled
const [password, setPassword] = useState("");
const [displayName, setDisplayName] = useState("Game Master");

View File

@@ -6,11 +6,13 @@ import { AppStateChangedEvent } from "../../events";
export function ControlsPanel(props: {}) {
const [controls, setControls] = useState(
null as {
actions: (string | number | IconDefinition)[];
target: IconDefinition;
text: string;
}[] | null
null as
| {
actions: (string | number | IconDefinition)[];
target: IconDefinition;
text: string;
}[]
| null
);
useEffect(() => {
@@ -20,9 +22,7 @@ export function ControlsPanel(props: {}) {
});
useEffect(() => {
AppStateChangedEvent.on(() => {
setControls(getApp().getMap().getCurrentControls());
});
AppStateChangedEvent.on(() => setControls(getApp().getMap().getCurrentControls()));
}, []);
return (
@@ -53,9 +53,14 @@ export function ControlsPanel(props: {}) {
return (
<div key={idx} className="flex gap-1">
<div>
{typeof action === "string" || typeof action === "number" ? action : <FontAwesomeIcon icon={action} className={`
my-auto ml-auto
`} />}
{typeof action === "string" || typeof action === "number" ? (
action
) : (
<FontAwesomeIcon
icon={action}
className={`my-auto ml-auto`}
/>
)}
</div>
{idx < control.actions.length - 1 && typeof control.actions[idx + 1] === "string" && <div>+</div>}
{idx < control.actions.length - 1 && typeof control.actions[idx + 1] === "number" && <div>x</div>}

View File

@@ -1,6 +1,6 @@
import React, { useEffect, useState } from "react";
import { Menu } from "./components/menu";
import { FaQuestionCircle, FaTrash } from "react-icons/fa";
import { FaTrash } from "react-icons/fa";
import { getApp } from "../../olympusapp";
import { OlStateButton } from "../components/olstatebutton";
import { faDrawPolygon } from "@fortawesome/free-solid-svg-icons";
@@ -12,11 +12,13 @@ import { OlCheckbox } from "../components/olcheckbox";
import { Coalition } from "../../types/types";
import { OlRangeSlider } from "../components/olrangeslider";
import { CoalitionCircle } from "../../map/coalitionarea/coalitioncircle";
import { DrawSubState, NO_SUBSTATE, OlympusState } from "../../constants/constants";
import { StateConsumer } from "../../statecontext";
import { CoalitionAreaSelectedEvent } from "../../events";
import { DrawSubState, NO_SUBSTATE, OlympusState, OlympusSubState } from "../../constants/constants";
import { AppStateChangedEvent, CoalitionAreaSelectedEvent } from "../../events";
import { UnitBlueprint } from "../../interfaces";
export function DrawingMenu(props: { open: boolean; onClose: () => void }) {
const [appState, setAppState] = useState(OlympusState.NOT_INITIALIZED);
const [appSubState, setAppSubState] = useState(NO_SUBSTATE as OlympusSubState);
const [activeCoalitionArea, setActiveCoalitionArea] = useState(null as null | CoalitionPolygon | CoalitionCircle);
const [areaCoalition, setAreaCoalition] = useState("blue" as Coalition);
const [IADSDensity, setIADSDensity] = useState(50);
@@ -27,6 +29,29 @@ export function DrawingMenu(props: { open: boolean; onClose: () => void }) {
const [erasSelection, setErasSelection] = useState({});
const [rangesSelection, setRangesSelection] = useState({});
useEffect(() => {
AppStateChangedEvent.on((state, subState) => {
setAppState(state);
setAppSubState(subState);
});
}, []);
/* Get all the unique types and eras for groundunits */
/* TODO move in effect */
const blueprints = getApp()?.getUnitsManager().getDatabase().getBlueprints();
let types: string[] = [];
let eras: string[] = [];
if (blueprints) {
types = blueprints
.filter((blueprint) => blueprint.category === "groundunit")
.map((blueprint) => blueprint.type)
.filter((type) => type !== undefined);
eras = blueprints
.filter((blueprint) => blueprint.category === "groundunit")
.map((blueprint) => blueprint.era)
.filter((era) => era !== undefined);
}
useEffect(() => {
if (getApp()) {
// TODO
@@ -49,258 +74,248 @@ export function DrawingMenu(props: { open: boolean; onClose: () => void }) {
}, []);
return (
<StateConsumer>
{(appState) => (
<Menu
open={props.open}
title="Draw"
onClose={props.onClose}
canBeHidden={true}
showBackButton={activeCoalitionArea !== null}
onBack={() => {
getApp().setState(OlympusState.DRAW, DrawSubState.NO_SUBSTATE);
}}
>
<>
{appState.appState === OlympusState.DRAW && appState.appSubState !== DrawSubState.EDIT && (
<div className="flex flex-col gap-2 p-6 text-sm text-gray-400">
<OlStateButton
className="!w-full"
icon={faDrawPolygon}
tooltip={"Add a new polygon"}
checked={appState.appSubState === DrawSubState.DRAW_POLYGON}
onClick={() => {
if (appState.appSubState === DrawSubState.DRAW_POLYGON) getApp().setState(OlympusState.DRAW, DrawSubState.EDIT);
else getApp().setState(OlympusState.DRAW, DrawSubState.DRAW_POLYGON);
}}
>
<div className="text-sm">Add polygon</div>
</OlStateButton>
<OlStateButton
className="!w-full"
icon={faCircle}
tooltip={"Add a new circle"}
checked={appState.appSubState === DrawSubState.DRAW_CIRCLE}
onClick={() => {
if (appState.appSubState === DrawSubState.DRAW_CIRCLE) getApp().setState(OlympusState.DRAW, DrawSubState.EDIT);
else getApp().setState(OlympusState.DRAW, DrawSubState.DRAW_CIRCLE);
}}
>
<div className="text-sm">Add circle</div>
</OlStateButton>
</div>
)}
</>
<div>
{activeCoalitionArea !== null && appState.appSubState === DrawSubState.EDIT && (
<div className={`flex flex-col gap-4 py-4`}>
<div
className={`
flex flex-col content-center justify-start gap-2 px-6
text-gray-200
`}
>
<div className="my-auto flex justify-between text-md">
Area label
<div
className="rounded-md bg-red-800 p-2"
onClick={() => {
getApp().getMap().deleteCoalitionArea(activeCoalitionArea);
setActiveCoalitionArea(null);
}}
>
<FaTrash className={`text-gray-50`}></FaTrash>
</div>
</div>
<input
type="text"
className={`
block w-full flex-grow rounded-lg border border-gray-300
bg-gray-50 p-2.5 text-sm text-gray-900
dark:border-gray-600 dark:bg-gray-700 dark:text-white
dark:placeholder-gray-400 dark:focus:border-blue-500
dark:focus:ring-blue-500
focus:border-blue-500 focus:ring-blue-500
`}
placeholder={activeCoalitionArea.getLabelText()}
onInput={(ev) => activeCoalitionArea.setLabelText(ev.currentTarget.value)}
></input>
</div>
<div
className={`
flex content-center justify-start gap-4 px-6 text-gray-200
`}
>
<div className="my-auto text-md">Coalition: </div>
<OlCoalitionToggle
coalition={areaCoalition}
onClick={() => {
let newCoalition = "";
if (areaCoalition === "blue") newCoalition = "neutral";
else if (areaCoalition === "neutral") newCoalition = "red";
else if (areaCoalition === "red") newCoalition = "blue";
setAreaCoalition(newCoalition as Coalition);
activeCoalitionArea.setCoalition(newCoalition as Coalition);
}}
></OlCoalitionToggle>
</div>
<div
className={`
flex flex-col gap-3 border-l-4 border-l-olympus-100
bg-olympus-600 p-5
`}
>
<div className={`
border-b-2 border-b-olympus-100 pb-4 text-gray-300
`}>Automatic IADS generation</div>
<OlDropdown className="" label="Units types">
{getApp()
.getGroundUnitDatabase()
.getTypes()
.map((type, idx) => {
if (!(type in typesSelection)) {
typesSelection[type] = true;
setTypesSelection(JSON.parse(JSON.stringify(typesSelection)));
}
return (
<OlDropdownItem key={idx} className={`flex gap-4`}>
<OlCheckbox
checked={typesSelection[type]}
onChange={(ev) => {
typesSelection[type] = ev.currentTarget.checked;
setTypesSelection(JSON.parse(JSON.stringify(typesSelection)));
}}
/>
<div>{type}</div>
</OlDropdownItem>
);
})}
</OlDropdown>
<OlDropdown className="" label="Units eras">
{getApp()
.getGroundUnitDatabase()
.getEras()
.map((era) => {
if (!(era in erasSelection)) {
erasSelection[era] = true;
setErasSelection(JSON.parse(JSON.stringify(erasSelection)));
}
return (
<OlDropdownItem className={`flex gap-4`}>
<OlCheckbox
checked={erasSelection[era]}
onChange={(ev) => {
erasSelection[era] = ev.currentTarget.checked;
setErasSelection(JSON.parse(JSON.stringify(erasSelection)));
}}
/>
<div>{era}</div>
</OlDropdownItem>
);
})}
</OlDropdown>
<OlDropdown className="" label="Units ranges">
{["Short range", "Medium range", "Long range"].map((range) => {
if (!(range in rangesSelection)) {
rangesSelection[range] = true;
setRangesSelection(JSON.parse(JSON.stringify(rangesSelection)));
}
return (
<OlDropdownItem className={`flex gap-4`}>
<OlCheckbox
checked={rangesSelection[range]}
onChange={(ev) => {
rangesSelection[range] = ev.currentTarget.checked;
setErasSelection(JSON.parse(JSON.stringify(rangesSelection)));
}}
/>
<div>{range}</div>
</OlDropdownItem>
);
})}
</OlDropdown>
<div>
<div className="flex justify-between">
<div className="text-gray-100">IADS Density</div>
<div
className={`
font-bold
dark:text-blue-500
`}
>
{IADSDensity}%
</div>
</div>
<OlRangeSlider
value={IADSDensity}
onChange={(ev) => {
setIADSDensity(Number(ev.currentTarget.value));
}}
></OlRangeSlider>
</div>
<div>
<div className="flex justify-between">
<div className="text-gray-100">IADS Distribution</div>
<div
className={`
font-bold
dark:text-blue-500
`}
>
{IADSDistribution}%
</div>
</div>
<OlRangeSlider
value={IADSDistribution}
onChange={(ev) => {
setIADSDistribution(Number(ev.target.value));
}}
></OlRangeSlider>
</div>
<div className="flex content-center gap-4 text-gray-200">
<OlCheckbox
checked={forceCoalitionAppropriateUnits}
onChange={() => {
setForceCoalitionApproriateUnits(!forceCoalitionAppropriateUnits);
}}
/>
Force coalition appropriate units
</div>
<button
type="button"
className={`
mb-2 me-2 rounded-lg bg-blue-700 px-5 py-2.5 text-sm
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
`}
onClick={() =>
getApp()
.getUnitsManager()
.createIADS(
activeCoalitionArea,
typesSelection,
erasSelection,
rangesSelection,
IADSDensity,
IADSDistribution,
forceCoalitionAppropriateUnits
)
}
>
Generate IADS
</button>
</div>
</div>
)}
<Menu
open={props.open}
title="Draw"
onClose={props.onClose}
canBeHidden={true}
showBackButton={activeCoalitionArea !== null}
onBack={() => {
getApp().setState(OlympusState.DRAW, DrawSubState.NO_SUBSTATE);
}}
>
<>
{appState === OlympusState.DRAW && appSubState !== DrawSubState.EDIT && (
<div className="flex flex-col gap-2 p-6 text-sm text-gray-400">
<OlStateButton
className="!w-full"
icon={faDrawPolygon}
tooltip={"Add a new polygon"}
checked={appSubState === DrawSubState.DRAW_POLYGON}
onClick={() => {
if (appSubState === DrawSubState.DRAW_POLYGON) getApp().setState(OlympusState.DRAW, DrawSubState.EDIT);
else getApp().setState(OlympusState.DRAW, DrawSubState.DRAW_POLYGON);
}}
>
<div className="text-sm">Add polygon</div>
</OlStateButton>
<OlStateButton
className="!w-full"
icon={faCircle}
tooltip={"Add a new circle"}
checked={appSubState === DrawSubState.DRAW_CIRCLE}
onClick={() => {
if (appSubState === DrawSubState.DRAW_CIRCLE) getApp().setState(OlympusState.DRAW, DrawSubState.EDIT);
else getApp().setState(OlympusState.DRAW, DrawSubState.DRAW_CIRCLE);
}}
>
<div className="text-sm">Add circle</div>
</OlStateButton>
</div>
</Menu>
)}
</StateConsumer>
)}
</>
<div>
{activeCoalitionArea !== null && appSubState === DrawSubState.EDIT && (
<div className={`flex flex-col gap-4 py-4`}>
<div
className={`
flex flex-col content-center justify-start gap-2 px-6
text-gray-200
`}
>
<div className="my-auto flex justify-between text-md">
Area label
<div
className="rounded-md bg-red-800 p-2"
onClick={() => {
getApp().getMap().deleteCoalitionArea(activeCoalitionArea);
setActiveCoalitionArea(null);
}}
>
<FaTrash className={`text-gray-50`}></FaTrash>
</div>
</div>
<input
type="text"
className={`
block w-full flex-grow rounded-lg border border-gray-300
bg-gray-50 p-2.5 text-sm text-gray-900
dark:border-gray-600 dark:bg-gray-700 dark:text-white
dark:placeholder-gray-400 dark:focus:border-blue-500
dark:focus:ring-blue-500
focus:border-blue-500 focus:ring-blue-500
`}
placeholder={activeCoalitionArea.getLabelText()}
onInput={(ev) => activeCoalitionArea.setLabelText(ev.currentTarget.value)}
></input>
</div>
<div
className={`
flex content-center justify-start gap-4 px-6 text-gray-200
`}
>
<div className="my-auto text-md">Coalition: </div>
<OlCoalitionToggle
coalition={areaCoalition}
onClick={() => {
let newCoalition = "";
if (areaCoalition === "blue") newCoalition = "neutral";
else if (areaCoalition === "neutral") newCoalition = "red";
else if (areaCoalition === "red") newCoalition = "blue";
setAreaCoalition(newCoalition as Coalition);
activeCoalitionArea.setCoalition(newCoalition as Coalition);
}}
></OlCoalitionToggle>
</div>
<div
className={`
flex flex-col gap-3 border-l-4 border-l-olympus-100
bg-olympus-600 p-5
`}
>
<div className={`
border-b-2 border-b-olympus-100 pb-4 text-gray-300
`}>Automatic IADS generation</div>
<OlDropdown className="" label="Units types">
{types.map((type, idx) => {
if (!(type in typesSelection)) {
typesSelection[type] = true;
setTypesSelection(JSON.parse(JSON.stringify(typesSelection)));
}
return (
<OlDropdownItem key={idx} className={`flex gap-4`}>
<OlCheckbox
checked={typesSelection[type]}
onChange={(ev) => {
typesSelection[type] = ev.currentTarget.checked;
setTypesSelection(JSON.parse(JSON.stringify(typesSelection)));
}}
/>
<div>{type}</div>
</OlDropdownItem>
);
})}
</OlDropdown>
<OlDropdown className="" label="Units eras">
{eras.map((era) => {
if (!(era in erasSelection)) {
erasSelection[era] = true;
setErasSelection(JSON.parse(JSON.stringify(erasSelection)));
}
return (
<OlDropdownItem className={`flex gap-4`}>
<OlCheckbox
checked={erasSelection[era]}
onChange={(ev) => {
erasSelection[era] = ev.currentTarget.checked;
setErasSelection(JSON.parse(JSON.stringify(erasSelection)));
}}
/>
<div>{era}</div>
</OlDropdownItem>
);
})}
</OlDropdown>
<OlDropdown className="" label="Units ranges">
{["Short range", "Medium range", "Long range"].map((range) => {
if (!(range in rangesSelection)) {
rangesSelection[range] = true;
setRangesSelection(JSON.parse(JSON.stringify(rangesSelection)));
}
return (
<OlDropdownItem className={`flex gap-4`}>
<OlCheckbox
checked={rangesSelection[range]}
onChange={(ev) => {
rangesSelection[range] = ev.currentTarget.checked;
setErasSelection(JSON.parse(JSON.stringify(rangesSelection)));
}}
/>
<div>{range}</div>
</OlDropdownItem>
);
})}
</OlDropdown>
<div>
<div className="flex justify-between">
<div className="text-gray-100">IADS Density</div>
<div
className={`
font-bold
dark:text-blue-500
`}
>
{IADSDensity}%
</div>
</div>
<OlRangeSlider
value={IADSDensity}
onChange={(ev) => {
setIADSDensity(Number(ev.currentTarget.value));
}}
></OlRangeSlider>
</div>
<div>
<div className="flex justify-between">
<div className="text-gray-100">IADS Distribution</div>
<div
className={`
font-bold
dark:text-blue-500
`}
>
{IADSDistribution}%
</div>
</div>
<OlRangeSlider
value={IADSDistribution}
onChange={(ev) => {
setIADSDistribution(Number(ev.target.value));
}}
></OlRangeSlider>
</div>
<div className="flex content-center gap-4 text-gray-200">
<OlCheckbox
checked={forceCoalitionAppropriateUnits}
onChange={() => {
setForceCoalitionApproriateUnits(!forceCoalitionAppropriateUnits);
}}
/>
Force coalition appropriate units
</div>
<button
type="button"
className={`
mb-2 me-2 rounded-lg bg-blue-700 px-5 py-2.5 text-sm
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
`}
onClick={() =>
getApp()
.getUnitsManager()
.createIADS(
activeCoalitionArea,
typesSelection,
erasSelection,
rangesSelection,
IADSDensity,
IADSDistribution,
forceCoalitionAppropriateUnits
)
}
>
Generate IADS
</button>
</div>
</div>
)}
</div>
</Menu>
);
}

View File

@@ -2,7 +2,6 @@ import React, { useEffect, useRef, useState } from "react";
import { OlRoundStateButton, OlStateButton, OlLockStateButton } from "../components/olstatebutton";
import { faSkull, faCamera, faFlag, faLink, faUnlink, faBars, faVolumeHigh } from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { StateConsumer } from "../../statecontext";
import { OlDropdownItem, OlDropdown } from "../components/oldropdown";
import { OlLabelToggle } from "../components/ollabeltoggle";
import { getApp, IP, connectedToServer } from "../../olympusapp";
@@ -18,12 +17,29 @@ import {
olButtonsVisibilityOlympus,
} from "../components/olicons";
import { FaChevronLeft, FaChevronRight } from "react-icons/fa6";
import { ConfigLoadedEvent, HiddenTypesChangedEvent, MapOptionsChangedEvent, MapSourceChangedEvent } from "../../events";
import { MAP_HIDDEN_TYPES_DEFAULTS, MAP_OPTIONS_DEFAULTS } from "../../constants/constants";
import { OlympusConfig } from "../../interfaces";
export function Header() {
const [mapHiddenTypes, setMapHiddenTypes] = useState(MAP_HIDDEN_TYPES_DEFAULTS);
const [mapOptions, setMapOptions] = useState(MAP_OPTIONS_DEFAULTS);
const [mapSource, setMapSource] = useState("");
const [mapSources, setMapSources] = useState([] as string[]);
const [scrolledLeft, setScrolledLeft] = useState(true);
const [scrolledRight, setScrolledRight] = useState(false);
const [audioEnabled, setAudioEnabled] = useState(false);
useEffect(() => {
HiddenTypesChangedEvent.on((hiddenTypes) => setMapHiddenTypes({ ...hiddenTypes }));
MapOptionsChangedEvent.on((mapOptions) => setMapOptions({ ...mapOptions }));
MapSourceChangedEvent.on((source) => setMapSource(source));
ConfigLoadedEvent.on((config: OlympusConfig) => {
var sources = Object.keys(config.mapMirrors).concat(Object.keys(config.mapLayers));
setMapSources(sources);
});
}, []);
/* Initialize the "scroll" position of the element */
var scrollRef = useRef(null);
useEffect(() => {
@@ -42,8 +58,6 @@ export function Header() {
}
return (
<StateConsumer>
{(appState) => (
<nav
className={`
z-10 flex w-full gap-4 border-gray-200 bg-gray-300 px-3
@@ -107,14 +121,12 @@ export function Header() {
</div>
</div>
<div
className={`
flex h-fit flex-row items-center justify-start gap-1
`}
className={`flex h-fit flex-row items-center justify-start gap-1`}
>
<OlLockStateButton
checked={!appState.mapOptions.protectDCSUnits}
checked={!mapOptions.protectDCSUnits}
onClick={() => {
getApp().getMap().setOption("protectDCSUnits", !appState.mapOptions.protectDCSUnits);
getApp().getMap().setOption("protectDCSUnits", !mapOptions.protectDCSUnits);
}}
tooltip="Lock/unlock protected units (from scripted mission)"
/>
@@ -130,9 +142,7 @@ export function Header() {
</div>
<div className={`h-8 w-0 border-l-[2px] border-gray-700`}></div>
<div
className={`
flex h-fit flex-row items-center justify-start gap-1
`}
className={`flex h-fit flex-row items-center justify-start gap-1`}
>
{Object.entries({
human: olButtonsVisibilityHuman,
@@ -143,9 +153,9 @@ export function Header() {
<OlRoundStateButton
key={entry[0]}
onClick={() => {
getApp().getMap().setHiddenType(entry[0], !appState.mapHiddenTypes[entry[0]]);
getApp().getMap().setHiddenType(entry[0], !mapHiddenTypes[entry[0]]);
}}
checked={!appState.mapHiddenTypes[entry[0]]}
checked={!mapHiddenTypes[entry[0]]}
icon={entry[1]}
tooltip={"Hide/show " + entry[0] + " units"}
/>
@@ -154,27 +164,25 @@ export function Header() {
</div>
<div className={`h-8 w-0 border-l-[2px] border-gray-700`}></div>
<div
className={`
flex h-fit flex-row items-center justify-start gap-1
`}
className={`flex h-fit flex-row items-center justify-start gap-1`}
>
<OlRoundStateButton
onClick={() => getApp().getMap().setHiddenType("blue", !appState.mapHiddenTypes["blue"])}
checked={!appState.mapHiddenTypes["blue"]}
onClick={() => getApp().getMap().setHiddenType("blue", !mapHiddenTypes["blue"])}
checked={!mapHiddenTypes["blue"]}
icon={faFlag}
className={"!text-blue-500"}
tooltip={"Hide/show blue units"}
/>
<OlRoundStateButton
onClick={() => getApp().getMap().setHiddenType("red", !appState.mapHiddenTypes["red"])}
checked={!appState.mapHiddenTypes["red"]}
onClick={() => getApp().getMap().setHiddenType("red", !mapHiddenTypes["red"])}
checked={!mapHiddenTypes["red"]}
icon={faFlag}
className={"!text-red-500"}
tooltip={"Hide/show red units"}
/>
<OlRoundStateButton
onClick={() => getApp().getMap().setHiddenType("neutral", !appState.mapHiddenTypes["neutral"])}
checked={!appState.mapHiddenTypes["neutral"]}
onClick={() => getApp().getMap().setHiddenType("neutral", !mapHiddenTypes["neutral"])}
checked={!mapHiddenTypes["neutral"]}
icon={faFlag}
className={"!text-gray-500"}
tooltip={"Hide/show neutral units"}
@@ -182,9 +190,7 @@ export function Header() {
</div>
<div className={`h-8 w-0 border-l-[2px] border-gray-700`}></div>
<div
className={`
flex h-fit flex-row items-center justify-start gap-1
`}
className={`flex h-fit flex-row items-center justify-start gap-1`}
>
{Object.entries({
aircraft: olButtonsVisibilityAircraft,
@@ -199,9 +205,9 @@ export function Header() {
<OlRoundStateButton
key={entry[0]}
onClick={() => {
getApp().getMap().setHiddenType(entry[0], !appState.mapHiddenTypes[entry[0]]);
getApp().getMap().setHiddenType(entry[0], !mapHiddenTypes[entry[0]]);
}}
checked={!appState.mapHiddenTypes[entry[0]]}
checked={!mapHiddenTypes[entry[0]]}
icon={entry[1]}
tooltip={"Hide/show " + entry[0] + " units"}
/>
@@ -211,8 +217,8 @@ export function Header() {
<OlLabelToggle toggled={false} leftLabel={"Live"} rightLabel={"Map"} onClick={() => {}}></OlLabelToggle>
<OlStateButton checked={false} icon={faCamera} onClick={() => {}} tooltip="Activate/deactivate camera plugin" />
<OlDropdown label={appState.activeMapSource} className="w-60">
{appState.mapSources.map((source) => {
<OlDropdown label={mapSource} className="w-60">
{mapSources.map((source) => {
return (
<OlDropdownItem key={source} onClick={() => getApp().getMap().setLayerName(source)}>
<div className="truncate">{source}</div>
@@ -230,7 +236,5 @@ export function Header() {
/>
)}
</nav>
)}
</StateConsumer>
);
}

View File

@@ -85,9 +85,7 @@ export function JTACMenu(props: { open: boolean; onClose: () => void; children?:
</div>
<div className="flex">
<span
className={`
my-auto h-full min-w-10 text-nowrap p-2 text-center
`}
className={`my-auto h-full min-w-10 text-nowrap p-2 text-center`}
>
BP
</span>
@@ -122,9 +120,7 @@ export function JTACMenu(props: { open: boolean; onClose: () => void; children?:
</div>
<div className="flex">
<span
className={`
my-auto h-full min-w-10 text-nowrap p-2 text-center
`}
className={`my-auto h-full min-w-10 text-nowrap p-2 text-center`}
>
IP
</span>
@@ -159,9 +155,7 @@ export function JTACMenu(props: { open: boolean; onClose: () => void; children?:
</div>
<div className="flex">
<span
className={`
my-auto h-full min-w-10 text-nowrap p-3 text-center
`}
className={`my-auto h-full min-w-10 text-nowrap p-3 text-center`}
>
<FaBullseye />
</span>

View File

@@ -1,15 +1,21 @@
import React, { useState, useEffect, useContext } from "react";
import { zeroAppend } from "../../other/utils";
import { DateAndTime } from "../../interfaces";
import { DateAndTime, ServerStatus } from "../../interfaces";
import { getApp } from "../../olympusapp";
import { FaChevronDown, FaChevronUp } from "react-icons/fa6";
import { StateContext } from "../../statecontext";
import { MapOptionsChangedEvent, ServerStatusUpdatedEvent } from "../../events";
import { MAP_OPTIONS_DEFAULTS } from "../../constants/constants";
export function MiniMapPanel(props: {}) {
const appState = useContext(StateContext)
const [serverStatus, setServerStatus] = useState({} as ServerStatus);
const [mapOptions, setMapOptions] = useState(MAP_OPTIONS_DEFAULTS);
const [showMissionTime, setShowMissionTime] = useState(false);
useEffect(() => {
ServerStatusUpdatedEvent.on((status) => setServerStatus(status));
MapOptionsChangedEvent.on((mapOptions) => setMapOptions({ ...mapOptions }));
}, []);
// A bit of a hack to set the rounded borders to the minimap
useEffect(() => {
let miniMap = document.querySelector(".leaflet-control-minimap");
@@ -24,45 +30,45 @@ export function MiniMapPanel(props: {}) {
let seconds = 0;
if (showMissionTime) {
hours = appState.serverStatus.missionTime.h;
minutes = appState.serverStatus.missionTime.m;
seconds = appState.serverStatus.missionTime.s;
hours = serverStatus.missionTime.h;
minutes = serverStatus.missionTime.m;
seconds = serverStatus.missionTime.s;
} else {
hours = Math.floor(appState.serverStatus.elapsedTime / 3600);
minutes = Math.floor(appState.serverStatus.elapsedTime / 60) % 60;
seconds = Math.round(appState.serverStatus.elapsedTime) % 60;
hours = Math.floor(serverStatus.elapsedTime / 3600);
minutes = Math.floor(serverStatus.elapsedTime / 60) % 60;
seconds = Math.round(serverStatus.elapsedTime) % 60;
}
let timeString = `${zeroAppend(hours, 2)}:${zeroAppend(minutes, 2)}:${zeroAppend(seconds, 2)}`;
// Choose frame rate string color
let frameRateColor = "#8BFF63";
if (appState.serverStatus.frameRate < 30) frameRateColor = "#F05252";
else if (appState.serverStatus.frameRate >= 30 && appState.serverStatus.frameRate < 60) frameRateColor = "#FF9900";
if (serverStatus.frameRate < 30) frameRateColor = "#F05252";
else if (serverStatus.frameRate >= 30 && serverStatus.frameRate < 60) frameRateColor = "#FF9900";
// Choose load string color
let loadColor = "#8BFF63";
if (appState.serverStatus.load > 1000) loadColor = "#F05252";
else if (appState.serverStatus.load >= 100 && appState.serverStatus.load < 1000) loadColor = "#FF9900";
if (serverStatus.load > 1000) loadColor = "#F05252";
else if (serverStatus.load >= 100 && serverStatus.load < 1000) loadColor = "#FF9900";
return (
<div
onClick={() => setShowMissionTime(!showMissionTime)}
className={`
absolute right-[10px]
${appState.mapOptions.showMinimap ? `top-[232px]` : `top-[70px]`}
${mapOptions.showMinimap ? `top-[232px]` : `top-[70px]`}
flex w-[288px] items-center justify-between
${appState.mapOptions.showMinimap ? `rounded-b-lg` : `rounded-lg`}
${mapOptions.showMinimap ? `rounded-b-lg` : `rounded-lg`}
bg-gray-200 p-3 text-sm backdrop-blur-lg backdrop-grayscale
dark:bg-olympus-800/90 dark:text-gray-200
`}
>
{!appState.serverStatus.connected ? (
{!serverStatus.connected ? (
<div className={`flex animate-pulse items-center gap-2 font-semibold`}>
<div className={`relative h-4 w-4 rounded-full bg-[#F05252]`}></div>
Server disconnected
</div>
) : appState.serverStatus.paused ? (
) : serverStatus.paused ? (
<div className={`flex animate-pulse items-center gap-2 font-semibold`}>
<div className={`relative h-4 w-4 rounded-full bg-[#FF9900]`}></div>
Server paused
@@ -72,13 +78,13 @@ export function MiniMapPanel(props: {}) {
<div className="flex gap-2 font-semibold">
FPS:
<span style={{ color: frameRateColor }} className={`font-semibold`}>
{appState.serverStatus.frameRate}
{serverStatus.frameRate}
</span>
</div>
<div className="flex gap-2 font-semibold">
Load:
<span style={{ color: loadColor }} className={`font-semibold`}>
{appState.serverStatus.load}
{serverStatus.load}
</span>
</div>
<div className="flex gap-2 font-semibold">
@@ -87,7 +93,7 @@ export function MiniMapPanel(props: {}) {
<div className={`relative h-4 w-4 rounded-full bg-[#8BFF63]`}></div>
</>
)}
{appState.mapOptions.showMinimap ? (
{mapOptions.showMinimap ? (
<FaChevronUp
onClick={() => {
getApp().getMap().setOption("showMinimap", false);

View File

@@ -1,4 +1,4 @@
import React from "react";
import React, { useEffect, useState } from "react";
import { OlStateButton } from "../components/olstatebutton";
import {
faGamepad,
@@ -12,88 +12,94 @@ import {
faVolumeHigh,
faJ,
} from "@fortawesome/free-solid-svg-icons";
import { StateConsumer } from "../../statecontext";
import { getApp } from "../../olympusapp";
import { OlympusState } from "../../constants/constants";
import { NO_SUBSTATE, OlympusState, OlympusSubState } from "../../constants/constants";
import { AppStateChangedEvent } from "../../events";
export function SideBar() {
const [appState, setAppState] = useState(OlympusState.NOT_INITIALIZED);
useEffect(() => {
AppStateChangedEvent.on((state, subState) => setAppState(state));
}, []);
return (
<StateConsumer>
{(appState) => (
<nav
className={`
z-20 flex h-full flex-col bg-gray-300
dark:bg-olympus-900
`}
>
<div
className={`
w-16 flex-1 flex-wrap items-center justify-center p-4
`}
>
<div
className={`
flex flex-col items-center justify-center gap-2.5
`}
>
<OlStateButton
onClick={() => {getApp().setState(appState.appState !== OlympusState.MAIN_MENU? OlympusState.MAIN_MENU: OlympusState.IDLE)}}
checked={appState.appState === OlympusState.MAIN_MENU}
icon={faEllipsisV}
tooltip="Hide/show main menu"
></OlStateButton>
<OlStateButton
onClick={() => {getApp().setState(appState.appState !== OlympusState.SPAWN? OlympusState.SPAWN: OlympusState.IDLE)}}
checked={appState.appState === OlympusState.SPAWN}
icon={faPlusSquare}
tooltip="Hide/show unit spawn menu"
></OlStateButton>
<OlStateButton
onClick={() => {getApp().setState(appState.appState !== OlympusState.UNIT_CONTROL? OlympusState.UNIT_CONTROL: OlympusState.IDLE)}}
checked={appState.appState === OlympusState.UNIT_CONTROL}
icon={faGamepad}
tooltip="Hide/show selection tool and unit control menu"
></OlStateButton>
<OlStateButton
onClick={() => {getApp().setState(appState.appState !== OlympusState.DRAW? OlympusState.DRAW: OlympusState.IDLE)}}
checked={appState.appState === OlympusState.DRAW}
icon={faPencil}
tooltip="Hide/show drawing menu"
></OlStateButton>
<OlStateButton
onClick={() => {getApp().setState(appState.appState !== OlympusState.AUDIO? OlympusState.AUDIO: OlympusState.IDLE)}}
checked={appState.appState === OlympusState.AUDIO}
icon={faVolumeHigh}
tooltip="Hide/show audio menu"
></OlStateButton>
<OlStateButton
onClick={() => {getApp().setState(appState.appState !== OlympusState.JTAC? OlympusState.JTAC: OlympusState.IDLE)}}
checked={appState.appState === OlympusState.JTAC}
icon={faJ} tooltip="Hide/show JTAC menu"></OlStateButton>
</div>
</div>
<div className="flex w-16 flex-wrap content-end justify-center p-4">
<div
className={`
flex flex-col items-center justify-center gap-2.5
`}
>
<OlStateButton
onClick={() => window.open("https://github.com/Pax1601/DCSOlympus/wiki")}
checked={false}
icon={faQuestionCircle}
tooltip="Open user guide on separate window"
></OlStateButton>
<OlStateButton
onClick={() => {getApp().setState(appState.appState !== OlympusState.OPTIONS? OlympusState.OPTIONS: OlympusState.IDLE)}}
checked={appState.appState === OlympusState.OPTIONS}
icon={faCog}
tooltip="Hide/show settings menu"
></OlStateButton>
</div>
</div>
</nav>
)}
</StateConsumer>
<nav
className={`
z-20 flex h-full flex-col bg-gray-300
dark:bg-olympus-900
`}
>
<div className={`w-16 flex-1 flex-wrap items-center justify-center p-4`}>
<div className={`flex flex-col items-center justify-center gap-2.5`}>
<OlStateButton
onClick={() => {
getApp().setState(appState !== OlympusState.MAIN_MENU ? OlympusState.MAIN_MENU : OlympusState.IDLE);
}}
checked={appState === OlympusState.MAIN_MENU}
icon={faEllipsisV}
tooltip="Hide/show main menu"
></OlStateButton>
<OlStateButton
onClick={() => {
getApp().setState(appState !== OlympusState.SPAWN ? OlympusState.SPAWN : OlympusState.IDLE);
}}
checked={appState === OlympusState.SPAWN}
icon={faPlusSquare}
tooltip="Hide/show unit spawn menu"
></OlStateButton>
<OlStateButton
onClick={() => {
getApp().setState(appState !== OlympusState.UNIT_CONTROL ? OlympusState.UNIT_CONTROL : OlympusState.IDLE);
}}
checked={appState === OlympusState.UNIT_CONTROL}
icon={faGamepad}
tooltip="Hide/show selection tool and unit control menu"
></OlStateButton>
<OlStateButton
onClick={() => {
getApp().setState(appState !== OlympusState.DRAW ? OlympusState.DRAW : OlympusState.IDLE);
}}
checked={appState === OlympusState.DRAW}
icon={faPencil}
tooltip="Hide/show drawing menu"
></OlStateButton>
<OlStateButton
onClick={() => {
getApp().setState(appState !== OlympusState.AUDIO ? OlympusState.AUDIO : OlympusState.IDLE);
}}
checked={appState === OlympusState.AUDIO}
icon={faVolumeHigh}
tooltip="Hide/show audio menu"
></OlStateButton>
<OlStateButton
onClick={() => {
getApp().setState(appState !== OlympusState.JTAC ? OlympusState.JTAC : OlympusState.IDLE);
}}
checked={appState === OlympusState.JTAC}
icon={faJ}
tooltip="Hide/show JTAC menu"
></OlStateButton>
</div>
</div>
<div className="flex w-16 flex-wrap content-end justify-center p-4">
<div className={`flex flex-col items-center justify-center gap-2.5`}>
<OlStateButton
onClick={() => window.open("https://github.com/Pax1601/DCSOlympus/wiki")}
checked={false}
icon={faQuestionCircle}
tooltip="Open user guide on separate window"
></OlStateButton>
<OlStateButton
onClick={() => {
getApp().setState(appState !== OlympusState.OPTIONS ? OlympusState.OPTIONS : OlympusState.IDLE);
}}
checked={appState === OlympusState.OPTIONS}
icon={faCog}
tooltip="Hide/show settings menu"
></OlStateButton>
</div>
</div>
</nav>
);
}

View File

@@ -16,15 +16,10 @@ import {
import { faExplosion, faSmog } from "@fortawesome/free-solid-svg-icons";
import { OlEffectListEntry } from "../components/oleffectlistentry";
import { EffectSpawnMenu } from "./effectspawnmenu";
import { NO_SUBSTATE, OlympusState, SpawnSubState } from "../../constants/constants";
import { aircraftDatabase } from "../../unit/databases/aircraftdatabase";
import { navyUnitDatabase } from "../../unit/databases/navyunitdatabase";
import { filterBlueprintsByLabel } from "../../other/utils";
import { helicopterDatabase } from "../../unit/databases/helicopterdatabase";
import { groundUnitDatabase } from "../../unit/databases/groundunitdatabase";
import { AppStateChangedEvent } from "../../events";
import { NO_SUBSTATE, OlympusState } from "../../constants/constants";
import { AppStateChangedEvent, UnitDatabaseLoadedEvent } from "../../events";
enum Accordion {
enum CategoryAccordion {
NONE,
AIRCRAFT,
HELICOPTER,
@@ -36,34 +31,53 @@ enum Accordion {
}
export function SpawnMenu(props: { open: boolean; onClose: () => void; children?: JSX.Element | JSX.Element[] }) {
const [openAccordion, setOpenAccordion] = useState(Accordion.NONE);
const [openAccordion, setOpenAccordion] = useState(CategoryAccordion.NONE);
const [blueprint, setBlueprint] = useState(null as null | UnitBlueprint);
const [effect, setEffect] = useState(null as null | string);
const [filterString, setFilterString] = useState("");
const [selectedRole, setSelectedRole] = useState(null as null | string);
const [selectedType, setSelectedType] = useState(null as null | string);
const [blueprints, setBlueprints] = useState([] as UnitBlueprint[]);
const [roles, setRoles] = useState({ aircraft: [] as string[], helicopter: [] as string[] });
const [types, setTypes] = useState({ groundunit: [] as string[], navyunit: [] as string[] });
const filteredAircraft = getApp()
? filterBlueprintsByLabel(selectedRole ? aircraftDatabase.getByRole(selectedRole) : Object.values(aircraftDatabase.getBlueprints()), filterString)
: ({} as { [key: string]: UnitBlueprint });
const filteredHelicopters = getApp()
? filterBlueprintsByLabel(selectedRole ? helicopterDatabase.getByRole(selectedRole) : Object.values(helicopterDatabase.getBlueprints()), filterString)
: ({} as { [key: string]: UnitBlueprint });
const filteredSAMs = getApp() ? filterBlueprintsByLabel(groundUnitDatabase.getByType("SAM Site"), filterString) : ({} as { [key: string]: UnitBlueprint });
const filteredAAA = getApp() ? filterBlueprintsByLabel(groundUnitDatabase.getByType("AAA"), filterString) : ({} as { [key: string]: UnitBlueprint });
const filteredGroundUnits = getApp()
? filterBlueprintsByLabel(selectedType ? groundUnitDatabase.getByType(selectedType) : Object.values(groundUnitDatabase.getBlueprints()), filterString)
: ({} as { [key: string]: UnitBlueprint });
const filteredNavyUnits = getApp()
? filterBlueprintsByLabel(selectedType ? navyUnitDatabase.getByType(selectedType) : Object.values(navyUnitDatabase.getBlueprints()), filterString)
: ({} as { [key: string]: UnitBlueprint });
useEffect(() => {
if (selectedRole)
setBlueprints(getApp()?.getUnitsManager().getDatabase().getByRole(selectedRole));
else if (selectedType)
setBlueprints(getApp()?.getUnitsManager().getDatabase().getByType(selectedType));
else
setBlueprints(getApp()?.getUnitsManager().getDatabase().getBlueprints())
}, [selectedRole, selectedType, openAccordion]);
useEffect(() => {
UnitDatabaseLoadedEvent.on(() => {
setRoles({
aircraft: getApp()?.getUnitsManager().getDatabase().getRoles((unit) => unit.category === "aircraft"),
helicopter: getApp()?.getUnitsManager().getDatabase().getRoles((unit) => unit.category === "helicopter"),
});
setTypes({
groundunit: getApp()?.getUnitsManager().getDatabase().getTypes((unit) => unit.category === "groundunit"),
navyunit: getApp()?.getUnitsManager().getDatabase().getTypes((unit) => unit.category === "navyunit")
});
});
}, []);
/* Filter the blueprints according to the label */
const filteredBlueprints: UnitBlueprint[] = [];
if (blueprints) {
blueprints.forEach((blueprint) => {
if (blueprint.enabled && (filterString === "" || blueprint.label.toLowerCase().includes(filterString.toLowerCase()))) filteredBlueprints.push(blueprint);
});
}
useEffect(() => {
if (!props.open) {
if (blueprint !== null) setBlueprint(null);
if (effect !== null) setEffect(null);
if (filterString !== "") setFilterString("");
if (openAccordion !== Accordion.NONE) setOpenAccordion(Accordion.NONE);
if (openAccordion !== CategoryAccordion.NONE) setOpenAccordion(CategoryAccordion.NONE);
}
});
@@ -94,36 +108,33 @@ export function SpawnMenu(props: { open: boolean; onClose: () => void; children?
<OlSearchBar onChange={(value) => setFilterString(value)} text={filterString} />
<OlAccordion
title={`Aircraft`}
open={openAccordion == Accordion.AIRCRAFT}
open={openAccordion == CategoryAccordion.AIRCRAFT}
onClick={() => {
setOpenAccordion(openAccordion === Accordion.AIRCRAFT ? Accordion.NONE : Accordion.AIRCRAFT);
setOpenAccordion(openAccordion === CategoryAccordion.AIRCRAFT ? CategoryAccordion.NONE : CategoryAccordion.AIRCRAFT);
setSelectedRole(null);
setSelectedType(null);
}}
>
<div className="mb-2 flex flex-wrap gap-1">
{aircraftDatabase
.getRoles()
.sort()
.map((role) => {
return (
<div
key={role}
data-selected={selectedRole === role}
className={`
cursor-pointer rounded-full bg-olympus-800 px-2 py-0.5
text-xs font-bold text-olympus-50
data-[selected='true']:bg-blue-500
data-[selected='true']:text-gray-200
`}
onClick={() => {
selectedRole === role ? setSelectedRole(null) : setSelectedRole(role);
}}
>
{role}
</div>
);
})}
{roles.aircraft.sort().map((role) => {
return (
<div
key={role}
data-selected={selectedRole === role}
className={`
cursor-pointer rounded-full bg-olympus-800 px-2 py-0.5
text-xs font-bold text-olympus-50
data-[selected='true']:bg-blue-500
data-[selected='true']:text-gray-200
`}
onClick={() => {
selectedRole === role ? setSelectedRole(null) : setSelectedRole(role);
}}
>
{role}
</div>
);
})}
</div>
<div
className={`
@@ -131,43 +142,42 @@ export function SpawnMenu(props: { open: boolean; onClose: () => void; children?
no-scrollbar
`}
>
{Object.entries(filteredAircraft).map((entry) => {
return <OlUnitListEntry key={entry[0]} icon={olButtonsVisibilityAircraft} blueprint={entry[1]} onClick={() => setBlueprint(entry[1])} />;
})}
{filteredBlueprints
.filter((blueprint) => blueprint.category === "aircraft")
.map((entry) => {
return <OlUnitListEntry key={entry.name} icon={olButtonsVisibilityAircraft} blueprint={entry} onClick={() => setBlueprint(entry)} />;
})}
</div>
</OlAccordion>
<OlAccordion
title={`Helicopters`}
open={openAccordion == Accordion.HELICOPTER}
open={openAccordion == CategoryAccordion.HELICOPTER}
onClick={() => {
setOpenAccordion(openAccordion === Accordion.HELICOPTER ? Accordion.NONE : Accordion.HELICOPTER);
setOpenAccordion(openAccordion === CategoryAccordion.HELICOPTER ? CategoryAccordion.NONE : CategoryAccordion.HELICOPTER);
setSelectedRole(null);
setSelectedType(null);
}}
>
<div className="mb-2 flex flex-wrap gap-1">
{helicopterDatabase
.getRoles()
.sort()
.map((role) => {
return (
<div
key={role}
data-selected={selectedRole === role}
className={`
cursor-pointer rounded-full bg-olympus-800 px-2 py-0.5
text-xs font-bold text-olympus-50
data-[selected='true']:bg-blue-500
data-[selected='true']:text-gray-200
`}
onClick={() => {
selectedRole === role ? setSelectedRole(null) : setSelectedRole(role);
}}
>
{role}
</div>
);
})}
{roles.helicopter.sort().map((role) => {
return (
<div
key={role}
data-selected={selectedRole === role}
className={`
cursor-pointer rounded-full bg-olympus-800 px-2 py-0.5
text-xs font-bold text-olympus-50
data-[selected='true']:bg-blue-500
data-[selected='true']:text-gray-200
`}
onClick={() => {
selectedRole === role ? setSelectedRole(null) : setSelectedRole(role);
}}
>
{role}
</div>
);
})}
</div>
<div
className={`
@@ -175,16 +185,18 @@ export function SpawnMenu(props: { open: boolean; onClose: () => void; children?
no-scrollbar
`}
>
{Object.entries(filteredHelicopters).map((entry) => {
return <OlUnitListEntry key={entry[0]} icon={olButtonsVisibilityHelicopter} blueprint={entry[1]} onClick={() => setBlueprint(entry[1])} />;
})}
{filteredBlueprints
.filter((blueprint) => blueprint.category === "helicopter")
.map((entry) => {
return <OlUnitListEntry key={entry.name} icon={olButtonsVisibilityHelicopter} blueprint={entry} onClick={() => setBlueprint(entry)} />;
})}
</div>
</OlAccordion>
<OlAccordion
title={`Surfact to Air Missiles (SAM sites)`}
open={openAccordion == Accordion.SAM}
open={openAccordion == CategoryAccordion.SAM}
onClick={() => {
setOpenAccordion(openAccordion === Accordion.SAM ? Accordion.NONE : Accordion.SAM);
setOpenAccordion(openAccordion === CategoryAccordion.SAM ? CategoryAccordion.NONE : CategoryAccordion.SAM);
setSelectedRole(null);
setSelectedType(null);
}}
@@ -195,16 +207,18 @@ export function SpawnMenu(props: { open: boolean; onClose: () => void; children?
no-scrollbar
`}
>
{Object.entries(filteredSAMs).map((entry) => {
return <OlUnitListEntry key={entry[0]} icon={olButtonsVisibilityGroundunitSam} blueprint={entry[1]} onClick={() => setBlueprint(entry[1])} />;
})}
{filteredBlueprints
.filter((blueprint) => blueprint.category === "groundunit")
.map((entry) => {
return <OlUnitListEntry key={entry.name} icon={olButtonsVisibilityGroundunitSam} blueprint={entry} onClick={() => setBlueprint(entry)} />;
})}
</div>
</OlAccordion>
<OlAccordion
title={`Anti Aircraft Artillery (AAA)`}
open={openAccordion == Accordion.AAA}
open={openAccordion == CategoryAccordion.AAA}
onClick={() => {
setOpenAccordion(openAccordion === Accordion.AAA ? Accordion.NONE : Accordion.AAA);
setOpenAccordion(openAccordion === CategoryAccordion.AAA ? CategoryAccordion.NONE : CategoryAccordion.AAA);
setSelectedRole(null);
setSelectedType(null);
}}
@@ -215,23 +229,24 @@ export function SpawnMenu(props: { open: boolean; onClose: () => void; children?
no-scrollbar
`}
>
{Object.entries(filteredAAA).map((entry) => {
return <OlUnitListEntry key={entry[0]} icon={olButtonsVisibilityGroundunitSam} blueprint={entry[1]} onClick={() => setBlueprint(entry[1])} />;
})}
{filteredBlueprints
.filter((blueprint) => blueprint.category === "groundunit")
.map((entry) => {
return <OlUnitListEntry key={entry.name} icon={olButtonsVisibilityGroundunitSam} blueprint={entry} onClick={() => setBlueprint(entry)} />;
})}
</div>
</OlAccordion>
<OlAccordion
title={`Ground Units`}
open={openAccordion == Accordion.GROUND_UNIT}
open={openAccordion == CategoryAccordion.GROUND_UNIT}
onClick={() => {
setOpenAccordion(openAccordion === Accordion.GROUND_UNIT ? Accordion.NONE : Accordion.GROUND_UNIT);
setOpenAccordion(openAccordion === CategoryAccordion.GROUND_UNIT ? CategoryAccordion.NONE : CategoryAccordion.GROUND_UNIT);
setSelectedRole(null);
setSelectedType(null);
}}
>
<div className="mb-2 flex flex-wrap gap-1">
{groundUnitDatabase
.getTypes()
{types.groundunit
.sort()
.filter((type) => {
return type !== "AAA" && type !== "SAM Site";
@@ -262,43 +277,42 @@ export function SpawnMenu(props: { open: boolean; onClose: () => void; children?
no-scrollbar
`}
>
{Object.entries(filteredGroundUnits).map((entry) => {
return <OlUnitListEntry key={entry[0]} icon={olButtonsVisibilityGroundunit} blueprint={entry[1]} onClick={() => setBlueprint(entry[1])} />;
})}
{filteredBlueprints
.filter((blueprint) => blueprint.category === "groundunit")
.map((entry) => {
return <OlUnitListEntry key={entry.name} icon={olButtonsVisibilityGroundunit} blueprint={entry} onClick={() => setBlueprint(entry)} />;
})}
</div>
</OlAccordion>
<OlAccordion
title={`Ships and submarines`}
open={openAccordion == Accordion.NAVY_UNIT}
open={openAccordion == CategoryAccordion.NAVY_UNIT}
onClick={() => {
setOpenAccordion(openAccordion === Accordion.NAVY_UNIT ? Accordion.NONE : Accordion.NAVY_UNIT);
setOpenAccordion(openAccordion === CategoryAccordion.NAVY_UNIT ? CategoryAccordion.NONE : CategoryAccordion.NAVY_UNIT);
setSelectedRole(null);
setSelectedType(null);
}}
>
<div className="mb-2 flex flex-wrap gap-1">
{navyUnitDatabase
.getTypes()
.sort()
.map((type) => {
return (
<div
key={type}
data-selected={selectedType === type}
className={`
cursor-pointer rounded-full bg-olympus-800 px-2 py-0.5
text-xs font-bold text-olympus-50
data-[selected='true']:bg-blue-500
data-[selected='true']:text-gray-200
`}
onClick={() => {
selectedType === type ? setSelectedType(null) : setSelectedType(type);
}}
>
{type}
</div>
);
})}
{types.navyunit.sort().map((type) => {
return (
<div
key={type}
data-selected={selectedType === type}
className={`
cursor-pointer rounded-full bg-olympus-800 px-2 py-0.5
text-xs font-bold text-olympus-50
data-[selected='true']:bg-blue-500
data-[selected='true']:text-gray-200
`}
onClick={() => {
selectedType === type ? setSelectedType(null) : setSelectedType(type);
}}
>
{type}
</div>
);
})}
</div>
<div
className={`
@@ -306,16 +320,18 @@ export function SpawnMenu(props: { open: boolean; onClose: () => void; children?
no-scrollbar
`}
>
{Object.entries(filteredNavyUnits).map((entry) => {
return <OlUnitListEntry key={entry[0]} icon={olButtonsVisibilityNavyunit} blueprint={entry[1]} onClick={() => setBlueprint(entry[1])} />;
})}
{filteredBlueprints
.filter((blueprint) => blueprint.category === "navyunit")
.map((entry) => {
return <OlUnitListEntry key={entry.name} icon={olButtonsVisibilityNavyunit} blueprint={entry} onClick={() => setBlueprint(entry)} />;
})}
</div>
</OlAccordion>
<OlAccordion
title="Effects (smokes, explosions etc)"
open={openAccordion == Accordion.EFFECT}
open={openAccordion == CategoryAccordion.EFFECT}
onClick={() => {
setOpenAccordion(openAccordion === Accordion.EFFECT ? Accordion.NONE : Accordion.EFFECT);
setOpenAccordion(openAccordion === CategoryAccordion.EFFECT ? CategoryAccordion.NONE : CategoryAccordion.EFFECT);
setSelectedRole(null);
setSelectedType(null);
}}

View File

@@ -1,5 +1,4 @@
import React, { useContext, useEffect, useRef, useState } from "react";
import { Unit } from "../../unit/unit";
import React, { useEffect, useRef, useState } from "react";
import { ContextActionSet } from "../../unit/contextactionset";
import { OlStateButton } from "../components/olstatebutton";
import { getApp } from "../../olympusapp";
@@ -8,13 +7,12 @@ import { CONTEXT_ACTION_COLORS } from "../../constants/constants";
import { FaInfoCircle } from "react-icons/fa";
import { FaChevronLeft, FaChevronRight } from "react-icons/fa6";
import { OlympusState } from "../../constants/constants";
import { AppStateChangedEvent } from "../../events";
import { StateContext } from "../../statecontext";
import { AppStateChangedEvent, ContextActionChangedEvent, ContextActionSetChangedEvent } from "../../events";
export function UnitMouseControlBar(props: {}) {
const appState = useContext(StateContext);
const [open, setOpen] = useState(false);
export function UnitControlBar(props: {}) {
const [appState, setAppState] = useState(OlympusState.NOT_INITIALIZED);
const [contextActionSet, setContextActionsSet] = useState(null as ContextActionSet | null);
const [contextAction, setContextAction] = useState(null as ContextAction | null);
const [scrolledLeft, setScrolledLeft] = useState(true);
const [scrolledRight, setScrolledRight] = useState(false);
@@ -25,9 +23,9 @@ export function UnitMouseControlBar(props: {}) {
});
useEffect(() => {
AppStateChangedEvent.on((state, subState) => {
setOpen(state === OlympusState.UNIT_CONTROL);
});
AppStateChangedEvent.on((state, subState) => setAppState(state));
ContextActionSetChangedEvent.on((contextActionSet) => setContextActionsSet(contextActionSet));
ContextActionChangedEvent.on((contextAction) => setContextAction(contextAction));
}, []);
function onScroll(el) {
@@ -43,8 +41,8 @@ export function UnitMouseControlBar(props: {}) {
let reorderedActions: ContextAction[] = [];
CONTEXT_ACTION_COLORS.forEach((color) => {
if (appState.contextActionSet) {
Object.values(appState.contextActionSet.getContextActions()).forEach((contextAction: ContextAction) => {
if (contextActionSet) {
Object.values(contextActionSet.getContextActions()).forEach((contextAction: ContextAction) => {
if (color === null && contextAction.getOptions().buttonColor === undefined) reorderedActions.push(contextAction);
else if (color === contextAction.getOptions().buttonColor) reorderedActions.push(contextAction);
});
@@ -53,7 +51,7 @@ export function UnitMouseControlBar(props: {}) {
return (
<>
{open && appState.contextActionSet && Object.keys(appState.contextActionSet.getContextActions()).length > 0 && (
{appState === OlympusState.UNIT_CONTROL && contextActionSet && Object.keys(contextActionSet.getContextActions()).length > 0 && (
<>
<div
className={`
@@ -72,26 +70,26 @@ export function UnitMouseControlBar(props: {}) {
/>
)}
<div className="flex gap-2 overflow-x-auto no-scrollbar p-2" onScroll={(ev) => onScroll(ev.target)} ref={scrollRef}>
{reorderedActions.map((contextAction: ContextAction) => {
{reorderedActions.map((contextActionIt: ContextAction) => {
return (
<OlStateButton
key={contextAction.getId()}
checked={contextAction === appState.contextAction}
icon={contextAction.getIcon()}
tooltip={contextAction.getLabel()}
key={contextActionIt.getId()}
checked={contextActionIt === contextAction}
icon={contextActionIt.getIcon()}
tooltip={contextActionIt.getLabel()}
className={
contextAction.getOptions().buttonColor
contextActionIt.getOptions().buttonColor
? `
border-2
border-${contextAction.getOptions().buttonColor}-500
border-${contextActionIt.getOptions().buttonColor}-500
`
: ""
}
onClick={() => {
if (contextAction.getOptions().executeImmediately) {
contextAction.executeCallback(null, null);
if (contextActionIt.getOptions().executeImmediately) {
contextActionIt.executeCallback(null, null);
} else {
appState.contextAction !== contextAction ? getApp().getMap().setContextAction(contextAction) : getApp().getMap().setContextAction(null);
contextActionIt !== contextAction ? getApp().getMap().setContextAction(contextActionIt) : getApp().getMap().setContextAction(null);
}
}}
/>
@@ -108,7 +106,7 @@ export function UnitMouseControlBar(props: {}) {
/>
)}
</div>
{appState.contextAction && (
{contextAction && (
<div
className={`
absolute left-[50%] top-32 flex min-w-[300px]
@@ -130,7 +128,7 @@ export function UnitMouseControlBar(props: {}) {
md:border-l-[1px] md:px-5
`}
>
{appState.contextAction.getDescription()}
{contextAction.getDescription()}
</div>
</div>
)}

View File

@@ -1,4 +1,4 @@
import React, { MutableRefObject, useContext, useEffect, useRef, useState } from "react";
import React, { MutableRefObject, useEffect, useRef, useState } from "react";
import { Menu } from "./components/menu";
import { Unit } from "../../unit/unit";
import { OlLabelToggle } from "../components/ollabeltoggle";
@@ -50,11 +50,11 @@ import { Radio, TACAN } from "../../interfaces";
import { OlStringInput } from "../components/olstringinput";
import { OlFrequencyInput } from "../components/olfrequencyinput";
import { UnitSink } from "../../audio/unitsink";
import { StateContext } from "../../statecontext";
import { AudioManagerStateChangedEvent, SelectedUnitsChangedEvent, SelectionClearedEvent } from "../../events";
export function UnitControlMenu(props: { open: boolean; onClose: () => void }) {
const appState = useContext(StateContext);
const [selectedUnits, setSelectedUnits] = useState([] as Unit[]);
const [audioManagerState, setAudioManagerState] = useState(false);
const [selectedUnitsData, setSelectedUnitsData] = useState({
desiredAltitude: undefined as undefined | number,
desiredAltitudeType: undefined as undefined | string,
@@ -110,6 +110,12 @@ export function UnitControlMenu(props: { open: boolean; onClose: () => void }) {
var searchBarRef = useRef(null);
useEffect(() => {
SelectedUnitsChangedEvent.on((units) => setSelectedUnits(units));
SelectionClearedEvent.on(() => setSelectedUnits([]));
AudioManagerStateChangedEvent.on((state) => setAudioManagerState(state));
}, []);
useEffect(() => {
if (!searchBarRefState) setSearchBarRefState(searchBarRef);
if (!props.open && selectionBlueprint !== null) setSelectionBlueprint(null);
@@ -154,12 +160,12 @@ export function UnitControlMenu(props: { open: boolean; onClose: () => void }) {
},
} as { [key in keyof typeof selectedUnitsData]: (unit: Unit) => void };
var updatedData = selectedUnitsData;
var updatedData = {};
Object.entries(getters).forEach(([key, getter]) => {
updatedData[key] = getApp()?.getUnitsManager()?.getSelectedUnitsVariable(getter);
});
setSelectedUnitsData(updatedData);
}, [appState.selectedUnits]);
setSelectedUnitsData(updatedData as typeof selectedUnitsData);
}, [selectedUnits]);
/* Count how many units are selected of each type, divided by coalition */
var unitOccurences: {
@@ -172,7 +178,7 @@ export function UnitControlMenu(props: { open: boolean; onClose: () => void }) {
neutral: {},
};
appState.selectedUnits.forEach((unit) => {
selectedUnits.forEach((unit) => {
if (!(unit.getName() in unitOccurences[unit.getCoalition()]))
unitOccurences[unit.getCoalition()][unit.getName()] = { occurences: 1, label: unit.getBlueprint()?.label };
else unitOccurences[unit.getCoalition()][unit.getName()].occurences++;
@@ -196,6 +202,7 @@ export function UnitControlMenu(props: { open: boolean; onClose: () => void }) {
return category === "Helicopter";
});
// TODO: use constants
const minAltitude = 0;
const minSpeed = 0;
@@ -220,13 +227,13 @@ export function UnitControlMenu(props: { open: boolean; onClose: () => void }) {
return (
<Menu
open={props.open}
title={appState.selectedUnits.length > 0 ? `Units selected (x${appState.selectedUnits.length})` : `No units selected`}
title={selectedUnits.length > 0 ? `Units selected (x${selectedUnits.length})` : `No units selected`}
onClose={props.onClose}
canBeHidden={true}
>
<>
{/* ============== Selection tool START ============== */}
{appState.selectedUnits.length == 0 && (
{selectedUnits.length == 0 && (
<div className="flex flex-col gap-4 p-4">
<div className="text-lg text-bold text-gray-200">Selection tool</div>
<div className="text-sm text-gray-400">
@@ -429,7 +436,7 @@ export function UnitControlMenu(props: { open: boolean; onClose: () => void }) {
{/* */}
<>
{/* ============== Unit control menu START ============== */}
{appState.selectedUnits.length > 0 && (
{selectedUnits.length > 0 && (
<>
{/* ============== Units list START ============== */}
<div
@@ -522,7 +529,7 @@ export function UnitControlMenu(props: { open: boolean; onClose: () => void }) {
leftLabel={"AGL"}
rightLabel={"ASL"}
onClick={() => {
appState.selectedUnits.forEach((unit) => {
selectedUnits.forEach((unit) => {
unit.setAltitudeType(selectedUnitsData.desiredAltitudeType === "ASL" ? "AGL" : "ASL");
setSelectedUnitsData({
...selectedUnitsData,
@@ -534,7 +541,7 @@ export function UnitControlMenu(props: { open: boolean; onClose: () => void }) {
</div>
<OlRangeSlider
onChange={(ev) => {
appState.selectedUnits.forEach((unit) => {
selectedUnits.forEach((unit) => {
unit.setAltitude(ftToM(Number(ev.target.value)));
setSelectedUnitsData({
...selectedUnitsData,
@@ -584,7 +591,7 @@ export function UnitControlMenu(props: { open: boolean; onClose: () => void }) {
leftLabel={"GS"}
rightLabel={"CAS"}
onClick={() => {
appState.selectedUnits.forEach((unit) => {
selectedUnits.forEach((unit) => {
unit.setSpeedType(selectedUnitsData.desiredSpeedType === "CAS" ? "GS" : "CAS");
setSelectedUnitsData({
...selectedUnitsData,
@@ -597,7 +604,7 @@ export function UnitControlMenu(props: { open: boolean; onClose: () => void }) {
</div>
<OlRangeSlider
onChange={(ev) => {
appState.selectedUnits.forEach((unit) => {
selectedUnits.forEach((unit) => {
unit.setSpeed(knotsToMs(Number(ev.target.value)));
setSelectedUnitsData({
...selectedUnitsData,
@@ -613,8 +620,8 @@ export function UnitControlMenu(props: { open: boolean; onClose: () => void }) {
</div>
{/* ============== Airspeed selector END ============== */}
{/* ============== Rules of Engagement START ============== */}
{!(appState.selectedUnits.length === 1 && appState.selectedUnits[0].isTanker()) &&
!(appState.selectedUnits.length === 1 && appState.selectedUnits[0].isAWACS()) && (
{!(selectedUnits.length === 1 && selectedUnits[0].isTanker()) &&
!(selectedUnits.length === 1 && selectedUnits[0].isAWACS()) && (
<div className="flex flex-col gap-2">
<span
className={`
@@ -630,7 +637,7 @@ export function UnitControlMenu(props: { open: boolean; onClose: () => void }) {
<OlButtonGroupItem
key={idx}
onClick={() => {
appState.selectedUnits.forEach((unit) => {
selectedUnits.forEach((unit) => {
unit.setROE(ROEs[idx]);
setSelectedUnitsData({
...selectedUnitsData,
@@ -668,7 +675,7 @@ export function UnitControlMenu(props: { open: boolean; onClose: () => void }) {
<OlButtonGroupItem
key={idx}
onClick={() => {
appState.selectedUnits.forEach((unit) => {
selectedUnits.forEach((unit) => {
unit.setReactionToThreat(reactionsToThreat[idx]);
setSelectedUnitsData({
...selectedUnitsData,
@@ -700,7 +707,7 @@ export function UnitControlMenu(props: { open: boolean; onClose: () => void }) {
<OlButtonGroupItem
key={idx}
onClick={() => {
appState.selectedUnits.forEach((unit) => {
selectedUnits.forEach((unit) => {
unit.setEmissionsCountermeasures(emissionsCountermeasures[idx]);
setSelectedUnitsData({
...selectedUnitsData,
@@ -736,7 +743,7 @@ export function UnitControlMenu(props: { open: boolean; onClose: () => void }) {
<OlToggle
toggled={selectedUnitsData.isActiveTanker}
onClick={() => {
appState.selectedUnits.forEach((unit) => {
selectedUnits.forEach((unit) => {
unit.setAdvancedOptions(
!selectedUnitsData.isActiveTanker,
unit.getIsActiveAWACS(),
@@ -770,7 +777,7 @@ export function UnitControlMenu(props: { open: boolean; onClose: () => void }) {
<OlToggle
toggled={selectedUnitsData.isActiveAWACS}
onClick={() => {
appState.selectedUnits.forEach((unit) => {
selectedUnits.forEach((unit) => {
unit.setAdvancedOptions(
unit.getIsActiveTanker(),
!selectedUnitsData.isActiveAWACS,
@@ -789,7 +796,7 @@ export function UnitControlMenu(props: { open: boolean; onClose: () => void }) {
)}
{/* ============== Tanker and AWACS available button END ============== */}
{/* ============== Advanced settings buttons START ============== */}
{appState.selectedUnits.length === 1 && (appState.selectedUnits[0].isTanker() || appState.selectedUnits[0].isAWACS()) && (
{selectedUnits.length === 1 && (selectedUnits[0].isTanker() || selectedUnits[0].isAWACS()) && (
<div className="flex content-center justify-between">
<button
className={`
@@ -800,13 +807,13 @@ export function UnitControlMenu(props: { open: boolean; onClose: () => void }) {
`}
onClick={() => {
setActiveAdvancedSettings({
radio: JSON.parse(JSON.stringify(appState.selectedUnits[0].getRadio())),
TACAN: JSON.parse(JSON.stringify(appState.selectedUnits[0].getTACAN())),
radio: JSON.parse(JSON.stringify(selectedUnits[0].getRadio())),
TACAN: JSON.parse(JSON.stringify(selectedUnits[0].getTACAN())),
});
setShowAdvancedSettings(true);
}}
>
<FaCog className="my-auto" /> {appState.selectedUnits[0].isTanker() ? "Configure tanker settings" : "Configure AWACS settings"}
<FaCog className="my-auto" /> {selectedUnits[0].isTanker() ? "Configure tanker settings" : "Configure AWACS settings"}
</button>
</div>
)}
@@ -834,7 +841,7 @@ export function UnitControlMenu(props: { open: boolean; onClose: () => void }) {
<OlToggle
toggled={selectedUnitsData.scenicAAA}
onClick={() => {
appState.selectedUnits.forEach((unit) => {
selectedUnits.forEach((unit) => {
selectedUnitsData.scenicAAA ? unit.changeSpeed("stop") : unit.scenicAAA();
setSelectedUnitsData({
...selectedUnitsData,
@@ -859,7 +866,7 @@ export function UnitControlMenu(props: { open: boolean; onClose: () => void }) {
<OlToggle
toggled={selectedUnitsData.missOnPurpose}
onClick={() => {
appState.selectedUnits.forEach((unit) => {
selectedUnits.forEach((unit) => {
selectedUnitsData.missOnPurpose ? unit.changeSpeed("stop") : unit.missOnPurpose();
setSelectedUnitsData({
...selectedUnitsData,
@@ -888,7 +895,7 @@ export function UnitControlMenu(props: { open: boolean; onClose: () => void }) {
<OlButtonGroupItem
key={idx}
onClick={() => {
appState.selectedUnits.forEach((unit) => {
selectedUnits.forEach((unit) => {
unit.setShotsScatter(idx + 1);
setSelectedUnitsData({
...selectedUnitsData,
@@ -920,7 +927,7 @@ export function UnitControlMenu(props: { open: boolean; onClose: () => void }) {
<OlButtonGroupItem
key={idx}
onClick={() => {
appState.selectedUnits.forEach((unit) => {
selectedUnits.forEach((unit) => {
unit.setShotsIntensity(idx + 1);
setSelectedUnitsData({
...selectedUnitsData,
@@ -950,7 +957,7 @@ export function UnitControlMenu(props: { open: boolean; onClose: () => void }) {
<OlCoalitionToggle
coalition={selectedUnitsData.operateAs as Coalition}
onClick={() => {
appState.selectedUnits.forEach((unit) => {
selectedUnits.forEach((unit) => {
unit.setOperateAs(selectedUnitsData.operateAs === "blue" ? "red" : "blue");
setSelectedUnitsData({
...selectedUnitsData,
@@ -975,7 +982,7 @@ export function UnitControlMenu(props: { open: boolean; onClose: () => void }) {
<OlToggle
toggled={selectedUnitsData.followRoads}
onClick={() => {
appState.selectedUnits.forEach((unit) => {
selectedUnits.forEach((unit) => {
unit.setFollowRoads(!selectedUnitsData.followRoads);
setSelectedUnitsData({
...selectedUnitsData,
@@ -999,7 +1006,7 @@ export function UnitControlMenu(props: { open: boolean; onClose: () => void }) {
<OlToggle
toggled={selectedUnitsData.onOff}
onClick={() => {
appState.selectedUnits.forEach((unit) => {
selectedUnits.forEach((unit) => {
unit.setOnOff(!selectedUnitsData.onOff);
setSelectedUnitsData({
...selectedUnitsData,
@@ -1022,11 +1029,11 @@ export function UnitControlMenu(props: { open: boolean; onClose: () => void }) {
>
Loudspeakers
</span>
{appState.audioManagerState ? (
{audioManagerState ? (
<OlToggle
toggled={selectedUnitsData.isAudioSink}
onClick={() => {
appState.selectedUnits.forEach((unit) => {
selectedUnits.forEach((unit) => {
if (!selectedUnitsData.isAudioSink) {
getApp()?.getAudioManager().addUnitSink(unit);
setSelectedUnitsData({
@@ -1077,14 +1084,14 @@ export function UnitControlMenu(props: { open: boolean; onClose: () => void }) {
<div className="flex content-center gap-2">
<OlDropdown
label={
appState.selectedUnits[0].isAWACS()
selectedUnits[0].isAWACS()
? ["Overlord", "Magic", "Wizard", "Focus", "Darkstar"][activeAdvancedSettings ? activeAdvancedSettings.radio.callsign - 1 : 0]
: ["Texaco", "Arco", "Shell"][activeAdvancedSettings ? activeAdvancedSettings.radio.callsign - 1 : 0]
}
className="my-auto w-full"
>
<>
{appState.selectedUnits[0].isAWACS() && (
{selectedUnits[0].isAWACS() && (
<>
{["Overlord", "Magic", "Wizard", "Focus", "Darkstar"].map((name, idx) => {
return (
@@ -1103,7 +1110,7 @@ export function UnitControlMenu(props: { open: boolean; onClose: () => void }) {
)}
</>
<>
{appState.selectedUnits[0].isTanker() && (
{selectedUnits[0].isTanker() && (
<>
{["Texaco", "Arco", "Shell"].map((name, idx) => {
return (
@@ -1238,12 +1245,12 @@ export function UnitControlMenu(props: { open: boolean; onClose: () => void }) {
`}
onClick={() => {
if (activeAdvancedSettings)
appState.selectedUnits[0].setAdvancedOptions(
appState.selectedUnits[0].getIsActiveTanker(),
appState.selectedUnits[0].getIsActiveAWACS(),
selectedUnits[0].setAdvancedOptions(
selectedUnits[0].getIsActiveTanker(),
selectedUnits[0].getIsActiveAWACS(),
activeAdvancedSettings.TACAN,
activeAdvancedSettings.radio,
appState.selectedUnits[0].getGeneralSettings()
selectedUnits[0].getGeneralSettings()
);
setActiveAdvancedSettings(null);
setShowAdvancedSettings(false);
@@ -1276,7 +1283,7 @@ export function UnitControlMenu(props: { open: boolean; onClose: () => void }) {
{/* ============== Unit basic options END ============== */}
<>
{/* ============== Fuel/payload/radio section START ============== */}
{appState.selectedUnits.length === 1 && (
{selectedUnits.length === 1 && (
<div
className={`
flex flex-col gap-4 border-l-4 border-l-olympus-100
@@ -1287,27 +1294,23 @@ export function UnitControlMenu(props: { open: boolean; onClose: () => void }) {
<div
className={`
flex content-center gap-2 rounded-full
${appState.selectedUnits[0].getFuel() > 40 && `
bg-green-700
`}
${selectedUnits[0].getFuel() > 40 && `bg-green-700`}
${
appState.selectedUnits[0].getFuel() > 10 &&
appState.selectedUnits[0].getFuel() <= 40 &&
selectedUnits[0].getFuel() > 10 &&
selectedUnits[0].getFuel() <= 40 &&
`bg-yellow-700`
}
${appState.selectedUnits[0].getFuel() <= 10 && `
bg-red-700
`}
${selectedUnits[0].getFuel() <= 10 && `bg-red-700`}
px-2 py-1 text-sm font-bold text-white
`}
>
<FaGasPump className="my-auto" />
{appState.selectedUnits[0].getFuel()}%
{selectedUnits[0].getFuel()}%
</div>
</div>
<div className="flex flex-col gap-2">
{appState.selectedUnits[0].isControlledByOlympus() && (appState.selectedUnits[0].isTanker() || appState.selectedUnits[0].isAWACS()) && (
{selectedUnits[0].isControlledByOlympus() && (selectedUnits[0].isTanker() || selectedUnits[0].isAWACS()) && (
<>
{/* ============== Radio section START ============== */}
<div className="flex content-center justify-between">
@@ -1329,7 +1332,7 @@ export function UnitControlMenu(props: { open: boolean; onClose: () => void }) {
dark:text-gray-300
`}
>
{`${appState.selectedUnits[0].isTanker() ? ["Texaco", "Arco", "Shell"][appState.selectedUnits[0].getRadio().callsign - 1] : ["Overlord", "Magic", "Wizard", "Focus", "Darkstar"][appState.selectedUnits[0].getRadio().callsign - 1]}-${appState.selectedUnits[0].getRadio().callsignNumber}`}
{`${selectedUnits[0].isTanker() ? ["Texaco", "Arco", "Shell"][selectedUnits[0].getRadio().callsign - 1] : ["Overlord", "Magic", "Wizard", "Focus", "Darkstar"][selectedUnits[0].getRadio().callsign - 1]}-${selectedUnits[0].getRadio().callsignNumber}`}
</div>
</div>
</div>
@@ -1352,7 +1355,7 @@ export function UnitControlMenu(props: { open: boolean; onClose: () => void }) {
dark:text-gray-300
`}
>
{`${(appState.selectedUnits[0].getRadio().frequency / 1000000).toFixed(3)} MHz`}
{`${(selectedUnits[0].getRadio().frequency / 1000000).toFixed(3)} MHz`}
</div>
</div>
</div>
@@ -1376,8 +1379,8 @@ export function UnitControlMenu(props: { open: boolean; onClose: () => void }) {
dark:text-gray-300
`}
>
{appState.selectedUnits[0].getTACAN().isOn
? `${appState.selectedUnits[0].getTACAN().channel}${appState.selectedUnits[0].getTACAN().XY} ${appState.selectedUnits[0].getTACAN().callsign}`
{selectedUnits[0].getTACAN().isOn
? `${selectedUnits[0].getTACAN().channel}${selectedUnits[0].getTACAN().XY} ${selectedUnits[0].getTACAN().callsign}`
: "TACAN OFF"}
</div>
</div>
@@ -1386,9 +1389,9 @@ export function UnitControlMenu(props: { open: boolean; onClose: () => void }) {
</>
)}
{/* ============== Payload section START ============== */}
{!appState.selectedUnits[0].isTanker() &&
!appState.selectedUnits[0].isAWACS() &&
appState.selectedUnits[0].getAmmo().map((ammo, idx) => {
{!selectedUnits[0].isTanker() &&
!selectedUnits[0].isAWACS() &&
selectedUnits[0].getAmmo().map((ammo, idx) => {
return (
<div className="flex content-center gap-2" key={idx}>
<div

View File

@@ -8,7 +8,7 @@ import { OlDropdownItem, OlDropdown } from "../components/oldropdown";
import { LoadoutBlueprint, UnitBlueprint } from "../../interfaces";
import { Coalition } from "../../types/types";
import { getApp } from "../../olympusapp";
import { ftToM, getUnitCategoryByBlueprint } from "../../other/utils";
import { ftToM } from "../../other/utils";
import { LatLng } from "leaflet";
import { Airbase } from "../../mission/airbase";
import { OlympusState, SpawnSubState } from "../../constants/constants";
@@ -39,7 +39,7 @@ export function UnitSpawnMenu(props: { blueprint: UnitBlueprint; spawnAtLocation
getApp()
?.getMap()
?.setSpawnRequestTable({
category: getUnitCategoryByBlueprint(props.blueprint),
category: props.blueprint.category,
unit: {
unitType: props.blueprint.name,
location: new LatLng(0, 0), // This will be filled when the user clicks on the map to spawn the unit
@@ -58,13 +58,13 @@ export function UnitSpawnMenu(props: { blueprint: UnitBlueprint; spawnAtLocation
if (getApp().getState() === OlympusState.SPAWN) getApp().setState(OlympusState.IDLE);
}
}
});
}, [props.blueprint, spawnAltitude, spawnLoadoutName, spawnCoalition]);
function spawnAtAirbase() {
getApp()
.getUnitsManager()
.spawnUnits(
getUnitCategoryByBlueprint(props.blueprint),
props.blueprint.category,
[
{
unitType: props.blueprint.name,

View File

@@ -1,8 +1,6 @@
import React, { useEffect, useState } from "react";
import "./ui.css";
import { StateProvider } from "../statecontext";
import { Header } from "./panels/header";
import { SpawnMenu } from "./panels/spawnmenu";
import { UnitControlMenu } from "./panels/unitcontrolmenu";
@@ -13,10 +11,8 @@ import { MapHiddenTypes, MapOptions } from "../types/types";
import {
BLUE_COMMANDER,
GAME_MASTER,
MAP_HIDDEN_TYPES_DEFAULTS,
MAP_OPTIONS_DEFAULTS,
NO_SUBSTATE,
OlympusEvent,
OlympusState,
OlympusSubState,
RED_COMMANDER,
@@ -26,7 +22,7 @@ import { getApp, setupApp } from "../olympusapp";
import { LoginModal } from "./modals/login";
import { sha256 } from "js-sha256";
import { MiniMapPanel } from "./panels/minimappanel";
import { UnitMouseControlBar } from "./panels/unitmousecontrolbar";
import { UnitControlBar } from "./panels/unitcontrolbar";
import { DrawingMenu } from "./panels/drawingmenu";
import { ControlsPanel } from "./panels/controlspanel";
import { MapContextMenu } from "./contextmenus/mapcontextmenu";
@@ -40,24 +36,8 @@ import { UnitExplosionMenu } from "./panels/unitexplosionmenu";
import { JTACMenu } from "./panels/jtacmenu";
import {
AppStateChangedEvent,
AudioManagerStateChangedEvent,
AudioSinksChangedEvent,
AudioSourcesChangedEvent,
ConfigLoadedEvent,
ContextActionChangedEvent,
ContextActionSetChangedEvent,
HiddenTypesChangedEvent,
MapOptionsChangedEvent,
MapSourceChangedEvent,
SelectedUnitsChangedEvent,
ServerStatusUpdatedEvent,
UnitSelectedEvent,
MapOptionsChangedEvent
} from "../events";
import { ServerStatus } from "../interfaces";
import { AudioSource } from "../audio/audiosource";
import { AudioSink } from "../audio/audiosink";
import { ContextAction } from "../unit/contextaction";
import { ContextActionSet } from "../unit/contextactionset";
export type OlympusUIState = {
mainMenuVisible: boolean;
@@ -74,17 +54,7 @@ export type OlympusUIState = {
export function UI() {
const [appState, setAppState] = useState(OlympusState.NOT_INITIALIZED);
const [appSubState, setAppSubState] = useState(NO_SUBSTATE as OlympusSubState);
const [mapHiddenTypes, setMapHiddenTypes] = useState(MAP_HIDDEN_TYPES_DEFAULTS);
const [mapOptions, setMapOptions] = useState(MAP_OPTIONS_DEFAULTS);
const [mapSources, setMapSources] = useState([] as string[]);
const [activeMapSource, setActiveMapSource] = useState("");
const [selectedUnits, setSelectedUnits] = useState([] as Unit[]);
const [audioSources, setAudioSources] = useState([] as AudioSource[]);
const [audioSinks, setAudioSinks] = useState([] as AudioSink[]);
const [audioManagerState, setAudioManagerState] = useState(false);
const [serverStatus, setServerStatus] = useState({} as ServerStatus);
const [contextActionSet, setContextActionsSet] = useState(null as ContextActionSet | null);
const [contextAction, setContextActions] = useState(null as ContextAction | null);
const [checkingPassword, setCheckingPassword] = useState(false);
const [loginError, setLoginError] = useState(false);
@@ -105,22 +75,7 @@ export function UI() {
setAppState(state);
setAppSubState(subState);
});
ConfigLoadedEvent.on(() => {
let config = getApp().getConfig();
var sources = Object.keys(config.mapMirrors).concat(Object.keys(config.mapLayers));
setMapSources(sources);
setActiveMapSource(sources[0]);
});
HiddenTypesChangedEvent.on((hiddenTypes) => setMapHiddenTypes({ ...hiddenTypes }));
MapOptionsChangedEvent.on((mapOptions) => setMapOptions({ ...mapOptions }));
MapSourceChangedEvent.on((source) => setActiveMapSource(source));
SelectedUnitsChangedEvent.on((units) => setSelectedUnits(units));
AudioSourcesChangedEvent.on((sources) => setAudioSources(sources));
AudioSinksChangedEvent.on((sinks) => setAudioSinks(sinks));
AudioManagerStateChangedEvent.on((state) => setAudioManagerState(state));
ServerStatusUpdatedEvent.on((status) => setServerStatus(status));
ContextActionSetChangedEvent.on((contextActionSet) => setContextActionsSet(contextActionSet));
ContextActionChangedEvent.on((contextAction) => setContextActions(contextAction));
document.addEventListener("showProtectionPrompt", (ev: CustomEventInit) => {
setProtectionPromptVisible(true);
@@ -168,97 +123,75 @@ export function UI() {
`}
onLoad={setupApp}
>
<StateProvider
value={{
appState,
appSubState,
mapOptions,
mapHiddenTypes,
mapSources,
activeMapSource,
selectedUnits,
audioSources,
audioSinks,
audioManagerState,
serverStatus,
contextActionSet,
contextAction,
}}
>
<Header />
<div className="flex h-full w-full flex-row-reverse">
{appState === OlympusState.LOGIN && (
<>
<div
className={`
fixed left-0 top-0 z-30 h-full w-full bg-[#111111]/95
`}
></div>
<LoginModal
onLogin={(password) => {
checkPassword(password);
}}
onContinue={(username) => {
connect(username);
}}
onBack={() => {
setCommandMode(null);
}}
checkingPassword={checkingPassword}
loginError={loginError}
commandMode={commandMode}
/>
</>
)}
{protectionPromptVisible && (
<>
<div
className={`
fixed left-0 top-0 z-30 h-full w-full bg-[#111111]/95
`}
></div>
<ProtectionPrompt
onContinue={(units) => {
protectionCallback(units);
setProtectionPromptVisible(false);
}}
onBack={() => {
setProtectionPromptVisible(false);
}}
units={protectionUnits}
/>
</>
)}
<div id="map-container" className="z-0 h-full w-screen" />
<MainMenu open={appState === OlympusState.MAIN_MENU} onClose={() => getApp().setState(OlympusState.IDLE)} />
<SpawnMenu open={appState === OlympusState.SPAWN} onClose={() => getApp().setState(OlympusState.IDLE)} />
<OptionsMenu open={appState === OlympusState.OPTIONS} onClose={() => getApp().setState(OlympusState.IDLE)} options={mapOptions} />
<Header />
<div className="flex h-full w-full flex-row-reverse">
{appState === OlympusState.LOGIN && (
<>
<div
className={`fixed left-0 top-0 z-30 h-full w-full bg-[#111111]/95`}
></div>
<LoginModal
onLogin={(password) => {
checkPassword(password);
}}
onContinue={(username) => {
connect(username);
}}
onBack={() => {
setCommandMode(null);
}}
checkingPassword={checkingPassword}
loginError={loginError}
commandMode={commandMode}
/>
</>
)}
{protectionPromptVisible && (
<>
<div
className={`fixed left-0 top-0 z-30 h-full w-full bg-[#111111]/95`}
></div>
<ProtectionPrompt
onContinue={(units) => {
protectionCallback(units);
setProtectionPromptVisible(false);
}}
onBack={() => {
setProtectionPromptVisible(false);
}}
units={protectionUnits}
/>
</>
)}
<div id="map-container" className="z-0 h-full w-screen" />
<MainMenu open={appState === OlympusState.MAIN_MENU} onClose={() => getApp().setState(OlympusState.IDLE)} />
<SpawnMenu open={appState === OlympusState.SPAWN} onClose={() => getApp().setState(OlympusState.IDLE)} />
<OptionsMenu open={appState === OlympusState.OPTIONS} onClose={() => getApp().setState(OlympusState.IDLE)} options={mapOptions} /* TODO remove *//>
<UnitControlMenu
open={appState === OlympusState.UNIT_CONTROL && appSubState !== UnitControlSubState.FORMATION}
onClose={() => getApp().setState(OlympusState.IDLE)}
/>
<FormationMenu
open={appState === OlympusState.UNIT_CONTROL && appSubState === UnitControlSubState.FORMATION}
leader={formationLeader}
wingmen={formationWingmen}
onClose={() => getApp().setState(OlympusState.IDLE)}
/>
<UnitControlMenu
open={appState === OlympusState.UNIT_CONTROL && appSubState !== UnitControlSubState.FORMATION}
onClose={() => getApp().setState(OlympusState.IDLE)}
/>
<FormationMenu
open={appState === OlympusState.UNIT_CONTROL && appSubState === UnitControlSubState.FORMATION}
leader={formationLeader}
wingmen={formationWingmen}
onClose={() => getApp().setState(OlympusState.IDLE)}
/>
<DrawingMenu open={appState === OlympusState.DRAW} onClose={() => getApp().setState(OlympusState.IDLE)} />
<AirbaseMenu open={appState === OlympusState.AIRBASE} onClose={() => getApp().setState(OlympusState.IDLE)} airbase={airbase} />
<AudioMenu open={appState === OlympusState.AUDIO} onClose={() => getApp().setState(OlympusState.IDLE)} />
<DrawingMenu open={appState === OlympusState.DRAW} onClose={() => getApp().setState(OlympusState.IDLE)} />
<AirbaseMenu open={appState === OlympusState.AIRBASE} onClose={() => getApp().setState(OlympusState.IDLE)} airbase={airbase} /* TODO remove */ />
<AudioMenu open={appState === OlympusState.AUDIO} onClose={() => getApp().setState(OlympusState.IDLE)} />
{/* TODO} <UnitExplosionMenu open={appState === OlympusState.MAIN_MENU} units={unitExplosionUnits} onClose={() => getApp().setState(OlympusState.IDLE)} /> {*/}
<JTACMenu open={appState === OlympusState.JTAC} onClose={() => getApp().setState(OlympusState.IDLE)} />
{/* TODO} <UnitExplosionMenu open={appState === OlympusState.MAIN_MENU} units={unitExplosionUnits} onClose={() => getApp().setState(OlympusState.IDLE)} /> {*/}
<JTACMenu open={appState === OlympusState.JTAC} onClose={() => getApp().setState(OlympusState.IDLE)} />
<MiniMapPanel />
<ControlsPanel />
<UnitMouseControlBar />
<MapContextMenu />
<SideBar />
</div>
</StateProvider>
<MiniMapPanel />
<ControlsPanel />
<UnitControlBar />
<MapContextMenu />
<SideBar />
</div>
</div>
);
}