More refactoring of events

This commit is contained in:
Pax1601
2024-10-28 07:53:09 +01:00
parent 14c0a2f1e8
commit 7f5873b5b8
32 changed files with 1010 additions and 891 deletions

View File

@@ -7,6 +7,7 @@ import { CONTEXT_ACTION_COLORS } from "../../constants/constants";
import { OlDropdownItem } from "../components/oldropdown";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { LatLng } from "leaflet";
import { SelectionClearedEvent } from "../../events";
export function MapContextMenu(props: {}) {
const [open, setOpen] = useState(false);
@@ -71,7 +72,7 @@ export function MapContextMenu(props: {}) {
setOpen(false);
});
document.addEventListener("clearSelection", () => {
SelectionClearedEvent.on(() => {
setOpen(false);
});
}, []);

View File

@@ -11,6 +11,7 @@ import { UnitSinkPanel } from "./components/unitsinkpanel";
import { UnitSink } from "../../audio/unitsink";
import { FaMinus, FaVolumeHigh } from "react-icons/fa6";
import { getRandomColor } from "../../other/utils";
import { AudioManagerStateChangedEvent, AudioSinksChangedEvent, AudioSourcesChangedEvent } from "../../events";
let shortcutKeys = ["Z", "X", "C", "V", "B", "N", "M", "K", "L"];
@@ -36,7 +37,7 @@ export function AudioMenu(props: { open: boolean; onClose: () => void; children?
useEffect(() => {
/* Force a rerender */
document.addEventListener("audioSinksUpdated", () => {
AudioSinksChangedEvent.on(() => {
setSinks(
getApp()
?.getAudioManager()
@@ -47,7 +48,7 @@ export function AudioMenu(props: { open: boolean; onClose: () => void; children?
});
/* Force a rerender */
document.addEventListener("audioSourcesUpdated", () => {
AudioSourcesChangedEvent.on(() => {
setSources(
getApp()
?.getAudioManager()
@@ -56,7 +57,7 @@ export function AudioMenu(props: { open: boolean; onClose: () => void; children?
);
});
document.addEventListener("audioManagerStateChanged", () => {
AudioManagerStateChangedEvent.on(() => {
setAudioManagerEnabled(getApp().getAudioManager().isRunning());
});
}, []);

View File

@@ -2,6 +2,7 @@ import React, { useEffect, useState } from "react";
import { getApp } from "../../olympusapp";
import { IconDefinition } from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { AppStateChangedEvent } from "../../events";
export function ControlsPanel(props: {}) {
const [controls, setControls] = useState(
@@ -19,7 +20,7 @@ export function ControlsPanel(props: {}) {
});
useEffect(() => {
document.addEventListener("appStateChanged", (ev) => {
AppStateChangedEvent.on(() => {
setControls(getApp().getMap().getCurrentControls());
});
}, []);

View File

@@ -13,10 +13,10 @@ 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";
export function DrawingMenu(props: { open: boolean; onClose: () => void }) {
const [appState, setAppState] = useState(OlympusState.NOT_INITIALIZED);
const [appSubState, setAppSubState] = useState(NO_SUBSTATE);
const [activeCoalitionArea, setActiveCoalitionArea] = useState(null as null | CoalitionPolygon | CoalitionCircle);
const [areaCoalition, setAreaCoalition] = useState("blue" as Coalition);
const [IADSDensity, setIADSDensity] = useState(50);
@@ -32,276 +32,275 @@ export function DrawingMenu(props: { open: boolean; onClose: () => void }) {
// TODO
///* If we are not in polygon drawing mode, force the draw polygon button off */
//if (drawingPolygon && getApp().getState() !== COALITIONAREA_DRAW_POLYGON) setDrawingPolygon(false);
//
//
///* If we are not in circle drawing mode, force the draw circle button off */
//if (drawingCircle && getApp().getState() !== COALITIONAREA_DRAW_CIRCLE) setDrawingCircle(false);
//
//
///* If we are not in any drawing mode, force the map in edit mode */
//if (props.open && !drawingPolygon && !drawingCircle) getApp().getMap().setState(COALITIONAREA_EDIT);
//
//
///* Align the state of the coalition toggle to the coalition of the area */
//if (activeCoalitionArea && activeCoalitionArea?.getCoalition() !== areaCoalition) setAreaCoalition(activeCoalitionArea?.getCoalition());
}
});
useEffect(() => {
document.addEventListener("coalitionAreaSelected", (event: any) => {
setActiveCoalitionArea(event.detail);
});
document.addEventListener("appStateChanged", (ev: CustomEventInit) => {
setAppState(ev.detail.state);
setAppSubState(ev.detail.subState);
});
CoalitionAreaSelectedEvent.on((coalitionArea) => setActiveCoalitionArea(coalitionArea));
}, []);
return (
<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>
)}
</>
<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"
<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={() => {
getApp().getMap().deleteCoalitionArea(activeCoalitionArea);
setActiveCoalitionArea(null);
if (appState.appSubState === DrawSubState.DRAW_POLYGON) getApp().setState(OlympusState.DRAW, DrawSubState.EDIT);
else getApp().setState(OlympusState.DRAW, DrawSubState.DRAW_POLYGON);
}}
>
<FaTrash className={`text-gray-50`}></FaTrash>
</div>
<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>
<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>
{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>
</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
<input
type="text"
className={`
font-bold
dark:text-blue-500
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
`}
>
{IADSDistribution}%
</div>
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>
<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>
</Menu>
)}
</StateConsumer>
);
}

View File

@@ -10,6 +10,7 @@ import { FaMousePointer } from "react-icons/fa";
import { OlLocation } from "../components/ollocation";
import { FaBullseye } from "react-icons/fa6";
import { JTACSubState, OlympusState } from "../../constants/constants";
import { AppStateChangedEvent } from "../../events";
export function JTACMenu(props: { open: boolean; onClose: () => void; children?: JSX.Element | JSX.Element[] }) {
const [referenceSystem, setReferenceSystem] = useState("LatLngDec");
@@ -23,24 +24,8 @@ export function JTACMenu(props: { open: boolean; onClose: () => void; children?:
const [type, setType] = useState("Type 1");
useEffect(() => {
document.addEventListener("selectJTACTarget", (ev: CustomEventInit) => {
setTargetLocation(null);
setTargetUnit(null);
if (ev.detail.location) setTargetLocation(ev.detail.location);
if (ev.detail.unit) setTargetUnit(ev.detail.unit);
});
document.addEventListener("selectJTACECHO", (ev: CustomEventInit) => {
setECHO(ev.detail);
});
document.addEventListener("selectJTACIP", (ev: CustomEventInit) => {
setIP(ev.detail);
});
document.addEventListener("appStateChanged", (ev: CustomEventInit) => {
if (ev.detail.subState === JTACSubState.SELECT_TARGET) {
AppStateChangedEvent.on((state, subState) => {
if (subState === JTACSubState.SELECT_TARGET) {
setTargetLocation(null);
setTargetUnit(null);
}

View File

@@ -1,34 +1,14 @@
import React, { useState, useEffect } from "react";
import React, { useState, useEffect, useContext } from "react";
import { zeroAppend } from "../../other/utils";
import { DateAndTime } from "../../interfaces";
import { getApp } from "../../olympusapp";
import { FaChevronDown, FaChevronUp } from "react-icons/fa6";
import { StateContext } from "../../statecontext";
export function MiniMapPanel(props: {}) {
const [frameRate, setFrameRate] = useState(0);
const [load, setLoad] = useState(0);
const [elapsedTime, setElapsedTime] = useState(0);
const [missionTime, setMissionTime] = useState({
h: 0,
m: 0,
s: 0,
} as DateAndTime["time"]);
const [connected, setConnected] = useState(false);
const [paused, setPaused] = useState(false);
const [showMissionTime, setShowMissionTime] = useState(false);
const [showMinimap, setShowMinimap] = useState(false);
const appState = useContext(StateContext)
useEffect(() => {
document.addEventListener("serverStatusUpdated", (ev) => {
const detail = (ev as CustomEvent).detail;
setFrameRate(detail.frameRate);
setLoad(detail.load);
setElapsedTime(detail.elapsedTime);
setMissionTime(detail.missionTime);
setConnected(detail.connected);
setPaused(detail.paused);
});
}, []);
const [showMissionTime, setShowMissionTime] = useState(false);
// A bit of a hack to set the rounded borders to the minimap
useEffect(() => {
@@ -38,55 +18,51 @@ export function MiniMapPanel(props: {}) {
}
});
document.addEventListener("mapOptionsChanged", (event) => {
setShowMinimap(getApp().getMap().getOptions().showMinimap);
});
// Compute the time string depending on mission or elapsed time
let hours = 0;
let minutes = 0;
let seconds = 0;
if (showMissionTime) {
hours = missionTime.h;
minutes = missionTime.m;
seconds = missionTime.s;
hours = appState.serverStatus.missionTime.h;
minutes = appState.serverStatus.missionTime.m;
seconds = appState.serverStatus.missionTime.s;
} else {
hours = Math.floor(elapsedTime / 3600);
minutes = Math.floor(elapsedTime / 60) % 60;
seconds = Math.round(elapsedTime) % 60;
hours = Math.floor(appState.serverStatus.elapsedTime / 3600);
minutes = Math.floor(appState.serverStatus.elapsedTime / 60) % 60;
seconds = Math.round(appState.serverStatus.elapsedTime) % 60;
}
let timeString = `${zeroAppend(hours, 2)}:${zeroAppend(minutes, 2)}:${zeroAppend(seconds, 2)}`;
// Choose frame rate string color
let frameRateColor = "#8BFF63";
if (frameRate < 30) frameRateColor = "#F05252";
else if (frameRate >= 30 && frameRate < 60) frameRateColor = "#FF9900";
if (appState.serverStatus.frameRate < 30) frameRateColor = "#F05252";
else if (appState.serverStatus.frameRate >= 30 && appState.serverStatus.frameRate < 60) frameRateColor = "#FF9900";
// Choose load string color
let loadColor = "#8BFF63";
if (load > 1000) loadColor = "#F05252";
else if (load >= 100 && load < 1000) loadColor = "#FF9900";
if (appState.serverStatus.load > 1000) loadColor = "#F05252";
else if (appState.serverStatus.load >= 100 && appState.serverStatus.load < 1000) loadColor = "#FF9900";
return (
<div
onClick={() => setShowMissionTime(!showMissionTime)}
className={`
absolute right-[10px]
${showMinimap ? `top-[232px]` : `top-[70px]`}
${appState.mapOptions.showMinimap ? `top-[232px]` : `top-[70px]`}
flex w-[288px] items-center justify-between
${showMinimap ? `rounded-b-lg` : `rounded-lg`}
${appState.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
`}
>
{!connected ? (
{!appState.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>
) : paused ? (
) : appState.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
@@ -96,13 +72,13 @@ export function MiniMapPanel(props: {}) {
<div className="flex gap-2 font-semibold">
FPS:
<span style={{ color: frameRateColor }} className={`font-semibold`}>
{frameRate}
{appState.serverStatus.frameRate}
</span>
</div>
<div className="flex gap-2 font-semibold">
Load:
<span style={{ color: loadColor }} className={`font-semibold`}>
{load}
{appState.serverStatus.load}
</span>
</div>
<div className="flex gap-2 font-semibold">
@@ -111,7 +87,7 @@ export function MiniMapPanel(props: {}) {
<div className={`relative h-4 w-4 rounded-full bg-[#8BFF63]`}></div>
</>
)}
{showMinimap ? (
{appState.mapOptions.showMinimap ? (
<FaChevronUp
onClick={() => {
getApp().getMap().setOption("showMinimap", false);

View File

@@ -22,6 +22,7 @@ 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";
enum Accordion {
NONE,
@@ -67,8 +68,8 @@ export function SpawnMenu(props: { open: boolean; onClose: () => void; children?
});
useEffect(() => {
document.addEventListener("appStateChanged", (ev: CustomEventInit) => {
if (ev.detail.subState === NO_SUBSTATE) {
AppStateChangedEvent.on((state, subState) => {
if (subState === NO_SUBSTATE) {
setBlueprint(null);
setEffect(null);
}

View File

@@ -1,4 +1,4 @@
import React, { MutableRefObject, useEffect, useRef, useState } from "react";
import React, { MutableRefObject, useContext, useEffect, useRef, useState } from "react";
import { Menu } from "./components/menu";
import { Unit } from "../../unit/unit";
import { OlLabelToggle } from "../components/ollabeltoggle";
@@ -50,9 +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";
export function UnitControlMenu(props: { open: boolean; onClose: () => void }) {
const [selectedUnits, setSelectedUnits] = useState([] as Unit[]);
const appState = useContext(StateContext);
const [selectedUnitsData, setSelectedUnitsData] = useState({
desiredAltitude: undefined as undefined | number,
desiredAltitudeType: undefined as undefined | string,
@@ -105,7 +107,6 @@ export function UnitControlMenu(props: { open: boolean; onClose: () => void }) {
const [filterString, setFilterString] = useState("");
const [showAdvancedSettings, setShowAdvancedSettings] = useState(false);
const [activeAdvancedSettings, setActiveAdvancedSettings] = useState(null as null | { radio: Radio; TACAN: TACAN });
const [audioManagerEnabled, setAudioManagerEnabled] = useState(false);
var searchBarRef = useRef(null);
@@ -115,83 +116,26 @@ export function UnitControlMenu(props: { open: boolean; onClose: () => void }) {
if (!props.open && filterString !== "") setFilterString("");
});
useEffect(() => {
/* When a unit is selected, update the data */
document.addEventListener("unitsSelection", (ev: CustomEventInit) => {
setSelectedUnits(ev.detail as Unit[]);
updateData();
});
/* When a unit is deselected, refresh the view */
document.addEventListener("unitDeselection", (ev: CustomEventInit) => {
window.setTimeout(() => updateData(), 200);
});
/* When all units are deselected clean the view */
document.addEventListener("clearSelection", () => {
setSelectedUnits([]);
});
document.addEventListener("audioManagerStateChanged", () => {
setAudioManagerEnabled(getApp().getAudioManager().isRunning());
});
}, []);
useEffect(() => {
setShowAdvancedSettings(false);
}, [selectedUnits])
/* Update the current values of the shown data */
function updateData() {
const getters = {
desiredAltitude: (unit: Unit) => {
return Math.round(mToFt(unit.getDesiredAltitude()));
},
desiredAltitudeType: (unit: Unit) => {
return unit.getDesiredAltitudeType();
},
desiredSpeed: (unit: Unit) => {
return Math.round(msToKnots(unit.getDesiredSpeed()));
},
desiredSpeedType: (unit: Unit) => {
return unit.getDesiredSpeedType();
},
ROE: (unit: Unit) => {
return unit.getROE();
},
reactionToThreat: (unit: Unit) => {
return unit.getReactionToThreat();
},
emissionsCountermeasures: (unit: Unit) => {
return unit.getEmissionsCountermeasures();
},
scenicAAA: (unit: Unit) => {
return unit.getState() === "scenic-aaa";
},
missOnPurpose: (unit: Unit) => {
return unit.getState() === "miss-on-purpose";
},
shotsScatter: (unit: Unit) => {
return unit.getShotsScatter();
},
shotsIntensity: (unit: Unit) => {
return unit.getShotsIntensity();
},
operateAs: (unit: Unit) => {
return unit.getOperateAs();
},
followRoads: (unit: Unit) => {
return unit.getFollowRoads();
},
isActiveAWACS: (unit: Unit) => {
return unit.getIsActiveAWACS();
},
isActiveTanker: (unit: Unit) => {
return unit.getIsActiveTanker();
},
onOff: (unit: Unit) => {
return unit.getOnOff();
},
desiredAltitude: (unit: Unit) => Math.round(mToFt(unit.getDesiredAltitude())),
desiredAltitudeType: (unit: Unit) => unit.getDesiredAltitudeType(),
desiredSpeed: (unit: Unit) => Math.round(msToKnots(unit.getDesiredSpeed())),
desiredSpeedType: (unit: Unit) => unit.getDesiredSpeedType(),
ROE: (unit: Unit) => unit.getROE(),
reactionToThreat: (unit: Unit) => unit.getReactionToThreat(),
emissionsCountermeasures: (unit: Unit) => unit.getEmissionsCountermeasures(),
scenicAAA: (unit: Unit) => unit.getState() === "scenic-aaa",
missOnPurpose: (unit: Unit) => unit.getState() === "miss-on-purpose",
shotsScatter: (unit: Unit) => unit.getShotsScatter(),
shotsIntensity: (unit: Unit) => unit.getShotsIntensity(),
operateAs: (unit: Unit) => unit.getOperateAs(),
followRoads: (unit: Unit) => unit.getFollowRoads(),
isActiveAWACS: (unit: Unit) => unit.getIsActiveAWACS(),
isActiveTanker: (unit: Unit) => unit.getIsActiveTanker(),
onOff: (unit: Unit) => unit.getOnOff(),
isAudioSink: (unit: Unit) => {
return (
getApp()
@@ -215,7 +159,7 @@ export function UnitControlMenu(props: { open: boolean; onClose: () => void }) {
updatedData[key] = getApp()?.getUnitsManager()?.getSelectedUnitsVariable(getter);
});
setSelectedUnitsData(updatedData);
}
}, [appState.selectedUnits]);
/* Count how many units are selected of each type, divided by coalition */
var unitOccurences: {
@@ -228,7 +172,7 @@ export function UnitControlMenu(props: { open: boolean; onClose: () => void }) {
neutral: {},
};
selectedUnits.forEach((unit) => {
appState.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++;
@@ -236,7 +180,7 @@ export function UnitControlMenu(props: { open: boolean; onClose: () => void }) {
const selectedCategories = getApp()?.getUnitsManager()?.getSelectedUnitsCategories() ?? [];
const [filteredAircraft, filteredHelicopters, filteredAirDefense, filteredGroundUnits, filteredNavyUnits] = [{}, {}, {}, {}, {}] // TODOgetUnitsByLabel(filterString);
const [filteredAircraft, filteredHelicopters, filteredAirDefense, filteredGroundUnits, filteredNavyUnits] = [{}, {}, {}, {}, {}]; // TODOgetUnitsByLabel(filterString);
const mergedFilteredUnits = Object.assign({}, filteredAircraft, filteredHelicopters, filteredAirDefense, filteredGroundUnits, filteredNavyUnits) as {
[key: string]: UnitBlueprint;
@@ -276,13 +220,13 @@ export function UnitControlMenu(props: { open: boolean; onClose: () => void }) {
return (
<Menu
open={props.open}
title={selectedUnits.length > 0 ? `Units selected (x${selectedUnits.length})` : `No units selected`}
title={appState.selectedUnits.length > 0 ? `Units selected (x${appState.selectedUnits.length})` : `No units selected`}
onClose={props.onClose}
canBeHidden={true}
>
<>
{/* ============== Selection tool START ============== */}
{selectedUnits.length == 0 && (
{appState.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">
@@ -485,7 +429,7 @@ export function UnitControlMenu(props: { open: boolean; onClose: () => void }) {
{/* */}
<>
{/* ============== Unit control menu START ============== */}
{selectedUnits.length > 0 && (
{appState.selectedUnits.length > 0 && (
<>
{/* ============== Units list START ============== */}
<div
@@ -578,7 +522,7 @@ export function UnitControlMenu(props: { open: boolean; onClose: () => void }) {
leftLabel={"AGL"}
rightLabel={"ASL"}
onClick={() => {
selectedUnits.forEach((unit) => {
appState.selectedUnits.forEach((unit) => {
unit.setAltitudeType(selectedUnitsData.desiredAltitudeType === "ASL" ? "AGL" : "ASL");
setSelectedUnitsData({
...selectedUnitsData,
@@ -590,7 +534,7 @@ export function UnitControlMenu(props: { open: boolean; onClose: () => void }) {
</div>
<OlRangeSlider
onChange={(ev) => {
selectedUnits.forEach((unit) => {
appState.selectedUnits.forEach((unit) => {
unit.setAltitude(ftToM(Number(ev.target.value)));
setSelectedUnitsData({
...selectedUnitsData,
@@ -640,7 +584,7 @@ export function UnitControlMenu(props: { open: boolean; onClose: () => void }) {
leftLabel={"GS"}
rightLabel={"CAS"}
onClick={() => {
selectedUnits.forEach((unit) => {
appState.selectedUnits.forEach((unit) => {
unit.setSpeedType(selectedUnitsData.desiredSpeedType === "CAS" ? "GS" : "CAS");
setSelectedUnitsData({
...selectedUnitsData,
@@ -653,7 +597,7 @@ export function UnitControlMenu(props: { open: boolean; onClose: () => void }) {
</div>
<OlRangeSlider
onChange={(ev) => {
selectedUnits.forEach((unit) => {
appState.selectedUnits.forEach((unit) => {
unit.setSpeed(knotsToMs(Number(ev.target.value)));
setSelectedUnitsData({
...selectedUnitsData,
@@ -669,38 +613,39 @@ export function UnitControlMenu(props: { open: boolean; onClose: () => void }) {
</div>
{/* ============== Airspeed selector END ============== */}
{/* ============== Rules of Engagement START ============== */}
{!(selectedUnits.length === 1 && selectedUnits[0].isTanker()) && !(selectedUnits.length === 1 && selectedUnits[0].isAWACS()) && (
<div className="flex flex-col gap-2">
<span
className={`
my-auto font-normal
dark:text-white
`}
>
Rules of engagement
</span>
<OlButtonGroup>
{[olButtonsRoeHold, olButtonsRoeReturn, olButtonsRoeDesignated, olButtonsRoeFree].map((icon, idx) => {
return (
<OlButtonGroupItem
key={idx}
onClick={() => {
selectedUnits.forEach((unit) => {
unit.setROE(ROEs[idx]);
setSelectedUnitsData({
...selectedUnitsData,
ROE: ROEs[idx],
{!(appState.selectedUnits.length === 1 && appState.selectedUnits[0].isTanker()) &&
!(appState.selectedUnits.length === 1 && appState.selectedUnits[0].isAWACS()) && (
<div className="flex flex-col gap-2">
<span
className={`
my-auto font-normal
dark:text-white
`}
>
Rules of engagement
</span>
<OlButtonGroup>
{[olButtonsRoeHold, olButtonsRoeReturn, olButtonsRoeDesignated, olButtonsRoeFree].map((icon, idx) => {
return (
<OlButtonGroupItem
key={idx}
onClick={() => {
appState.selectedUnits.forEach((unit) => {
unit.setROE(ROEs[idx]);
setSelectedUnitsData({
...selectedUnitsData,
ROE: ROEs[idx],
});
});
});
}}
active={selectedUnitsData.ROE === ROEs[idx]}
icon={icon}
/>
);
})}
</OlButtonGroup>
</div>
)}
}}
active={selectedUnitsData.ROE === ROEs[idx]}
icon={icon}
/>
);
})}
</OlButtonGroup>
</div>
)}
{/* ============== Rules of Engagement END ============== */}
{selectedCategories.every((category) => {
@@ -723,7 +668,7 @@ export function UnitControlMenu(props: { open: boolean; onClose: () => void }) {
<OlButtonGroupItem
key={idx}
onClick={() => {
selectedUnits.forEach((unit) => {
appState.selectedUnits.forEach((unit) => {
unit.setReactionToThreat(reactionsToThreat[idx]);
setSelectedUnitsData({
...selectedUnitsData,
@@ -755,7 +700,7 @@ export function UnitControlMenu(props: { open: boolean; onClose: () => void }) {
<OlButtonGroupItem
key={idx}
onClick={() => {
selectedUnits.forEach((unit) => {
appState.selectedUnits.forEach((unit) => {
unit.setEmissionsCountermeasures(emissionsCountermeasures[idx]);
setSelectedUnitsData({
...selectedUnitsData,
@@ -791,7 +736,7 @@ export function UnitControlMenu(props: { open: boolean; onClose: () => void }) {
<OlToggle
toggled={selectedUnitsData.isActiveTanker}
onClick={() => {
selectedUnits.forEach((unit) => {
appState.selectedUnits.forEach((unit) => {
unit.setAdvancedOptions(
!selectedUnitsData.isActiveTanker,
unit.getIsActiveAWACS(),
@@ -825,7 +770,7 @@ export function UnitControlMenu(props: { open: boolean; onClose: () => void }) {
<OlToggle
toggled={selectedUnitsData.isActiveAWACS}
onClick={() => {
selectedUnits.forEach((unit) => {
appState.selectedUnits.forEach((unit) => {
unit.setAdvancedOptions(
unit.getIsActiveTanker(),
!selectedUnitsData.isActiveAWACS,
@@ -844,7 +789,7 @@ export function UnitControlMenu(props: { open: boolean; onClose: () => void }) {
)}
{/* ============== Tanker and AWACS available button END ============== */}
{/* ============== Advanced settings buttons START ============== */}
{selectedUnits.length === 1 && (selectedUnits[0].isTanker() || selectedUnits[0].isAWACS()) && (
{appState.selectedUnits.length === 1 && (appState.selectedUnits[0].isTanker() || appState.selectedUnits[0].isAWACS()) && (
<div className="flex content-center justify-between">
<button
className={`
@@ -855,13 +800,13 @@ export function UnitControlMenu(props: { open: boolean; onClose: () => void }) {
`}
onClick={() => {
setActiveAdvancedSettings({
radio: JSON.parse(JSON.stringify(selectedUnits[0].getRadio())),
TACAN: JSON.parse(JSON.stringify(selectedUnits[0].getTACAN())),
radio: JSON.parse(JSON.stringify(appState.selectedUnits[0].getRadio())),
TACAN: JSON.parse(JSON.stringify(appState.selectedUnits[0].getTACAN())),
});
setShowAdvancedSettings(true);
}}
>
<FaCog className="my-auto" /> {selectedUnits[0].isTanker() ? "Configure tanker settings" : "Configure AWACS settings"}
<FaCog className="my-auto" /> {appState.selectedUnits[0].isTanker() ? "Configure tanker settings" : "Configure AWACS settings"}
</button>
</div>
)}
@@ -889,7 +834,7 @@ export function UnitControlMenu(props: { open: boolean; onClose: () => void }) {
<OlToggle
toggled={selectedUnitsData.scenicAAA}
onClick={() => {
selectedUnits.forEach((unit) => {
appState.selectedUnits.forEach((unit) => {
selectedUnitsData.scenicAAA ? unit.changeSpeed("stop") : unit.scenicAAA();
setSelectedUnitsData({
...selectedUnitsData,
@@ -914,7 +859,7 @@ export function UnitControlMenu(props: { open: boolean; onClose: () => void }) {
<OlToggle
toggled={selectedUnitsData.missOnPurpose}
onClick={() => {
selectedUnits.forEach((unit) => {
appState.selectedUnits.forEach((unit) => {
selectedUnitsData.missOnPurpose ? unit.changeSpeed("stop") : unit.missOnPurpose();
setSelectedUnitsData({
...selectedUnitsData,
@@ -943,7 +888,7 @@ export function UnitControlMenu(props: { open: boolean; onClose: () => void }) {
<OlButtonGroupItem
key={idx}
onClick={() => {
selectedUnits.forEach((unit) => {
appState.selectedUnits.forEach((unit) => {
unit.setShotsScatter(idx + 1);
setSelectedUnitsData({
...selectedUnitsData,
@@ -975,7 +920,7 @@ export function UnitControlMenu(props: { open: boolean; onClose: () => void }) {
<OlButtonGroupItem
key={idx}
onClick={() => {
selectedUnits.forEach((unit) => {
appState.selectedUnits.forEach((unit) => {
unit.setShotsIntensity(idx + 1);
setSelectedUnitsData({
...selectedUnitsData,
@@ -1005,7 +950,7 @@ export function UnitControlMenu(props: { open: boolean; onClose: () => void }) {
<OlCoalitionToggle
coalition={selectedUnitsData.operateAs as Coalition}
onClick={() => {
selectedUnits.forEach((unit) => {
appState.selectedUnits.forEach((unit) => {
unit.setOperateAs(selectedUnitsData.operateAs === "blue" ? "red" : "blue");
setSelectedUnitsData({
...selectedUnitsData,
@@ -1030,7 +975,7 @@ export function UnitControlMenu(props: { open: boolean; onClose: () => void }) {
<OlToggle
toggled={selectedUnitsData.followRoads}
onClick={() => {
selectedUnits.forEach((unit) => {
appState.selectedUnits.forEach((unit) => {
unit.setFollowRoads(!selectedUnitsData.followRoads);
setSelectedUnitsData({
...selectedUnitsData,
@@ -1054,7 +999,7 @@ export function UnitControlMenu(props: { open: boolean; onClose: () => void }) {
<OlToggle
toggled={selectedUnitsData.onOff}
onClick={() => {
selectedUnits.forEach((unit) => {
appState.selectedUnits.forEach((unit) => {
unit.setOnOff(!selectedUnitsData.onOff);
setSelectedUnitsData({
...selectedUnitsData,
@@ -1077,11 +1022,11 @@ export function UnitControlMenu(props: { open: boolean; onClose: () => void }) {
>
Loudspeakers
</span>
{audioManagerEnabled ? (
{appState.audioManagerState ? (
<OlToggle
toggled={selectedUnitsData.isAudioSink}
onClick={() => {
selectedUnits.forEach((unit) => {
appState.selectedUnits.forEach((unit) => {
if (!selectedUnitsData.isAudioSink) {
getApp()?.getAudioManager().addUnitSink(unit);
setSelectedUnitsData({
@@ -1115,7 +1060,8 @@ export function UnitControlMenu(props: { open: boolean; onClose: () => void }) {
`}
>
<FaVolumeHigh />
</span>{" "}first
</span>{" "}
first
</div>
)}
</div>
@@ -1131,14 +1077,14 @@ export function UnitControlMenu(props: { open: boolean; onClose: () => void }) {
<div className="flex content-center gap-2">
<OlDropdown
label={
selectedUnits[0].isAWACS()
appState.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"
>
<>
{selectedUnits[0].isAWACS() && (
{appState.selectedUnits[0].isAWACS() && (
<>
{["Overlord", "Magic", "Wizard", "Focus", "Darkstar"].map((name, idx) => {
return (
@@ -1157,7 +1103,7 @@ export function UnitControlMenu(props: { open: boolean; onClose: () => void }) {
)}
</>
<>
{selectedUnits[0].isTanker() && (
{appState.selectedUnits[0].isTanker() && (
<>
{["Texaco", "Arco", "Shell"].map((name, idx) => {
return (
@@ -1220,9 +1166,10 @@ export function UnitControlMenu(props: { open: boolean; onClose: () => void }) {
value={activeAdvancedSettings ? activeAdvancedSettings.TACAN.channel : 1}
></OlNumberInput>
<OlDropdown label={activeAdvancedSettings ? activeAdvancedSettings.TACAN.XY : "X"} className={`
my-auto w-20
`}>
<OlDropdown
label={activeAdvancedSettings ? activeAdvancedSettings.TACAN.XY : "X"}
className={`my-auto w-20`}
>
<OlDropdownItem
key={"X"}
onClick={() => {
@@ -1291,12 +1238,12 @@ export function UnitControlMenu(props: { open: boolean; onClose: () => void }) {
`}
onClick={() => {
if (activeAdvancedSettings)
selectedUnits[0].setAdvancedOptions(
selectedUnits[0].getIsActiveTanker(),
selectedUnits[0].getIsActiveAWACS(),
appState.selectedUnits[0].setAdvancedOptions(
appState.selectedUnits[0].getIsActiveTanker(),
appState.selectedUnits[0].getIsActiveAWACS(),
activeAdvancedSettings.TACAN,
activeAdvancedSettings.radio,
selectedUnits[0].getGeneralSettings()
appState.selectedUnits[0].getGeneralSettings()
);
setActiveAdvancedSettings(null);
setShowAdvancedSettings(false);
@@ -1329,7 +1276,7 @@ export function UnitControlMenu(props: { open: boolean; onClose: () => void }) {
{/* ============== Unit basic options END ============== */}
<>
{/* ============== Fuel/payload/radio section START ============== */}
{selectedUnits.length === 1 && (
{appState.selectedUnits.length === 1 && (
<div
className={`
flex flex-col gap-4 border-l-4 border-l-olympus-100
@@ -1340,21 +1287,27 @@ export function UnitControlMenu(props: { open: boolean; onClose: () => void }) {
<div
className={`
flex content-center gap-2 rounded-full
${selectedUnits[0].getFuel() > 40 && `bg-green-700`}
${selectedUnits[0].getFuel() > 10 && selectedUnits[0].getFuel() <= 40 && `
bg-yellow-700
${appState.selectedUnits[0].getFuel() > 40 && `
bg-green-700
`}
${
appState.selectedUnits[0].getFuel() > 10 &&
appState.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" />
{selectedUnits[0].getFuel()}%
{appState.selectedUnits[0].getFuel()}%
</div>
</div>
<div className="flex flex-col gap-2">
{selectedUnits[0].isControlledByOlympus() && (selectedUnits[0].isTanker() || selectedUnits[0].isAWACS()) && (
{appState.selectedUnits[0].isControlledByOlympus() && (appState.selectedUnits[0].isTanker() || appState.selectedUnits[0].isAWACS()) && (
<>
{/* ============== Radio section START ============== */}
<div className="flex content-center justify-between">
@@ -1376,7 +1329,7 @@ export function UnitControlMenu(props: { open: boolean; onClose: () => void }) {
dark:text-gray-300
`}
>
{`${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}`}
{`${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}`}
</div>
</div>
</div>
@@ -1399,7 +1352,7 @@ export function UnitControlMenu(props: { open: boolean; onClose: () => void }) {
dark:text-gray-300
`}
>
{`${(selectedUnits[0].getRadio().frequency / 1000000).toFixed(3)} MHz`}
{`${(appState.selectedUnits[0].getRadio().frequency / 1000000).toFixed(3)} MHz`}
</div>
</div>
</div>
@@ -1423,8 +1376,8 @@ export function UnitControlMenu(props: { open: boolean; onClose: () => void }) {
dark:text-gray-300
`}
>
{selectedUnits[0].getTACAN().isOn
? `${selectedUnits[0].getTACAN().channel}${selectedUnits[0].getTACAN().XY} ${selectedUnits[0].getTACAN().callsign}`
{appState.selectedUnits[0].getTACAN().isOn
? `${appState.selectedUnits[0].getTACAN().channel}${appState.selectedUnits[0].getTACAN().XY} ${appState.selectedUnits[0].getTACAN().callsign}`
: "TACAN OFF"}
</div>
</div>
@@ -1433,9 +1386,9 @@ export function UnitControlMenu(props: { open: boolean; onClose: () => void }) {
</>
)}
{/* ============== Payload section START ============== */}
{!selectedUnits[0].isTanker() &&
!selectedUnits[0].isAWACS() &&
selectedUnits[0].getAmmo().map((ammo, idx) => {
{!appState.selectedUnits[0].isTanker() &&
!appState.selectedUnits[0].isAWACS() &&
appState.selectedUnits[0].getAmmo().map((ammo, idx) => {
return (
<div className="flex content-center gap-2" key={idx}>
<div

View File

@@ -1,4 +1,4 @@
import React, { useEffect, useRef, useState } from "react";
import React, { useContext, useEffect, useRef, useState } from "react";
import { Unit } from "../../unit/unit";
import { ContextActionSet } from "../../unit/contextactionset";
import { OlStateButton } from "../components/olstatebutton";
@@ -8,11 +8,13 @@ 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";
export function UnitMouseControlBar(props: {}) {
const appState = useContext(StateContext);
const [open, setOpen] = useState(false);
const [contextActionsSet, setContextActionsSet] = useState(new ContextActionSet());
const [activeContextAction, setActiveContextAction] = useState(null as null | ContextAction);
const [scrolledLeft, setScrolledLeft] = useState(true);
const [scrolledRight, setScrolledRight] = useState(false);
@@ -23,45 +25,11 @@ export function UnitMouseControlBar(props: {}) {
});
useEffect(() => {
/* When a unit is selected, open the menu */
document.addEventListener("unitsSelection", (ev: CustomEventInit) => {
setOpen(true);
updateData();
setActiveContextAction(null);
});
/* When a unit is deselected, refresh the view */
document.addEventListener("unitDeselection", (ev: CustomEventInit) => {
window.setTimeout(() => updateData(), 200);
});
/* When all units are deselected clean the view */
document.addEventListener("clearSelection", () => {
setOpen(false);
updateData();
});
/* Deselect the context action when exiting state */
document.addEventListener("appStateChanged", (ev) => {
setOpen((ev as CustomEvent).detail.state === OlympusState.UNIT_CONTROL);
AppStateChangedEvent.on((state, subState) => {
setOpen(state === OlympusState.UNIT_CONTROL);
});
}, []);
/* Update the current values of the shown data */
function updateData() {
var newContextActionSet = new ContextActionSet();
getApp()
.getUnitsManager()
.getSelectedUnits()
.forEach((unit: Unit) => {
unit.appendContextActions(newContextActionSet);
});
setContextActionsSet(newContextActionSet);
return newContextActionSet;
}
function onScroll(el) {
const sl = el.scrollLeft;
const sr = el.scrollWidth - el.scrollLeft - el.clientWidth;
@@ -75,15 +43,17 @@ export function UnitMouseControlBar(props: {}) {
let reorderedActions: ContextAction[] = [];
CONTEXT_ACTION_COLORS.forEach((color) => {
Object.values(contextActionsSet.getContextActions()).forEach((contextAction: ContextAction) => {
if (color === null && contextAction.getOptions().buttonColor === undefined) reorderedActions.push(contextAction);
else if (color === contextAction.getOptions().buttonColor) reorderedActions.push(contextAction);
});
if (appState.contextActionSet) {
Object.values(appState.contextActionSet.getContextActions()).forEach((contextAction: ContextAction) => {
if (color === null && contextAction.getOptions().buttonColor === undefined) reorderedActions.push(contextAction);
else if (color === contextAction.getOptions().buttonColor) reorderedActions.push(contextAction);
});
}
});
return (
<>
{open && Object.keys(contextActionsSet.getContextActions()).length > 0 && (
{open && appState.contextActionSet && Object.keys(appState.contextActionSet.getContextActions()).length > 0 && (
<>
<div
className={`
@@ -106,7 +76,7 @@ export function UnitMouseControlBar(props: {}) {
return (
<OlStateButton
key={contextAction.getId()}
checked={contextAction === activeContextAction}
checked={contextAction === appState.contextAction}
icon={contextAction.getIcon()}
tooltip={contextAction.getLabel()}
className={
@@ -119,18 +89,9 @@ export function UnitMouseControlBar(props: {}) {
}
onClick={() => {
if (contextAction.getOptions().executeImmediately) {
setActiveContextAction(null);
contextAction.executeCallback(null, null);
} else {
if (activeContextAction !== contextAction) {
setActiveContextAction(contextAction);
getApp().getMap().setContextAction(contextAction);
getApp().getMap().setDefaultContextAction(contextActionsSet.getDefaultContextAction());
} else {
setActiveContextAction(null);
getApp().getMap().setContextAction(null);
getApp().getMap().setDefaultContextAction(null);
}
appState.contextAction !== contextAction ? getApp().getMap().setContextAction(contextAction) : getApp().getMap().setContextAction(null);
}
}}
/>
@@ -147,7 +108,7 @@ export function UnitMouseControlBar(props: {}) {
/>
)}
</div>
{activeContextAction && (
{appState.contextAction && (
<div
className={`
absolute left-[50%] top-32 flex min-w-[300px]
@@ -169,7 +130,7 @@ export function UnitMouseControlBar(props: {}) {
md:border-l-[1px] md:px-5
`}
>
{activeContextAction.getDescription()}
{appState.contextAction.getDescription()}
</div>
</div>
)}

View File

@@ -10,7 +10,18 @@ import { MainMenu } from "./panels/mainmenu";
import { SideBar } from "./panels/sidebar";
import { OptionsMenu } from "./panels/optionsmenu";
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, UnitControlSubState } from "../constants/constants";
import {
BLUE_COMMANDER,
GAME_MASTER,
MAP_HIDDEN_TYPES_DEFAULTS,
MAP_OPTIONS_DEFAULTS,
NO_SUBSTATE,
OlympusEvent,
OlympusState,
OlympusSubState,
RED_COMMANDER,
UnitControlSubState,
} from "../constants/constants";
import { getApp, setupApp } from "../olympusapp";
import { LoginModal } from "./modals/login";
import { sha256 } from "js-sha256";
@@ -27,6 +38,26 @@ import { Unit } from "../unit/unit";
import { ProtectionPrompt } from "./modals/protectionprompt";
import { UnitExplosionMenu } from "./panels/unitexplosionmenu";
import { JTACMenu } from "./panels/jtacmenu";
import {
AppStateChangedEvent,
AudioManagerStateChangedEvent,
AudioSinksChangedEvent,
AudioSourcesChangedEvent,
ConfigLoadedEvent,
ContextActionChangedEvent,
ContextActionSetChangedEvent,
HiddenTypesChangedEvent,
MapOptionsChangedEvent,
MapSourceChangedEvent,
SelectedUnitsChangedEvent,
ServerStatusUpdatedEvent,
UnitSelectedEvent,
} 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;
@@ -43,16 +74,22 @@ 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);
const [commandMode, setCommandMode] = useState(null as null | string);
const [airbase, setAirbase] = useState(null as null | Airbase);
const [formationLeader, setFormationLeader] = useState(null as null | Unit);
@@ -64,31 +101,27 @@ export function UI() {
const [unitExplosionUnits, setUnitExplosionUnits] = useState([] as Unit[]);
useEffect(() => {
getApp()?.registerEventCallback(OlympusEvent.STATE_CHANGED, (state, subState) => {
AppStateChangedEvent.on((state, subState) => {
setAppState(state);
setAppSubState(subState);
})
document.addEventListener("hiddenTypesChanged", (ev) => {
setMapHiddenTypes({ ...getApp().getMap().getHiddenTypes() });
});
document.addEventListener("mapOptionsChanged", (ev) => {
setMapOptions({ ...getApp().getMap().getOptions() });
});
document.addEventListener("mapSourceChanged", (ev) => {
var source = (ev as CustomEvent).detail;
setActiveMapSource(source);
});
document.addEventListener("configLoaded", (ev) => {
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);
setProtectionCallback(() => {
@@ -143,74 +176,88 @@ export function UI() {
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} />
<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)} />
{/* TODO} <UnitExplosionMenu open={appState === OlympusState.MAIN_MENU} units={unitExplosionUnits} onClose={() => getApp().setState(OlympusState.IDLE)} /> {*/}
<JTACMenu open={appState === OlympusState.JTAC} 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)}
/>
<MiniMapPanel />
<ControlsPanel />
<UnitMouseControlBar />
<MapContextMenu />
<SideBar />
</div>
<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)} />
{/* 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>
</div>
);