Added airbase info and spawn menu

This commit is contained in:
Davide Passoni 2024-08-29 15:31:16 +02:00
parent fd15406f5d
commit ebfa7916c6
12 changed files with 272 additions and 61 deletions

View File

@ -7,12 +7,14 @@ export const EventsContext = createContext({
setMeasureMenuVisible: (e: boolean) => {},
setDrawingMenuVisible: (e: boolean) => {},
setOptionsMenuVisible: (e: boolean) => {},
setAirbaseMenuVisible: (e: boolean) => {},
toggleMainMenuVisible: () => {},
toggleSpawnMenuVisible: () => {},
toggleUnitControlMenuVisible: () => {},
toggleMeasureMenuVisible: () => {},
toggleDrawingMenuVisible: () => {},
toggleOptionsMenuVisible: () => {},
toggleAirbaseMenuVisible: () => {},
});
export const EventsProvider = EventsContext.Provider;

View File

@ -12,7 +12,6 @@ export class Airbase extends CustomMarker {
runways: [],
};
#coalition: string = "";
#hasChartDataBeenSet: boolean = false;
#properties: string[] = [];
#parkings: string[] = [];
@ -22,10 +21,6 @@ export class Airbase extends CustomMarker {
this.#name = options.name;
}
chartDataHasBeenSet() {
return this.#hasChartDataBeenSet;
}
createIcon() {
var icon = new DivIcon({
className: "leaflet-airbase-marker",
@ -43,10 +38,10 @@ export class Airbase extends CustomMarker {
el.appendChild(img);
this.getElement()?.appendChild(el);
el.addEventListener("mouseover", (ev) => {
document.dispatchEvent(new CustomEvent("airbaseMouseover", { detail: this }));
document.dispatchEvent(new CustomEvent("airbasemouseover", { detail: this }));
});
el.addEventListener("mouseout", (ev) => {
document.dispatchEvent(new CustomEvent("airbaseMouseout", { detail: this }));
document.dispatchEvent(new CustomEvent("airbasemouseout", { detail: this }));
});
el.dataset.coalition = this.#coalition;
}
@ -73,7 +68,6 @@ export class Airbase extends CustomMarker {
}
setChartData(chartData: AirbaseChartData) {
this.#hasChartDataBeenSet = true;
this.#chartData = chartData;
}

View File

@ -87,7 +87,7 @@ export class MissionManager {
position: new LatLng(airbase.latitude, airbase.longitude),
name: airbase.callsign,
}).addTo(getApp().getMap());
this.#airbases[airbase.callsign].on("contextmenu", (e) => this.#onAirbaseClick(e));
this.#airbases[airbase.callsign].on("click", (e) => this.#onAirbaseClick(e));
this.#loadAirbaseChartData(airbase.callsign);
}
@ -316,7 +316,9 @@ export class MissionManager {
if (requestRefresh) getApp().getServerManager().refreshAll();
}
#onAirbaseClick(e: any) {}
#onAirbaseClick(ev: any) {
document.dispatchEvent(new CustomEvent("airbaseclick", { detail: ev.target }));
}
#loadAirbaseChartData(callsign: string) {
if (!this.#theatre) {
@ -324,7 +326,7 @@ export class MissionManager {
}
var xhr = new XMLHttpRequest();
xhr.open("GET", `api/airbases/${this.#theatre.toLowerCase()}/${callsign}`, true);
xhr.open("GET", window.location.href.split("?")[0].replace("vite/", "") + `api/airbases/${this.#theatre.toLowerCase()}/${callsign}`, true);
xhr.responseType = "json";
xhr.onload = () => {
var status = xhr.status;

View File

@ -142,8 +142,6 @@ export class ServerManager {
setAddress(address: string) {
this.#REST_ADDRESS = `${address.replace("vite/", "").replace("vite", "")}olympus`;
// TODO: TEMPORARY FOR DEBUGGING
// this.#REST_ADDRESS = `https://refugees.dcsolympus.com/olympus`;
console.log(`Setting REST address to ${this.#REST_ADDRESS}`);
}

View File

@ -8,6 +8,7 @@ export const StateContext = createContext({
measureMenuVisible: false,
drawingMenuVisible: false,
optionsMenuVisible: false,
airbaseMenuVisible: false,
mapHiddenTypes: MAP_HIDDEN_TYPES_DEFAULTS,
mapOptions: MAP_OPTIONS_DEFAULTS,
mapSources: [] as string[],

View File

@ -2,7 +2,7 @@ import React, { useEffect, useRef, useState } from "react";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { faArrowCircleDown } from "@fortawesome/free-solid-svg-icons";
export function OlAccordion(props: { title: string; children?: JSX.Element | JSX.Element[]; showArrows?: boolean }) {
export function OlAccordion(props: { title: string; children?: JSX.Element | JSX.Element[]; showArrows?: boolean; className?: string }) {
const [open, setOpen] = useState(false);
const [scrolledUp, setScrolledUp] = useState(true);
const [scrolledDown, setScrolledDown] = useState(false);
@ -31,6 +31,7 @@ export function OlAccordion(props: { title: string; children?: JSX.Element | JSX
type="button"
onClick={() => setOpen(!open)}
className={`
${props.className ?? ""}
flex w-full items-center justify-between gap-3 border-gray-200 py-2
text-gray-700
dark:border-gray-700 dark:text-white

View File

@ -0,0 +1,141 @@
import React, { useState } from "react";
import { Menu } from "./components/menu";
import { OlCheckbox } from "../components/olcheckbox";
import { OlRangeSlider } from "../components/olrangeslider";
import { OlNumberInput } from "../components/olnumberinput";
import { Coalition, MapOptions } from "../../types/types";
import { getApp } from "../../olympusapp";
import { Airbase } from "../../mission/airbase";
import { FaArrowLeft, FaCompass } from "react-icons/fa6";
import { getUnitsByLabel } from "../../other/utils";
import { UnitBlueprint } from "../../interfaces";
import { IDLE } from "../../constants/constants";
import { OlSearchBar } from "../components/olsearchbar";
import { OlAccordion } from "../components/olaccordion";
import { OlUnitEntryList } from "../components/olunitlistentry";
import { olButtonsVisibilityAircraft, olButtonsVisibilityHelicopter } from "../components/olicons";
import { UnitSpawnMenu } from "./unitspawnmenu";
export function AirbaseMenu(props: { open: boolean; onClose: () => void; airbase: Airbase | null; children?: JSX.Element | JSX.Element[] }) {
const [blueprint, setBlueprint] = useState(null as null | UnitBlueprint);
const [filterString, setFilterString] = useState("");
const [filteredAircraft, filteredHelicopters, filteredAirDefense, filteredGroundUnits, filteredNavyUnits] = getUnitsByLabel(filterString);
return (
<Menu title={props.airbase?.getName() ?? "No airbase selected"} open={props.open} onClose={props.onClose} showBackButton={false} canBeHidden={true}>
<div
className={`
flex flex-col gap-2 font-normal text-gray-800
dark:text-white
`}
>
<div
data-coalition={props.airbase?.getCoalition()}
className={`
flex flex-col content-center justify-between gap-2 border-l-4
bg-olympus-200/30 py-3 pl-4 pr-5
data-[coalition='blue']:border-blue-500
data-[coalition='neutral']:border-gray-500
data-[coalition='red']:border-red-500
`}
>
<div className="flex w-full justify-between">
<span className="text-gray-400">ICAO name</span>
<span>{props.airbase?.getChartData().ICAO !== "" ? props.airbase?.getChartData().ICAO : "N/A"}</span>
</div>
<div className="flex w-full justify-between">
<span className="text-gray-400">TACAN</span>
<span>{props.airbase?.getChartData().TACAN !== "" ? props.airbase?.getChartData().TACAN : "None"}</span>
</div>
<div className="flex w-full justify-between">
<span className="text-gray-400">Elevation</span>
<span>{props.airbase?.getChartData().elevation !== "" ? props.airbase?.getChartData().elevation : "N/A"}ft</span>
</div>
<OlAccordion title={`Runways`} className="!p-0 !text-gray-400">
<div className="flex flex-col gap-2">
{props.airbase?.getChartData().runways.map((runway) => {
return (
<>
{Object.keys(runway.headings[0]).map((runwayName) => {
return (
<div className="flex w-full justify-between">
<span>
{" "}
<span className="text-gray-400">RWY</span> {runwayName}
</span>
<span
className={`
flex gap-1 rounded-full bg-olympus-200/30 px-2
py-1
`}
>
<FaCompass className={`my-auto text-gray-400`} /> {runway.headings[0][runwayName].magHeading}°{" "}
<span className={`text-gray-400`}>ILS</span>{" "}
{runway.headings[0][runwayName].ILS !== "" ? runway.headings[0][runwayName].ILS + "MHz" : "None"}
</span>
</div>
);
})}
</>
);
})}
</div>
</OlAccordion>
</div>
<div className="mt-5 flex gap-2 px-5 text-white bold">
{blueprint && (
<FaArrowLeft
className={`
my-auto h-8 w-8 cursor-pointer rounded-md p-2
dark:text-gray-500 dark:hover:bg-gray-700 dark:hover:text-white
`}
onClick={() => setBlueprint(null)}
/>
)}
<span className="my-auto">Spawn units at airbase</span>
</div>
<>
{blueprint === null && (
<div className="p-5">
<OlSearchBar onChange={(value) => setFilterString(value)} text={filterString} />
<OlAccordion title={`Aircraft`}>
<div
className={`
flex max-h-80 flex-col gap-1 overflow-y-scroll no-scrollbar
`}
>
{Object.entries(filteredAircraft).map((entry) => {
return <OlUnitEntryList key={entry[0]} icon={olButtonsVisibilityAircraft} blueprint={entry[1]} onClick={() => setBlueprint(entry[1])} />;
})}
</div>
</OlAccordion>
<OlAccordion title={`Helicopters`}>
<div
className={`
flex max-h-80 flex-col gap-1 overflow-y-scroll no-scrollbar
`}
>
{Object.entries(filteredHelicopters).map((entry) => {
return <OlUnitEntryList key={entry[0]} icon={olButtonsVisibilityHelicopter} blueprint={entry[1]} onClick={() => setBlueprint(entry[1])} />;
})}
</div>
</OlAccordion>
</div>
)}
{!(blueprint === null) && (
<UnitSpawnMenu
blueprint={blueprint}
spawnAtLocation={false}
airbase={props.airbase}
coalition={(props.airbase?.getCoalition() ?? "blue") as Coalition}
/>
)}
</>
</div>
</Menu>
);
}

View File

@ -1,6 +1,6 @@
import React, { useState } from "react";
import { OlStateButton } from "../components/olstatebutton";
import { faGamepad, faRuler, faPencil, faEllipsisV, faCog, faQuestionCircle, faPlusSquare, faMagnifyingGlass } from "@fortawesome/free-solid-svg-icons";
import { faGamepad, faRuler, faPencil, faEllipsisV, faCog, faQuestionCircle, faPlusSquare, faMagnifyingGlass, faPlaneDeparture } from "@fortawesome/free-solid-svg-icons";
import { EventsConsumer } from "../../eventscontext";
import { StateConsumer } from "../../statecontext";
import { IDLE } from "../../constants/constants";
@ -52,6 +52,12 @@ export function SideBar() {
icon={faPencil}
tooltip="Hide/show drawing menu"
></OlStateButton>
<OlStateButton
onClick={events.toggleAirbaseMenuVisible}
checked={appState.airbaseMenuVisible}
icon={faPlaneDeparture}
tooltip="Hide/show airbase menu"
></OlStateButton>
</div>
</div>
<div className="flex w-16 flex-wrap content-end justify-center p-4">

View File

@ -1,7 +1,5 @@
import React, { useState, useEffect } from "react";
import { Menu } from "./components/menu";
import { faPlus } from "@fortawesome/free-solid-svg-icons";
import { library } from "@fortawesome/fontawesome-svg-core";
import { OlSearchBar } from "../components/olsearchbar";
import { OlAccordion } from "../components/olaccordion";
import { getApp } from "../../olympusapp";
@ -18,8 +16,6 @@ import {
import { IDLE, SPAWN_UNIT } from "../../constants/constants";
import { getUnitsByLabel } from "../../other/utils";
library.add(faPlus);
export function SpawnMenu(props: { open: boolean; onClose: () => void; children?: JSX.Element | JSX.Element[] }) {
const [blueprint, setBlueprint] = useState(null as null | UnitBlueprint);
const [filterString, setFilterString] = useState("");
@ -107,7 +103,7 @@ export function SpawnMenu(props: { open: boolean; onClose: () => void; children?
</div>
)}
{!(blueprint === null) && <UnitSpawnMenu blueprint={blueprint} />}
{!(blueprint === null) && <UnitSpawnMenu blueprint={blueprint} spawnAtLocation={true} />}
</>
</Menu>
);

View File

@ -11,8 +11,9 @@ import { getApp } from "../../olympusapp";
import { IDLE, SPAWN_UNIT } from "../../constants/constants";
import { ftToM, getUnitCategoryByBlueprint } from "../../other/utils";
import { LatLng } from "leaflet";
import { Airbase } from "../../mission/airbase";
export function UnitSpawnMenu(props: { blueprint: UnitBlueprint }) {
export function UnitSpawnMenu(props: { blueprint: UnitBlueprint; spawnAtLocation: boolean; airbase?: Airbase | null; coalition?: Coalition }) {
/* Compute the min and max values depending on the unit type */
const minNumber = 1;
const maxNumber = 4;
@ -30,31 +31,60 @@ export function UnitSpawnMenu(props: { blueprint: UnitBlueprint }) {
/* When the menu is opened show the unit preview on the map as a cursor */
useEffect(() => {
if (props.blueprint !== null) {
getApp()
?.getMap()
?.setState(SPAWN_UNIT, {
spawnRequestTable: {
category: getUnitCategoryByBlueprint(props.blueprint),
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
skill: "High",
liveryID: "",
altitude: ftToM(spawnAltitude),
loadout:
props.blueprint.loadouts?.find((loadout) => {
return loadout.name === spawnLoadoutName;
})?.code ?? "",
if (props.coalition && props.coalition !== spawnCoalition) {
setSpawnCoalition(props.coalition);
}
if (props.spawnAtLocation) {
if (props.blueprint !== null) {
getApp()
?.getMap()
?.setState(SPAWN_UNIT, {
spawnRequestTable: {
category: getUnitCategoryByBlueprint(props.blueprint),
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
skill: "High",
liveryID: "",
altitude: ftToM(spawnAltitude),
loadout:
props.blueprint.loadouts?.find((loadout) => {
return loadout.name === spawnLoadoutName;
})?.code ?? "",
},
coalition: spawnCoalition,
},
coalition: spawnCoalition,
},
});
} else {
if (getApp()?.getMap()?.getState() === SPAWN_UNIT) getApp().getMap().setState(IDLE);
});
} else {
if (getApp()?.getMap()?.getState() === SPAWN_UNIT) getApp().getMap().setState(IDLE);
}
}
});
function spawnAtAirbase() {
getApp()
.getUnitsManager()
.spawnUnits(
getUnitCategoryByBlueprint(props.blueprint),
[
{
unitType: props.blueprint.name,
location: new LatLng(0, 0), // Not relevant spawning at airbase
skill: "High",
liveryID: "",
altitude: 0,
loadout:
props.blueprint.loadouts?.find((loadout) => {
return loadout.name === spawnLoadoutName;
})?.code ?? "",
},
],
props.coalition,
false,
props.airbase?.getName()
);
}
/* Get a list of all the roles */
const roles: string[] = [];
(props.blueprint as UnitBlueprint).loadouts?.forEach((loadout) => {
@ -87,14 +117,16 @@ export function UnitSpawnMenu(props: { blueprint: UnitBlueprint }) {
inline-flex w-full flex-row content-center justify-between
`}
>
<OlCoalitionToggle
coalition={spawnCoalition}
onClick={() => {
spawnCoalition === "blue" && setSpawnCoalition("neutral");
spawnCoalition === "neutral" && setSpawnCoalition("red");
spawnCoalition === "red" && setSpawnCoalition("blue");
}}
/>
{!props.coalition && (
<OlCoalitionToggle
coalition={spawnCoalition}
onClick={() => {
spawnCoalition === "blue" && setSpawnCoalition("neutral");
spawnCoalition === "neutral" && setSpawnCoalition("red");
spawnCoalition === "red" && setSpawnCoalition("blue");
}}
/>
)}
<OlNumberInput
value={spawnNumber}
min={minNumber}
@ -203,14 +235,14 @@ export function UnitSpawnMenu(props: { blueprint: UnitBlueprint }) {
</OlDropdown>
</div>
</div>
<div
className={`
flex h-fit flex-col gap-1 p-4
dark:bg-olympus-200/30
`}
>
{spawnLoadout &&
spawnLoadout.items.map((item) => {
{spawnLoadout && spawnLoadout.items.length > 0 && (
<div
className={`
flex h-fit flex-col gap-1 p-4
dark:bg-olympus-200/30
`}
>
{spawnLoadout.items.map((item) => {
return (
<div className="flex content-center gap-2">
<div
@ -233,7 +265,25 @@ export function UnitSpawnMenu(props: { blueprint: UnitBlueprint }) {
</div>
);
})}
</div>
</div>
)}
{!props.spawnAtLocation && (
<button
type="button"
className={`
m-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={() => {
spawnAtAirbase();
}}
>
Spawn
</button>
)}
</div>
);
}

View File

@ -18,16 +18,19 @@ import { sha256 } from "js-sha256";
import { MiniMapPanel } from "./panels/minimappanel";
import { UnitMouseControlBar } from "./panels/unitmousecontrolbar";
import { DrawingMenu } from "./panels/drawingmenu";
import { ControlsPanel } from "./panels/controls";
import { ControlsPanel } from "./panels/controlspanel";
import { MapContextMenu } from "./contextmenus/mapcontextmenu";
import { AirbaseMenu } from "./panels/airbasemenu";
import { Airbase } from "../mission/airbase";
export type OlympusState = {
export type OlympusUIState = {
mainMenuVisible: boolean;
spawnMenuVisible: boolean;
unitControlMenuVisible: boolean;
measureMenuVisible: boolean;
drawingMenuVisible: boolean;
optionsMenuVisible: boolean;
airbaseMenuVisible: boolean;
mapHiddenTypes: MapHiddenTypes;
mapOptions: MapOptions;
};
@ -40,6 +43,7 @@ export function UI() {
const [measureMenuVisible, setMeasureMenuVisible] = useState(false);
const [drawingMenuVisible, setDrawingMenuVisible] = useState(false);
const [optionsMenuVisible, setOptionsMenuVisible] = useState(false);
const [airbaseMenuVisible, setAirbaseMenuVisible] = useState(false);
const [mapHiddenTypes, setMapHiddenTypes] = useState(MAP_HIDDEN_TYPES_DEFAULTS);
const [mapOptions, setMapOptions] = useState(MAP_OPTIONS_DEFAULTS);
const [checkingPassword, setCheckingPassword] = useState(false);
@ -48,6 +52,7 @@ export function UI() {
const [mapSources, setMapSources] = useState([] as string[]);
const [activeMapSource, setActiveMapSource] = useState("");
const [mapState, setMapState] = useState(IDLE);
const [airbase, setAirbase] = useState(null as null | Airbase);
useEffect(() => {
document.addEventListener("hiddenTypesChanged", (ev) => {
@ -75,6 +80,13 @@ export function UI() {
setMapSources(sources);
setActiveMapSource(sources[0]);
});
document.addEventListener("airbaseclick", (ev) => {
hideAllMenus();
getApp().getMap().setState(IDLE);
setAirbase((ev as CustomEvent).detail);
setAirbaseMenuVisible(true);
});
}, []);
function hideAllMenus() {
@ -84,6 +96,7 @@ export function UI() {
setMeasureMenuVisible(false);
setDrawingMenuVisible(false);
setOptionsMenuVisible(false);
setAirbaseMenuVisible(false);
}
function checkPassword(password: string) {
@ -139,6 +152,7 @@ export function UI() {
measureMenuVisible: measureMenuVisible,
drawingMenuVisible: drawingMenuVisible,
optionsMenuVisible: optionsMenuVisible,
airbaseMenuVisible: airbaseMenuVisible,
mapOptions: mapOptions,
mapHiddenTypes: mapHiddenTypes,
mapSources: mapSources,
@ -154,6 +168,7 @@ export function UI() {
setDrawingMenuVisible: setDrawingMenuVisible,
setMeasureMenuVisible: setMeasureMenuVisible,
setOptionsMenuVisible: setOptionsMenuVisible,
setAirbaseMenuVisible: setAirbaseMenuVisible,
toggleMainMenuVisible: () => {
hideAllMenus();
setMainMenuVisible(!mainMenuVisible);
@ -178,6 +193,10 @@ export function UI() {
hideAllMenus();
setOptionsMenuVisible(!optionsMenuVisible);
},
toggleAirbaseMenuVisible: () => {
hideAllMenus();
setAirbaseMenuVisible(!airbaseMenuVisible);
},
}}
>
<Header />
@ -211,6 +230,7 @@ export function UI() {
<Options open={optionsMenuVisible} onClose={() => setOptionsMenuVisible(false)} options={mapOptions} />
<UnitControlMenu open={unitControlMenuVisible} onClose={() => setUnitControlMenuVisible(false)} />
<DrawingMenu open={drawingMenuVisible} onClose={() => setDrawingMenuVisible(false)} />
<AirbaseMenu open={airbaseMenuVisible} onClose={() => setAirbaseMenuVisible(false)} airbase={airbase}/>
<MiniMapPanel />
<ControlsPanel />