mirror of
https://github.com/Pax1601/DCSOlympus.git
synced 2025-10-29 16:56:34 +00:00
Added spawn context menu and controls rework
This commit is contained in:
@@ -1,7 +1,7 @@
|
||||
import React from "react";
|
||||
import { Coalition } from "../../types/types";
|
||||
|
||||
export function OlCoalitionToggle(props: { coalition: Coalition | undefined; onClick: () => void }) {
|
||||
export function OlCoalitionToggle(props: { coalition: Coalition | undefined; onClick: () => void; showLabel?: boolean }) {
|
||||
return (
|
||||
<div className="inline-flex cursor-pointer items-center" onClick={props.onClick}>
|
||||
<button className="peer sr-only" />
|
||||
@@ -26,15 +26,17 @@ export function OlCoalitionToggle(props: { coalition: Coalition | undefined; onC
|
||||
rtl:data-[coalition='red']:after:-translate-x-full
|
||||
`}
|
||||
></div>
|
||||
<span
|
||||
className={`
|
||||
ms-3 overflow-hidden text-ellipsis text-nowrap text-gray-900
|
||||
dark:text-white
|
||||
data-[flash='true']:after:animate-pulse
|
||||
`}
|
||||
>
|
||||
{props.coalition ? `${props.coalition[0].toLocaleUpperCase() + props.coalition.substring(1)}` : "Diff. values"}
|
||||
</span>
|
||||
{props.showLabel && (
|
||||
<span
|
||||
className={`
|
||||
ms-3 overflow-hidden text-ellipsis text-nowrap text-gray-900
|
||||
dark:text-white
|
||||
data-[flash='true']:after:animate-pulse
|
||||
`}
|
||||
>
|
||||
{props.coalition ? `${props.coalition[0].toLocaleUpperCase() + props.coalition.substring(1)}` : "Diff. values"}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -113,7 +113,6 @@ export function MapContextMenu(props: {}) {
|
||||
contextActionIt.executeCallback(unit, null);
|
||||
}
|
||||
}
|
||||
getApp().setState(OlympusState.UNIT_CONTROL);
|
||||
}}
|
||||
>
|
||||
<FontAwesomeIcon className="my-auto" icon={contextActionIt.getIcon()} />
|
||||
|
||||
606
frontend/react/src/ui/contextmenus/spawncontextmenu.tsx
Normal file
606
frontend/react/src/ui/contextmenus/spawncontextmenu.tsx
Normal file
@@ -0,0 +1,606 @@
|
||||
import React, { useEffect, useRef, useState } from "react";
|
||||
import { BLUE_COMMANDER, COMMAND_MODE_OPTIONS_DEFAULTS, GAME_MASTER, NO_SUBSTATE, OlympusState, OlympusSubState } from "../../constants/constants";
|
||||
import { LatLng } from "leaflet";
|
||||
import {
|
||||
AppStateChangedEvent,
|
||||
CommandModeOptionsChangedEvent,
|
||||
SpawnContextMenuRequestEvent,
|
||||
StarredSpawnsChangedEvent,
|
||||
UnitDatabaseLoadedEvent,
|
||||
} from "../../events";
|
||||
import { getApp } from "../../olympusapp";
|
||||
import { SpawnRequestTable, UnitBlueprint } from "../../interfaces";
|
||||
import { faArrowLeft, faEllipsisVertical, faExplosion, faListDots, faSearch, faSmog, faStar } from "@fortawesome/free-solid-svg-icons";
|
||||
import { EffectSpawnMenu } from "../panels/effectspawnmenu";
|
||||
import { UnitSpawnMenu } from "../panels/unitspawnmenu";
|
||||
import { OlEffectListEntry } from "../components/oleffectlistentry";
|
||||
import {
|
||||
olButtonsVisibilityAircraft,
|
||||
olButtonsVisibilityGroundunit,
|
||||
olButtonsVisibilityGroundunitSam,
|
||||
olButtonsVisibilityHelicopter,
|
||||
olButtonsVisibilityNavyunit,
|
||||
} from "../components/olicons";
|
||||
import { OlUnitListEntry } from "../components/olunitlistentry";
|
||||
import { OlSearchBar } from "../components/olsearchbar";
|
||||
import { OlStateButton } from "../components/olstatebutton";
|
||||
import { OlDropdownItem } from "../components/oldropdown";
|
||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||
import { OlCoalitionToggle } from "../components/olcoalitiontoggle";
|
||||
import { Coalition } from "../../types/types";
|
||||
import { CompactUnitSpawnMenu } from "../panels/compactunitspawnmenu";
|
||||
import { CompactEffectSpawnMenu } from "../panels/compacteffectspawnmenu";
|
||||
|
||||
enum CategoryGroup {
|
||||
NONE,
|
||||
AIRCRAFT,
|
||||
HELICOPTER,
|
||||
AIR_DEFENCE,
|
||||
GROUND_UNIT,
|
||||
NAVY_UNIT,
|
||||
EFFECT,
|
||||
SEARCH,
|
||||
STARRED,
|
||||
}
|
||||
|
||||
export function SpawnContextMenu(props: {}) {
|
||||
const [appState, setAppState] = useState(OlympusState.NOT_INITIALIZED);
|
||||
const [appSubState, setAppSubState] = useState(NO_SUBSTATE as OlympusSubState);
|
||||
const [xPosition, setXPosition] = useState(0);
|
||||
const [yPosition, setYPosition] = useState(0);
|
||||
const [latlng, setLatLng] = useState(null as null | LatLng);
|
||||
const [starredSpawns, setStarredSpawns] = useState({} as { [key: string]: SpawnRequestTable });
|
||||
const [openAccordion, setOpenAccordion] = useState(CategoryGroup.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 [commandModeOptions, setCommandModeOptions] = useState(COMMAND_MODE_OPTIONS_DEFAULTS);
|
||||
const [showCost, setShowCost] = useState(false);
|
||||
const [spawnCoalition, setSpawnCoalition] = useState("blue" as Coalition);
|
||||
const [showMore, setShowMore] = useState(false);
|
||||
|
||||
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"),
|
||||
});
|
||||
});
|
||||
|
||||
CommandModeOptionsChangedEvent.on((commandModeOptions) => {
|
||||
setCommandModeOptions(commandModeOptions);
|
||||
setShowCost(!(commandModeOptions.commandMode == GAME_MASTER || !commandModeOptions.restrictSpawns));
|
||||
setOpenAccordion(CategoryGroup.NONE);
|
||||
});
|
||||
|
||||
StarredSpawnsChangedEvent.on((starredSpawns) => setStarredSpawns({ ...starredSpawns }));
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
setBlueprint(null);
|
||||
setEffect(null);
|
||||
setSelectedType(null);
|
||||
setSelectedRole(null);
|
||||
}, [openAccordion]);
|
||||
|
||||
/* Filter the blueprints according to the label */
|
||||
const filteredBlueprints: UnitBlueprint[] = [];
|
||||
if (blueprints && filterString !== "") {
|
||||
blueprints.forEach((blueprint) => {
|
||||
if (blueprint.enabled && (filterString === "" || blueprint.label.toLowerCase().includes(filterString.toLowerCase()))) filteredBlueprints.push(blueprint);
|
||||
});
|
||||
}
|
||||
|
||||
var contentRef = useRef(null);
|
||||
|
||||
useEffect(() => {
|
||||
AppStateChangedEvent.on((state, subState) => {
|
||||
setAppState(state);
|
||||
setAppSubState(subState);
|
||||
});
|
||||
StarredSpawnsChangedEvent.on((starredSpawns) => setStarredSpawns({ ...starredSpawns }));
|
||||
SpawnContextMenuRequestEvent.on((latlng) => {
|
||||
setLatLng(latlng);
|
||||
const containerPoint = getApp().getMap().latLngToContainerPoint(latlng);
|
||||
setXPosition(getApp().getMap().getContainer().offsetLeft + containerPoint.x);
|
||||
setYPosition(getApp().getMap().getContainer().offsetTop + containerPoint.y);
|
||||
});
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
if (contentRef.current) {
|
||||
const content = contentRef.current as HTMLDivElement;
|
||||
|
||||
content.style.left = `${xPosition}px`;
|
||||
content.style.top = `${yPosition}px`;
|
||||
|
||||
let newXPosition = xPosition;
|
||||
let newYposition = yPosition;
|
||||
|
||||
let [cxr, cyb] = [content.getBoundingClientRect().x + content.clientWidth, content.getBoundingClientRect().y + content.clientHeight];
|
||||
|
||||
/* Try and move the content so it is inside the screen */
|
||||
if (cxr > window.innerWidth) newXPosition -= cxr - window.innerWidth;
|
||||
if (cyb > window.innerHeight) newYposition -= cyb - window.innerHeight;
|
||||
|
||||
content.style.left = `${newXPosition}px`;
|
||||
content.style.top = `${newYposition}px`;
|
||||
}
|
||||
});
|
||||
|
||||
return (
|
||||
<>
|
||||
{appState === OlympusState.SPAWN_CONTEXT && (
|
||||
<>
|
||||
<div
|
||||
ref={contentRef}
|
||||
className={`
|
||||
absolute flex w-[395px] flex-wrap gap-2 rounded-md bg-olympus-800
|
||||
`}
|
||||
>
|
||||
<div className="flex w-full flex-col gap-4 px-6 py-3">
|
||||
<div className="flex flex-wrap justify-between gap-2">
|
||||
<OlCoalitionToggle
|
||||
coalition={spawnCoalition}
|
||||
onClick={() => {
|
||||
spawnCoalition === "blue" && setSpawnCoalition("neutral");
|
||||
spawnCoalition === "neutral" && setSpawnCoalition("red");
|
||||
spawnCoalition === "red" && setSpawnCoalition("blue");
|
||||
}}
|
||||
/>
|
||||
<OlStateButton
|
||||
checked={openAccordion === CategoryGroup.AIRCRAFT}
|
||||
onClick={() => (openAccordion !== CategoryGroup.AIRCRAFT ? setOpenAccordion(CategoryGroup.AIRCRAFT) : setOpenAccordion(CategoryGroup.NONE))}
|
||||
icon={olButtonsVisibilityAircraft}
|
||||
tooltip="Show aircraft units"
|
||||
buttonColor={spawnCoalition === "blue" ? "#2563eb" : spawnCoalition === "neutral" ? "#9ca3af" : "#ef4444"}
|
||||
/>
|
||||
<OlStateButton
|
||||
checked={openAccordion === CategoryGroup.HELICOPTER}
|
||||
onClick={() =>
|
||||
openAccordion !== CategoryGroup.HELICOPTER ? setOpenAccordion(CategoryGroup.HELICOPTER) : setOpenAccordion(CategoryGroup.NONE)
|
||||
}
|
||||
icon={olButtonsVisibilityHelicopter}
|
||||
tooltip="Show helicopter units"
|
||||
buttonColor={spawnCoalition === "blue" ? "#2563eb" : spawnCoalition === "neutral" ? "#9ca3af" : "#ef4444"}
|
||||
/>
|
||||
<OlStateButton
|
||||
checked={openAccordion === CategoryGroup.AIR_DEFENCE}
|
||||
onClick={() =>
|
||||
openAccordion !== CategoryGroup.AIR_DEFENCE ? setOpenAccordion(CategoryGroup.AIR_DEFENCE) : setOpenAccordion(CategoryGroup.NONE)
|
||||
}
|
||||
icon={olButtonsVisibilityGroundunitSam}
|
||||
tooltip="Show air defence units"
|
||||
buttonColor={spawnCoalition === "blue" ? "#2563eb" : spawnCoalition === "neutral" ? "#9ca3af" : "#ef4444"}
|
||||
/>
|
||||
<OlStateButton
|
||||
checked={openAccordion === CategoryGroup.GROUND_UNIT}
|
||||
onClick={() =>
|
||||
openAccordion !== CategoryGroup.GROUND_UNIT ? setOpenAccordion(CategoryGroup.GROUND_UNIT) : setOpenAccordion(CategoryGroup.NONE)
|
||||
}
|
||||
icon={olButtonsVisibilityGroundunit}
|
||||
tooltip="Show ground units"
|
||||
buttonColor={spawnCoalition === "blue" ? "#2563eb" : spawnCoalition === "neutral" ? "#9ca3af" : "#ef4444"}
|
||||
/>
|
||||
<OlStateButton
|
||||
checked={openAccordion === CategoryGroup.NAVY_UNIT}
|
||||
onClick={() => (openAccordion !== CategoryGroup.NAVY_UNIT ? setOpenAccordion(CategoryGroup.NAVY_UNIT) : setOpenAccordion(CategoryGroup.NONE))}
|
||||
icon={olButtonsVisibilityNavyunit}
|
||||
tooltip="Show navy units"
|
||||
buttonColor={spawnCoalition === "blue" ? "#2563eb" : spawnCoalition === "neutral" ? "#9ca3af" : "#ef4444"}
|
||||
/>
|
||||
<OlStateButton checked={showMore} onClick={() => setShowMore(!showMore)} icon={faEllipsisVertical} tooltip="Show more options" />
|
||||
{showMore && (
|
||||
<>
|
||||
<OlStateButton
|
||||
checked={openAccordion === CategoryGroup.EFFECT}
|
||||
onClick={() => (openAccordion !== CategoryGroup.EFFECT ? setOpenAccordion(CategoryGroup.EFFECT) : setOpenAccordion(CategoryGroup.NONE))}
|
||||
icon={faExplosion}
|
||||
tooltip="Show effects"
|
||||
className="ml-auto"
|
||||
/>
|
||||
<OlStateButton
|
||||
checked={openAccordion === CategoryGroup.SEARCH}
|
||||
onClick={() => (openAccordion !== CategoryGroup.SEARCH ? setOpenAccordion(CategoryGroup.SEARCH) : setOpenAccordion(CategoryGroup.NONE))}
|
||||
icon={faSearch}
|
||||
tooltip="Search unit"
|
||||
/>
|
||||
<OlStateButton
|
||||
checked={openAccordion === CategoryGroup.STARRED}
|
||||
onClick={() => (openAccordion !== CategoryGroup.STARRED ? setOpenAccordion(CategoryGroup.STARRED) : setOpenAccordion(CategoryGroup.NONE))}
|
||||
icon={faStar}
|
||||
tooltip="Show starred spanws"
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
{blueprint === null && effect === null && openAccordion !== CategoryGroup.NONE && (
|
||||
<div className="mb-3 flex flex-col gap-4">
|
||||
<>
|
||||
<>
|
||||
{openAccordion === CategoryGroup.AIRCRAFT && (
|
||||
<>
|
||||
<div className="flex flex-wrap gap-1">
|
||||
{roles.aircraft.sort().map((role) => {
|
||||
return (
|
||||
<div
|
||||
key={role}
|
||||
data-selected={selectedRole === role}
|
||||
className={`
|
||||
cursor-pointer rounded-full bg-olympus-900
|
||||
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={`
|
||||
flex max-h-[350px] flex-col gap-1
|
||||
overflow-y-scroll no-scrollbar
|
||||
`}
|
||||
>
|
||||
{blueprints
|
||||
?.sort((a, b) => (a.label > b.label ? 1 : -1))
|
||||
.filter((blueprint) => blueprint.category === "aircraft")
|
||||
.map((blueprint) => {
|
||||
return (
|
||||
<OlUnitListEntry
|
||||
key={blueprint.name}
|
||||
icon={olButtonsVisibilityAircraft}
|
||||
blueprint={blueprint}
|
||||
onClick={() => setBlueprint(blueprint)}
|
||||
showCost={showCost}
|
||||
cost={getApp().getUnitsManager().getDatabase().getSpawnPointsByName(blueprint.name)}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
{openAccordion === CategoryGroup.HELICOPTER && (
|
||||
<>
|
||||
<div className="flex flex-wrap gap-1">
|
||||
{roles.helicopter.sort().map((role) => {
|
||||
return (
|
||||
<div
|
||||
key={role}
|
||||
data-selected={selectedRole === role}
|
||||
className={`
|
||||
cursor-pointer rounded-full bg-olympus-900
|
||||
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={`
|
||||
flex max-h-[350px] flex-col gap-1
|
||||
overflow-y-scroll no-scrollbar
|
||||
`}
|
||||
>
|
||||
{blueprints
|
||||
?.sort((a, b) => (a.label > b.label ? 1 : -1))
|
||||
.filter((blueprint) => blueprint.category === "helicopter")
|
||||
.map((blueprint) => {
|
||||
return (
|
||||
<OlUnitListEntry
|
||||
key={blueprint.name}
|
||||
icon={olButtonsVisibilityHelicopter}
|
||||
blueprint={blueprint}
|
||||
onClick={() => setBlueprint(blueprint)}
|
||||
showCost={showCost}
|
||||
cost={getApp().getUnitsManager().getDatabase().getSpawnPointsByName(blueprint.name)}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
{openAccordion === CategoryGroup.AIR_DEFENCE && (
|
||||
<>
|
||||
<div className="flex flex-wrap gap-1">
|
||||
{types.groundunit
|
||||
.sort()
|
||||
?.filter((type) => type === "SAM Site" || type === "AAA")
|
||||
.map((type) => {
|
||||
return (
|
||||
<div
|
||||
key={type}
|
||||
data-selected={selectedType === type}
|
||||
className={`
|
||||
cursor-pointer rounded-full bg-olympus-900
|
||||
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={`
|
||||
flex max-h-[350px] flex-col gap-1
|
||||
overflow-y-scroll no-scrollbar
|
||||
`}
|
||||
>
|
||||
{blueprints
|
||||
?.sort((a, b) => (a.label > b.label ? 1 : -1))
|
||||
.filter((blueprint) => blueprint.category === "groundunit" && (blueprint.type === "SAM Site" || blueprint.type === "AAA"))
|
||||
.map((blueprint) => {
|
||||
return (
|
||||
<OlUnitListEntry
|
||||
key={blueprint.name}
|
||||
icon={olButtonsVisibilityGroundunit}
|
||||
blueprint={blueprint}
|
||||
onClick={() => setBlueprint(blueprint)}
|
||||
showCost={showCost}
|
||||
cost={getApp().getUnitsManager().getDatabase().getSpawnPointsByName(blueprint.name)}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
{openAccordion === CategoryGroup.GROUND_UNIT && (
|
||||
<>
|
||||
<div className="flex flex-wrap gap-1">
|
||||
{types.groundunit
|
||||
.sort()
|
||||
?.filter((type) => type !== "SAM Site" && type !== "AAA")
|
||||
.map((type) => {
|
||||
return (
|
||||
<div
|
||||
key={type}
|
||||
data-selected={selectedType === type}
|
||||
className={`
|
||||
cursor-pointer rounded-full bg-olympus-900
|
||||
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={`
|
||||
flex max-h-[350px] flex-col gap-1
|
||||
overflow-y-scroll no-scrollbar
|
||||
`}
|
||||
>
|
||||
{blueprints
|
||||
?.sort((a, b) => (a.label > b.label ? 1 : -1))
|
||||
.filter((blueprint) => blueprint.category === "groundunit" && blueprint.type !== "SAM Site" && blueprint.type !== "AAA")
|
||||
.map((blueprint) => {
|
||||
return (
|
||||
<OlUnitListEntry
|
||||
key={blueprint.name}
|
||||
icon={olButtonsVisibilityGroundunit}
|
||||
blueprint={blueprint}
|
||||
onClick={() => setBlueprint(blueprint)}
|
||||
showCost={showCost}
|
||||
cost={getApp().getUnitsManager().getDatabase().getSpawnPointsByName(blueprint.name)}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
{openAccordion === CategoryGroup.NAVY_UNIT && (
|
||||
<>
|
||||
<div className="flex flex-wrap gap-1">
|
||||
{types.navyunit.sort().map((type) => {
|
||||
return (
|
||||
<div
|
||||
key={type}
|
||||
data-selected={selectedType === type}
|
||||
className={`
|
||||
cursor-pointer rounded-full bg-olympus-900
|
||||
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={`
|
||||
flex max-h-[350px] flex-col gap-1
|
||||
overflow-y-scroll no-scrollbar
|
||||
`}
|
||||
>
|
||||
{blueprints
|
||||
?.sort((a, b) => (a.label > b.label ? 1 : -1))
|
||||
.filter((blueprint) => blueprint.category === "navyunit")
|
||||
.map((blueprint) => {
|
||||
return (
|
||||
<OlUnitListEntry
|
||||
key={blueprint.name}
|
||||
icon={olButtonsVisibilityNavyunit}
|
||||
blueprint={blueprint}
|
||||
onClick={() => setBlueprint(blueprint)}
|
||||
showCost={showCost}
|
||||
cost={getApp().getUnitsManager().getDatabase().getSpawnPointsByName(blueprint.name)}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
{openAccordion === CategoryGroup.EFFECT && (
|
||||
<>
|
||||
<div
|
||||
className={`
|
||||
flex max-h-[350px] flex-col gap-1
|
||||
overflow-y-scroll no-scrollbar
|
||||
`}
|
||||
>
|
||||
<OlEffectListEntry
|
||||
key={"explosion"}
|
||||
icon={faExplosion}
|
||||
label={"Explosion"}
|
||||
onClick={() => {
|
||||
setEffect("explosion");
|
||||
}}
|
||||
/>
|
||||
<OlEffectListEntry
|
||||
key={"smoke"}
|
||||
icon={faSmog}
|
||||
label={"Smoke"}
|
||||
onClick={() => {
|
||||
setEffect("smoke");
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
{openAccordion === CategoryGroup.SEARCH && (
|
||||
<div className="flex flex-col gap-2">
|
||||
<OlSearchBar onChange={(value) => setFilterString(value)} text={filterString} />
|
||||
<div
|
||||
className={`
|
||||
flex max-h-[350px] flex-col gap-1
|
||||
overflow-y-scroll no-scrollbar
|
||||
`}
|
||||
>
|
||||
{filteredBlueprints.length > 0 ? (
|
||||
filteredBlueprints.map((blueprint) => {
|
||||
return (
|
||||
<OlUnitListEntry
|
||||
key={blueprint.name}
|
||||
icon={olButtonsVisibilityNavyunit}
|
||||
blueprint={blueprint}
|
||||
onClick={() => setBlueprint(blueprint)}
|
||||
showCost={showCost}
|
||||
cost={getApp().getUnitsManager().getDatabase().getSpawnPointsByName(blueprint.name)}
|
||||
/>
|
||||
);
|
||||
})
|
||||
) : filterString === "" ? (
|
||||
<span className={`text-gray-200`}>Type to search</span>
|
||||
) : (
|
||||
<span className={`text-gray-200`}>No results</span>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
{openAccordion === CategoryGroup.STARRED && (
|
||||
<div className="flex flex-col gap-2">
|
||||
{Object.values(starredSpawns).length > 0 ? (
|
||||
Object.values(starredSpawns).map((spawnRequestTable) => {
|
||||
return (
|
||||
<OlDropdownItem
|
||||
className={`
|
||||
flex w-full content-center gap-2 text-sm
|
||||
text-white
|
||||
`}
|
||||
onClick={() => {
|
||||
if (latlng) {
|
||||
spawnRequestTable.unit.location = latlng;
|
||||
getApp()
|
||||
.getUnitsManager()
|
||||
.spawnUnits(spawnRequestTable.category, Array(spawnRequestTable.amount).fill(spawnRequestTable.unit), spawnRequestTable.coalition, false);
|
||||
getApp().setState(OlympusState.IDLE);
|
||||
}
|
||||
}}
|
||||
>
|
||||
<FontAwesomeIcon
|
||||
data-coalition={spawnRequestTable.coalition}
|
||||
className={`
|
||||
my-auto
|
||||
data-[coalition='blue']:text-blue-500
|
||||
data-[coalition='neutral']:text-gay-500
|
||||
data-[coalition='red']:text-red-500
|
||||
`}
|
||||
icon={faStar}
|
||||
/>
|
||||
<div>
|
||||
{getApp().getUnitsManager().getDatabase().getByName(spawnRequestTable.unit.unitType)?.label} (
|
||||
{spawnRequestTable.quickAccessName})
|
||||
</div>
|
||||
</OlDropdownItem>
|
||||
);
|
||||
})
|
||||
) : (
|
||||
<div className="p-2 text-sm text-white">No starred spawns, use the spawn menu to create a quick access spawn</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
</>
|
||||
</div>
|
||||
)}
|
||||
{!(blueprint === null) && <CompactUnitSpawnMenu blueprint={blueprint} starredSpawns={starredSpawns} latlng={latlng} coalition={spawnCoalition} onBack={() => setBlueprint(null)}/>}
|
||||
{!(effect === null) && latlng && <CompactEffectSpawnMenu effect={effect} latlng={latlng} onBack={() => setEffect(null)} />}
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -1,108 +0,0 @@
|
||||
import React, { useEffect, useRef, useState } from "react";
|
||||
import { NO_SUBSTATE, OlympusState, OlympusSubState } from "../../constants/constants";
|
||||
import { OlDropdownItem } from "../components/oldropdown";
|
||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||
import { LatLng } from "leaflet";
|
||||
import { AppStateChangedEvent, StarredSpawnContextMenuRequestEvent, StarredSpawnsChangedEvent } from "../../events";
|
||||
import { getApp } from "../../olympusapp";
|
||||
import { SpawnRequestTable } from "../../interfaces";
|
||||
import { faStar } from "@fortawesome/free-solid-svg-icons";
|
||||
|
||||
export function StarredSpawnContextMenu(props: {}) {
|
||||
const [appState, setAppState] = useState(OlympusState.NOT_INITIALIZED);
|
||||
const [appSubState, setAppSubState] = useState(NO_SUBSTATE as OlympusSubState);
|
||||
const [xPosition, setXPosition] = useState(0);
|
||||
const [yPosition, setYPosition] = useState(0);
|
||||
const [latlng, setLatLng] = useState(null as null | LatLng);
|
||||
const [starredSpawns, setStarredSpawns] = useState({} as { [key: string]: SpawnRequestTable });
|
||||
|
||||
var contentRef = useRef(null);
|
||||
|
||||
useEffect(() => {
|
||||
AppStateChangedEvent.on((state, subState) => {
|
||||
setAppState(state);
|
||||
setAppSubState(subState);
|
||||
});
|
||||
StarredSpawnsChangedEvent.on((starredSpawns) => setStarredSpawns({ ...starredSpawns }));
|
||||
StarredSpawnContextMenuRequestEvent.on((latlng) => {
|
||||
setLatLng(latlng);
|
||||
const containerPoint = getApp().getMap().latLngToContainerPoint(latlng);
|
||||
setXPosition(getApp().getMap().getContainer().offsetLeft + containerPoint.x);
|
||||
setYPosition(getApp().getMap().getContainer().offsetTop + containerPoint.y);
|
||||
});
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
if (contentRef.current) {
|
||||
const content = contentRef.current as HTMLDivElement;
|
||||
|
||||
content.style.left = `${xPosition}px`;
|
||||
content.style.top = `${yPosition}px`;
|
||||
|
||||
let newXPosition = xPosition;
|
||||
let newYposition = yPosition;
|
||||
|
||||
let [cxr, cyb] = [content.getBoundingClientRect().x + content.clientWidth, content.getBoundingClientRect().y + content.clientHeight];
|
||||
|
||||
/* Try and move the content so it is inside the screen */
|
||||
if (cxr > window.innerWidth) newXPosition -= cxr - window.innerWidth;
|
||||
if (cyb > window.innerHeight) newYposition -= cyb - window.innerHeight;
|
||||
|
||||
content.style.left = `${newXPosition}px`;
|
||||
content.style.top = `${newYposition}px`;
|
||||
}
|
||||
});
|
||||
|
||||
return (
|
||||
<>
|
||||
{appState === OlympusState.STARRED_SPAWN && (
|
||||
<>
|
||||
<div
|
||||
ref={contentRef}
|
||||
className={`
|
||||
absolute flex min-w-80 max-w-80 gap-2 rounded-md bg-olympus-600
|
||||
`}
|
||||
>
|
||||
<div
|
||||
className={`
|
||||
flex w-full flex-col gap-2 overflow-y-auto no-scrollbar p-2
|
||||
`}
|
||||
>
|
||||
{Object.values(starredSpawns).length > 0? Object.values(starredSpawns).map((spawnRequestTable) => {
|
||||
return (
|
||||
<OlDropdownItem
|
||||
className={`
|
||||
flex w-full content-center gap-2 text-sm text-white
|
||||
`}
|
||||
onClick={() => {
|
||||
if (latlng) {
|
||||
spawnRequestTable.unit.location = latlng;
|
||||
getApp().getUnitsManager().spawnUnits(spawnRequestTable.category, [spawnRequestTable.unit], spawnRequestTable.coalition, false);
|
||||
getApp().setState(OlympusState.IDLE)
|
||||
}
|
||||
}}
|
||||
>
|
||||
<FontAwesomeIcon
|
||||
data-coalition={spawnRequestTable.coalition}
|
||||
className={`
|
||||
my-auto
|
||||
data-[coalition='blue']:text-blue-500
|
||||
data-[coalition='neutral']:text-gay-500
|
||||
data-[coalition='red']:text-red-500
|
||||
`}
|
||||
icon={faStar}
|
||||
/>
|
||||
<div>
|
||||
{getApp().getUnitsManager().getDatabase().getByName(spawnRequestTable.unit.unitType)?.label} ({spawnRequestTable.quickAccessName})
|
||||
</div>
|
||||
</OlDropdownItem>
|
||||
);
|
||||
}):
|
||||
<div className="p-2 text-sm text-white">No starred spawns, use the spawn menu to create a quick access spawn</div>}
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -272,7 +272,6 @@ export function AirbaseMenu(props: { open: boolean; onClose: () => void; childre
|
||||
<UnitSpawnMenu
|
||||
blueprint={blueprint}
|
||||
starredSpawns={starredSpawns}
|
||||
spawnAtLocation={false}
|
||||
airbase={airbase}
|
||||
coalition={(airbase?.getCoalition() ?? "blue") as Coalition}
|
||||
/>
|
||||
|
||||
101
frontend/react/src/ui/panels/compacteffectspawnmenu.tsx
Normal file
101
frontend/react/src/ui/panels/compacteffectspawnmenu.tsx
Normal file
@@ -0,0 +1,101 @@
|
||||
import React, { useEffect, useState } from "react";
|
||||
import { OlDropdown, OlDropdownItem } from "../components/oldropdown";
|
||||
import { getApp } from "../../olympusapp";
|
||||
import { OlympusState, SpawnSubState } from "../../constants/constants";
|
||||
import { OlStateButton } from "../components/olstatebutton";
|
||||
import { faArrowLeft, faSmog } from "@fortawesome/free-solid-svg-icons";
|
||||
import { LatLng } from "leaflet";
|
||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||
|
||||
export function CompactEffectSpawnMenu(props: { effect: string; latlng: LatLng; onBack: () => void }) {
|
||||
const [explosionType, setExplosionType] = useState("High explosive");
|
||||
const [smokeColor, setSmokeColor] = useState("white");
|
||||
|
||||
return (
|
||||
<div className="flex h-full flex-col gap-4">
|
||||
{props.effect === "explosion" && (
|
||||
<>
|
||||
<div className="flex">
|
||||
<FontAwesomeIcon
|
||||
onClick={props.onBack}
|
||||
icon={faArrowLeft}
|
||||
className={`
|
||||
my-auto mr-1 h-4 cursor-pointer rounded-md p-2
|
||||
dark:text-gray-500 dark:hover:bg-gray-700 dark:hover:text-white
|
||||
`}
|
||||
/>
|
||||
<span className="my-auto text-white">Explosion type</span>
|
||||
</div>
|
||||
<OlDropdown label={explosionType} className="w-full">
|
||||
{["High explosive", "Napalm", "White phosphorous"].map((optionExplosionType) => {
|
||||
return (
|
||||
<OlDropdownItem
|
||||
key={optionExplosionType}
|
||||
onClick={() => {
|
||||
setExplosionType(optionExplosionType);
|
||||
}}
|
||||
>
|
||||
{optionExplosionType}
|
||||
</OlDropdownItem>
|
||||
);
|
||||
})}
|
||||
</OlDropdown>
|
||||
</>
|
||||
)}
|
||||
{props.effect === "smoke" && (
|
||||
<>
|
||||
<div className="flex">
|
||||
<FontAwesomeIcon
|
||||
onClick={props.onBack}
|
||||
icon={faArrowLeft}
|
||||
className={`
|
||||
my-auto mr-1 h-4 cursor-pointer rounded-md p-2
|
||||
dark:text-gray-500 dark:hover:bg-gray-700 dark:hover:text-white
|
||||
`}
|
||||
/>
|
||||
<span className="my-auto text-white">Smoke color</span>
|
||||
</div>
|
||||
<div className="flex w-full gap-2">
|
||||
{["white", "blue", "red", "green", "orange"].map((optionSmokeColor) => {
|
||||
return (
|
||||
<OlStateButton
|
||||
checked={smokeColor === optionSmokeColor}
|
||||
icon={faSmog}
|
||||
onClick={() => {
|
||||
setSmokeColor(optionSmokeColor);
|
||||
}}
|
||||
tooltip=""
|
||||
buttonColor={optionSmokeColor}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
<button
|
||||
type="button"
|
||||
className={`
|
||||
m-2 rounded-lg bg-blue-600 px-5 py-2.5 text-sm font-medium text-white
|
||||
focus:outline-none focus:ring-4
|
||||
`}
|
||||
onClick={() => {
|
||||
if (props.effect === "explosion") {
|
||||
if (explosionType === "High explosive") getApp().getServerManager().spawnExplosion(50, "normal", props.latlng);
|
||||
else if (explosionType === "Napalm") getApp().getServerManager().spawnExplosion(50, "napalm", props.latlng);
|
||||
else if (explosionType === "White phosphorous") getApp().getServerManager().spawnExplosion(50, "phosphorous", props.latlng);
|
||||
getApp().getMap().addExplosionMarker(props.latlng);
|
||||
} else if (props.effect === "smoke") {
|
||||
getApp().getServerManager().spawnSmoke(smokeColor, props.latlng);
|
||||
getApp()
|
||||
.getMap()
|
||||
.addSmokeMarker(props.latlng, smokeColor ?? "white");
|
||||
}
|
||||
|
||||
getApp().setState(OlympusState.IDLE);
|
||||
}}
|
||||
>
|
||||
Spawn
|
||||
</button>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
445
frontend/react/src/ui/panels/compactunitspawnmenu.tsx
Normal file
445
frontend/react/src/ui/panels/compactunitspawnmenu.tsx
Normal file
@@ -0,0 +1,445 @@
|
||||
import React, { useState, useEffect, useCallback } from "react";
|
||||
import { OlUnitSummary } from "../components/olunitsummary";
|
||||
import { OlCoalitionToggle } from "../components/olcoalitiontoggle";
|
||||
import { OlNumberInput } from "../components/olnumberinput";
|
||||
import { OlLabelToggle } from "../components/ollabeltoggle";
|
||||
import { OlRangeSlider } from "../components/olrangeslider";
|
||||
import { OlDropdownItem, OlDropdown } from "../components/oldropdown";
|
||||
import { LoadoutBlueprint, SpawnRequestTable, UnitBlueprint } from "../../interfaces";
|
||||
import { OlStateButton } from "../components/olstatebutton";
|
||||
import { Coalition } from "../../types/types";
|
||||
import { getApp } from "../../olympusapp";
|
||||
import { ftToM, hash } from "../../other/utils";
|
||||
import { LatLng } from "leaflet";
|
||||
import { Airbase } from "../../mission/airbase";
|
||||
import { altitudeIncrements, groupUnitCount, maxAltitudeValues, minAltitudeValues, OlympusState, SpawnSubState } from "../../constants/constants";
|
||||
import { faArrowLeft, faStar } from "@fortawesome/free-solid-svg-icons";
|
||||
import { OlStringInput } from "../components/olstringinput";
|
||||
import { countryCodes } from "../data/codes";
|
||||
import { OlAccordion } from "../components/olaccordion";
|
||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||
|
||||
export function CompactUnitSpawnMenu(props: {
|
||||
starredSpawns: { [key: string]: SpawnRequestTable };
|
||||
blueprint: UnitBlueprint;
|
||||
onBack: () => void;
|
||||
latlng?: LatLng | null;
|
||||
airbase?: Airbase | null;
|
||||
coalition?: Coalition;
|
||||
}) {
|
||||
/* Compute the min and max values depending on the unit type */
|
||||
const minNumber = 1;
|
||||
const maxNumber = groupUnitCount[props.blueprint.category];
|
||||
const minAltitude = minAltitudeValues[props.blueprint.category];
|
||||
const maxAltitude = maxAltitudeValues[props.blueprint.category];
|
||||
const altitudeStep = altitudeIncrements[props.blueprint.category];
|
||||
|
||||
/* State initialization */
|
||||
const [spawnCoalition, setSpawnCoalition] = useState("blue" as Coalition);
|
||||
const [spawnNumber, setSpawnNumber] = useState(1);
|
||||
const [spawnRole, setSpawnRole] = useState("");
|
||||
const [spawnLoadoutName, setSpawnLoadout] = useState("");
|
||||
const [spawnAltitude, setSpawnAltitude] = useState((maxAltitude - minAltitude) / 2);
|
||||
const [spawnAltitudeType, setSpawnAltitudeType] = useState(false);
|
||||
const [spawnLiveryID, setSpawnLiveryID] = useState("");
|
||||
const [spawnSkill, setSpawnSkill] = useState("High");
|
||||
|
||||
const [showAdvancedOptions, setShowAdvancedOptions] = useState(false);
|
||||
const [showLoadout, setShowLoadout] = useState(false);
|
||||
const [showUnitSummary, setShowUnitSummary] = useState(false);
|
||||
|
||||
const [quickAccessName, setQuickAccessName] = useState("No name");
|
||||
const [key, setKey] = useState("");
|
||||
const [spawnRequestTable, setSpawnRequestTable] = useState(null as null | SpawnRequestTable);
|
||||
|
||||
/* When the menu is opened show the unit preview on the map as a cursor */
|
||||
useEffect(() => {
|
||||
if (!props.airbase && !props.latlng && spawnRequestTable) {
|
||||
/* Refresh the unique key identified */
|
||||
const newKey = hash(JSON.stringify(spawnRequestTable));
|
||||
setKey(newKey);
|
||||
|
||||
getApp()?.getMap()?.setSpawnRequestTable(spawnRequestTable);
|
||||
getApp().setState(OlympusState.SPAWN, SpawnSubState.SPAWN_UNIT);
|
||||
}
|
||||
}, [spawnRequestTable]);
|
||||
|
||||
/* Callback and effect to update the quick access name of the starredSpawn */
|
||||
const updateStarredSpawnQuickAccessNameS = useCallback(() => {
|
||||
if (key in props.starredSpawns) props.starredSpawns[key].quickAccessName = quickAccessName;
|
||||
}, [props.starredSpawns, key, quickAccessName]);
|
||||
useEffect(updateStarredSpawnQuickAccessNameS, [quickAccessName]);
|
||||
|
||||
/* Callback and effect to update the quick access name in the input field */
|
||||
const updateQuickAccessName = useCallback(() => {
|
||||
/* If the spawn is starred, set the quick access name */
|
||||
if (key in props.starredSpawns && props.starredSpawns[key].quickAccessName) setQuickAccessName(props.starredSpawns[key].quickAccessName);
|
||||
else setQuickAccessName("No name");
|
||||
}, [props.starredSpawns, key]);
|
||||
useEffect(updateQuickAccessName, [key]);
|
||||
|
||||
/* Callback and effect to update the spawn request table */
|
||||
const updateSpawnRequestTable = useCallback(() => {
|
||||
if (props.blueprint !== null) {
|
||||
setSpawnRequestTable({
|
||||
category: props.blueprint.category,
|
||||
unit: {
|
||||
unitType: props.blueprint.name,
|
||||
location: props.latlng ?? new LatLng(0, 0), // This will be filled when the user clicks on the map to spawn the unit
|
||||
skill: spawnSkill,
|
||||
liveryID: spawnLiveryID,
|
||||
altitude: ftToM(spawnAltitude),
|
||||
loadout: props.blueprint.loadouts?.find((loadout) => loadout.name === spawnLoadoutName)?.code ?? "",
|
||||
},
|
||||
amount: spawnNumber,
|
||||
coalition: spawnCoalition,
|
||||
});
|
||||
}
|
||||
}, [props.blueprint, spawnAltitude, spawnLoadoutName, spawnCoalition, spawnNumber, spawnLiveryID, spawnSkill]);
|
||||
useEffect(updateSpawnRequestTable, [props.blueprint, spawnAltitude, spawnLoadoutName, spawnCoalition, spawnNumber, spawnLiveryID, spawnSkill]);
|
||||
|
||||
/* Effect to update the coalition if it is force externally */
|
||||
useEffect(() => {
|
||||
if (props.coalition) setSpawnCoalition(props.coalition);
|
||||
}, [props.coalition]);
|
||||
|
||||
/* Get a list of all the roles */
|
||||
const roles: string[] = [];
|
||||
(props.blueprint as UnitBlueprint).loadouts?.forEach((loadout) => {
|
||||
loadout.roles.forEach((role) => {
|
||||
!roles.includes(role) && roles.push(role);
|
||||
});
|
||||
});
|
||||
|
||||
/* Initialize the role */
|
||||
spawnRole === "" && roles.length > 0 && setSpawnRole(roles[0]);
|
||||
|
||||
/* Get a list of all the loadouts */
|
||||
const loadouts: LoadoutBlueprint[] = [];
|
||||
(props.blueprint as UnitBlueprint).loadouts?.forEach((loadout) => {
|
||||
loadout.roles.includes(spawnRole) && loadouts.push(loadout);
|
||||
});
|
||||
|
||||
/* Initialize the loadout */
|
||||
spawnLoadoutName === "" && loadouts.length > 0 && setSpawnLoadout(loadouts[0].name);
|
||||
const spawnLoadout = props.blueprint.loadouts?.find((loadout) => {
|
||||
return loadout.name === spawnLoadoutName;
|
||||
});
|
||||
|
||||
return (
|
||||
<div className="flex flex-col">
|
||||
<div className="flex h-fit flex-col gap-3">
|
||||
<div className="flex">
|
||||
<FontAwesomeIcon
|
||||
onClick={props.onBack}
|
||||
icon={faArrowLeft}
|
||||
className={`
|
||||
my-auto mr-1 h-4 cursor-pointer rounded-md p-2
|
||||
dark:text-gray-500 dark:hover:bg-gray-700 dark:hover:text-white
|
||||
`}
|
||||
/>
|
||||
<h5 className="my-auto text-gray-200">{props.blueprint.label}</h5>
|
||||
<OlNumberInput
|
||||
className={"ml-auto"}
|
||||
value={spawnNumber}
|
||||
min={minNumber}
|
||||
max={maxNumber}
|
||||
onDecrease={() => {
|
||||
setSpawnNumber(Math.max(minNumber, spawnNumber - 1));
|
||||
}}
|
||||
onIncrease={() => {
|
||||
setSpawnNumber(Math.min(maxNumber, spawnNumber + 1));
|
||||
}}
|
||||
onChange={(ev) => {
|
||||
!isNaN(Number(ev.target.value)) && setSpawnNumber(Math.max(minNumber, Math.min(maxNumber, Number(ev.target.value))));
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
<div
|
||||
className={`
|
||||
inline-flex w-full flex-row content-center justify-between gap-2
|
||||
`}
|
||||
>
|
||||
<div className="my-auto text-sm text-white">Quick access: </div>
|
||||
<OlStringInput
|
||||
onChange={(e) => {
|
||||
setQuickAccessName(e.target.value);
|
||||
}}
|
||||
value={quickAccessName}
|
||||
/>
|
||||
<OlStateButton
|
||||
onClick={() => {
|
||||
if (spawnRequestTable)
|
||||
key in props.starredSpawns
|
||||
? getApp().getMap().removeStarredSpawnRequestTable(key)
|
||||
: getApp().getMap().addStarredSpawnRequestTable(key, spawnRequestTable);
|
||||
}}
|
||||
tooltip="Save this spawn for quick access"
|
||||
checked={key in props.starredSpawns}
|
||||
icon={faStar}
|
||||
></OlStateButton>
|
||||
</div>
|
||||
{["aircraft", "helicopter"].includes(props.blueprint.category) && (
|
||||
<>
|
||||
{!props.airbase && (
|
||||
<div>
|
||||
<div
|
||||
className={`
|
||||
flex flex-row content-center items-center justify-between
|
||||
`}
|
||||
>
|
||||
<div className="flex flex-col">
|
||||
<span
|
||||
className={`
|
||||
font-normal
|
||||
dark:text-white
|
||||
`}
|
||||
>
|
||||
Altitude
|
||||
</span>
|
||||
<span
|
||||
className={`
|
||||
font-bold
|
||||
dark:text-blue-500
|
||||
`}
|
||||
>{`${Intl.NumberFormat("en-US").format(spawnAltitude)} FT`}</span>
|
||||
</div>
|
||||
<OlLabelToggle toggled={spawnAltitudeType} leftLabel={"AGL"} rightLabel={"ASL"} onClick={() => setSpawnAltitudeType(!spawnAltitudeType)} />
|
||||
</div>
|
||||
<OlRangeSlider
|
||||
onChange={(ev) => setSpawnAltitude(Number(ev.target.value))}
|
||||
value={spawnAltitude}
|
||||
min={minAltitude}
|
||||
max={maxAltitude}
|
||||
step={altitudeStep}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
<div className="flex content-center justify-between gap-2">
|
||||
<span
|
||||
className={`
|
||||
my-auto font-normal
|
||||
dark:text-white
|
||||
`}
|
||||
>
|
||||
Role
|
||||
</span>
|
||||
<OlDropdown label={spawnRole} className="w-64">
|
||||
{roles.map((role) => {
|
||||
return (
|
||||
<OlDropdownItem
|
||||
onClick={() => {
|
||||
setSpawnRole(role);
|
||||
setSpawnLoadout("");
|
||||
}}
|
||||
className={`w-full`}
|
||||
>
|
||||
{role}
|
||||
</OlDropdownItem>
|
||||
);
|
||||
})}
|
||||
</OlDropdown>
|
||||
</div>
|
||||
<div className="flex content-center justify-between gap-2">
|
||||
<span
|
||||
className={`
|
||||
my-auto font-normal
|
||||
dark:text-white
|
||||
`}
|
||||
>
|
||||
Weapons
|
||||
</span>
|
||||
<OlDropdown label={spawnLoadoutName} className={`w-64`}>
|
||||
{loadouts.map((loadout) => {
|
||||
return (
|
||||
<OlDropdownItem
|
||||
onClick={() => {
|
||||
setSpawnLoadout(loadout.name);
|
||||
}}
|
||||
className={`w-full`}
|
||||
>
|
||||
<span
|
||||
className={`
|
||||
w-full overflow-hidden text-ellipsis text-nowrap
|
||||
text-left w-max-full
|
||||
`}
|
||||
>
|
||||
{loadout.name}
|
||||
</span>
|
||||
</OlDropdownItem>
|
||||
);
|
||||
})}
|
||||
</OlDropdown>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
<OlAccordion
|
||||
onClick={() => {
|
||||
setShowAdvancedOptions(!showAdvancedOptions);
|
||||
}}
|
||||
open={showAdvancedOptions}
|
||||
title="Advanced options"
|
||||
>
|
||||
<div className="flex flex-col gap-2">
|
||||
<div className="flex content-center justify-between gap-2">
|
||||
<span
|
||||
className={`
|
||||
my-auto font-normal
|
||||
dark:text-white
|
||||
`}
|
||||
>
|
||||
Livery
|
||||
</span>
|
||||
<OlDropdown
|
||||
label={props.blueprint.liveries ? (props.blueprint.liveries[spawnLiveryID]?.name ?? "Default") : "No livery"}
|
||||
className={`w-64`}
|
||||
>
|
||||
{props.blueprint.liveries &&
|
||||
Object.keys(props.blueprint.liveries)
|
||||
.sort((ida, idb) => {
|
||||
if (props.blueprint.liveries) {
|
||||
if (props.blueprint.liveries[ida].countries.length > 1) return 1;
|
||||
return props.blueprint.liveries[ida].countries[0] > props.blueprint.liveries[idb].countries[0] ? 1 : -1;
|
||||
} else return -1;
|
||||
})
|
||||
.map((id) => {
|
||||
let country = Object.values(countryCodes).find((countryCode) => {
|
||||
if (props.blueprint.liveries && countryCode.liveryCodes?.includes(props.blueprint.liveries[id].countries[0])) return true;
|
||||
});
|
||||
return (
|
||||
<OlDropdownItem
|
||||
onClick={() => {
|
||||
setSpawnLiveryID(id);
|
||||
}}
|
||||
className={`w-full`}
|
||||
>
|
||||
<span
|
||||
className={`
|
||||
w-full content-center overflow-hidden
|
||||
text-ellipsis text-nowrap text-left w-max-full
|
||||
flex gap-2
|
||||
`}
|
||||
>
|
||||
{props.blueprint.liveries && props.blueprint.liveries[id].countries.length == 1 && (
|
||||
<img src={`images/countries/${country?.flagCode.toLowerCase()}.svg`} className={`
|
||||
h-6
|
||||
`} />
|
||||
)}
|
||||
|
||||
<div className="my-auto truncate">
|
||||
<span
|
||||
className={`
|
||||
w-full overflow-hidden text-left w-max-full
|
||||
`}
|
||||
>
|
||||
{props.blueprint.liveries ? props.blueprint.liveries[id].name : ""}
|
||||
</span>
|
||||
</div>
|
||||
</span>
|
||||
</OlDropdownItem>
|
||||
);
|
||||
})}
|
||||
</OlDropdown>
|
||||
</div>
|
||||
<div className="flex content-center justify-between gap-2">
|
||||
<span
|
||||
className={`
|
||||
my-auto font-normal
|
||||
dark:text-white
|
||||
`}
|
||||
>
|
||||
Skill
|
||||
</span>
|
||||
<OlDropdown label={spawnSkill} className={`w-64`}>
|
||||
{["Average", "Good", "High", "Excellent"].map((skill) => {
|
||||
return (
|
||||
<OlDropdownItem
|
||||
onClick={() => {
|
||||
setSpawnSkill(skill);
|
||||
}}
|
||||
className={`w-full`}
|
||||
>
|
||||
<span
|
||||
className={`
|
||||
w-full content-center overflow-hidden text-ellipsis
|
||||
text-nowrap text-left w-max-full flex gap-2
|
||||
`}
|
||||
>
|
||||
<div className="my-auto">{skill}</div>
|
||||
</span>
|
||||
</OlDropdownItem>
|
||||
);
|
||||
})}
|
||||
</OlDropdown>
|
||||
</div>
|
||||
</div>
|
||||
</OlAccordion>
|
||||
</div>
|
||||
<OlAccordion
|
||||
onClick={() => {
|
||||
setShowUnitSummary(!showUnitSummary);
|
||||
}}
|
||||
open={showUnitSummary}
|
||||
title="Unit summary"
|
||||
>
|
||||
<OlUnitSummary blueprint={props.blueprint} coalition={spawnCoalition} />
|
||||
</OlAccordion>
|
||||
{spawnLoadout && spawnLoadout.items.length > 0 && (
|
||||
<OlAccordion
|
||||
onClick={() => {
|
||||
setShowLoadout(!showLoadout);
|
||||
}}
|
||||
open={showLoadout}
|
||||
title="Loadout"
|
||||
>
|
||||
{spawnLoadout.items.map((item) => {
|
||||
return (
|
||||
<div className="flex content-center gap-2">
|
||||
<div
|
||||
className={`
|
||||
my-auto w-6 min-w-6 rounded-full py-0.5 text-center text-sm
|
||||
font-bold text-gray-500
|
||||
dark:bg-[#17212D]
|
||||
`}
|
||||
>
|
||||
{item.quantity}
|
||||
</div>
|
||||
<div
|
||||
className={`
|
||||
my-auto overflow-hidden text-ellipsis text-nowrap text-sm
|
||||
dark:text-gray-300
|
||||
`}
|
||||
>
|
||||
{item.name}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</OlAccordion>
|
||||
)}
|
||||
{(props.latlng || props.airbase) && (
|
||||
<button
|
||||
type="button"
|
||||
data-coalition={props.coalition ?? "blue"}
|
||||
className={`
|
||||
m-2 rounded-lg px-5 py-2.5 text-sm font-medium text-white
|
||||
data-[coalition='blue']:bg-blue-600
|
||||
data-[coalition='neutral']:bg-gray-400
|
||||
data-[coalition='red']:bg-red-500
|
||||
focus:outline-none focus:ring-4
|
||||
`}
|
||||
onClick={() => {
|
||||
if (spawnRequestTable)
|
||||
getApp()
|
||||
.getUnitsManager()
|
||||
.spawnUnits(spawnRequestTable.category, Array(spawnRequestTable.amount).fill(spawnRequestTable.unit), spawnRequestTable.coalition, false, props.airbase?.getName() ?? undefined);
|
||||
|
||||
getApp().setState(OlympusState.IDLE)
|
||||
}}
|
||||
>
|
||||
Spawn
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -2,7 +2,6 @@ import { faArrowLeft, faClose } from "@fortawesome/free-solid-svg-icons";
|
||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||
import React, { useEffect, useState } from "react";
|
||||
import { FaChevronDown, FaChevronUp } from "react-icons/fa";
|
||||
import { HideMenuEvent } from "../../../events";
|
||||
|
||||
export function Menu(props: {
|
||||
title: string;
|
||||
@@ -16,11 +15,7 @@ export function Menu(props: {
|
||||
const [hide, setHide] = useState(true);
|
||||
|
||||
if (!props.open && hide) setHide(false);
|
||||
|
||||
useEffect(() => {
|
||||
HideMenuEvent.dispatch(hide)
|
||||
}, [hide])
|
||||
|
||||
|
||||
return (
|
||||
<div
|
||||
data-open={props.open}
|
||||
|
||||
@@ -1,15 +1,17 @@
|
||||
import React, { useEffect, useState } from "react";
|
||||
import React, { useCallback, useEffect, useState } from "react";
|
||||
import { faHandPointer, faJetFighter, faMap, IconDefinition } from "@fortawesome/free-solid-svg-icons";
|
||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||
import { MAP_OPTIONS_DEFAULTS, NO_SUBSTATE, OlympusState, OlympusSubState, SpawnSubState } from "../../constants/constants";
|
||||
import { ContextActionTarget, MAP_OPTIONS_DEFAULTS, NO_SUBSTATE, OlympusState, OlympusSubState, SpawnSubState } from "../../constants/constants";
|
||||
import { AppStateChangedEvent, MapOptionsChangedEvent } from "../../events";
|
||||
import { getApp } from "../../olympusapp";
|
||||
import { ContextAction } from "../../unit/contextaction";
|
||||
|
||||
export function ControlsPanel(props: {}) {
|
||||
const [controls, setControls] = useState(
|
||||
null as
|
||||
| {
|
||||
actions: (string | number | IconDefinition)[];
|
||||
target: IconDefinition;
|
||||
target: IconDefinition | null;
|
||||
text: string;
|
||||
}[]
|
||||
| null
|
||||
@@ -26,11 +28,11 @@ export function ControlsPanel(props: {}) {
|
||||
MapOptionsChangedEvent.on((mapOptions) => setMapOptions({ ...mapOptions }));
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
const callback = useCallback(() => {
|
||||
const touch = matchMedia("(hover: none)").matches;
|
||||
let controls: {
|
||||
actions: (string | number | IconDefinition)[];
|
||||
target: IconDefinition;
|
||||
target: IconDefinition | null;
|
||||
text: string;
|
||||
}[] = [];
|
||||
|
||||
@@ -42,7 +44,7 @@ export function ControlsPanel(props: {}) {
|
||||
text: "Select unit",
|
||||
},
|
||||
{
|
||||
actions: [touch ? faHandPointer : "LMB", 2],
|
||||
actions: touch ? [faHandPointer, "Hold"] : ["RMB"],
|
||||
target: faMap,
|
||||
text: "Quick spawn menu",
|
||||
},
|
||||
@@ -57,6 +59,58 @@ export function ControlsPanel(props: {}) {
|
||||
text: "Move map location",
|
||||
},
|
||||
];
|
||||
} else if (appState === OlympusState.SPAWN_CONTEXT) {
|
||||
controls = [
|
||||
{
|
||||
actions: [touch ? faHandPointer : "LMB"],
|
||||
target: faJetFighter,
|
||||
text: "Close context menu",
|
||||
},
|
||||
{
|
||||
actions: touch ? [faHandPointer, "Hold"] : ["RMB"],
|
||||
target: faMap,
|
||||
text: "Move context menu",
|
||||
},
|
||||
{
|
||||
actions: touch ? [faHandPointer, "Drag"] : ["Shift", "LMB", "Drag"],
|
||||
target: faMap,
|
||||
text: "Box selection",
|
||||
},
|
||||
{
|
||||
actions: [touch ? faHandPointer : "LMB", "Drag"],
|
||||
target: faMap,
|
||||
text: "Move map location",
|
||||
},
|
||||
];
|
||||
} else if (appState === OlympusState.UNIT_CONTROL) {
|
||||
if (!mapOptions.tabletMode) {
|
||||
controls = Object.values(getApp().getMap().getContextActionSet()?.getContextActions() ?? {})
|
||||
.sort((a: ContextAction, b: ContextAction) => (a.getLabel() > b.getLabel() ? 1 : -1))
|
||||
.filter((contextAction: ContextAction) => contextAction.getOptions().code)
|
||||
.map((contextAction: ContextAction) => {
|
||||
let actions: (string | IconDefinition)[] = [];
|
||||
contextAction.getOptions().shiftKey && actions.push("Shift");
|
||||
contextAction.getOptions().altKey && actions.push("Alt");
|
||||
contextAction.getOptions().ctrlKey && actions.push("Ctrl");
|
||||
actions.push(
|
||||
(contextAction.getOptions().code as string)
|
||||
.replace("Key", "")
|
||||
.replace("ControlLeft", "Ctrl LH")
|
||||
.replace("AltLeft", "Alt LH")
|
||||
.replace("ShiftLeft", "Shift LH")
|
||||
.replace("ControlRight", "Ctrl RH")
|
||||
.replace("AltRight", "Alt RH")
|
||||
.replace("ShiftRight", "Shift RH")
|
||||
);
|
||||
contextAction.getTarget() !== ContextActionTarget.NONE && actions.push(touch ? faHandPointer : "LMB");
|
||||
return {
|
||||
actions: actions,
|
||||
target:
|
||||
contextAction.getTarget() === ContextActionTarget.NONE ? null : contextAction.getTarget() === ContextActionTarget.POINT ? faMap : faJetFighter,
|
||||
text: contextAction.getLabel(),
|
||||
};
|
||||
});
|
||||
}
|
||||
} else if (appState === OlympusState.SPAWN) {
|
||||
controls = [
|
||||
{
|
||||
@@ -91,7 +145,9 @@ export function ControlsPanel(props: {}) {
|
||||
}
|
||||
|
||||
setControls(controls);
|
||||
}, [appState, appSubState]);
|
||||
}, [appState, appSubState, mapOptions]);
|
||||
|
||||
useEffect(callback, [appState, appSubState, mapOptions]);
|
||||
|
||||
return (
|
||||
<div
|
||||
@@ -122,9 +178,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>}
|
||||
|
||||
@@ -15,7 +15,7 @@ import {
|
||||
olButtonsVisibilityNavyunit,
|
||||
olButtonsVisibilityOlympus,
|
||||
} from "../components/olicons";
|
||||
import { FaChevronLeft, FaChevronRight } from "react-icons/fa6";
|
||||
import { FaChevronLeft, FaChevronRight, FaComputer, FaTabletScreenButton } from "react-icons/fa6";
|
||||
import { CommandModeOptionsChangedEvent, ConfigLoadedEvent, HiddenTypesChangedEvent, MapOptionsChangedEvent, MapSourceChangedEvent } from "../../events";
|
||||
import { BLUE_COMMANDER, COMMAND_MODE_OPTIONS_DEFAULTS, MAP_HIDDEN_TYPES_DEFAULTS, MAP_OPTIONS_DEFAULTS } from "../../constants/constants";
|
||||
import { OlympusConfig } from "../../interfaces";
|
||||
@@ -111,12 +111,18 @@ export function Header() {
|
||||
</div>
|
||||
</div>
|
||||
{commandModeOptions.commandMode === BLUE_COMMANDER && (
|
||||
<div
|
||||
className={`flex h-full rounded-md bg-blue-600 px-4 text-white`}
|
||||
>
|
||||
<div className={`flex h-full rounded-md bg-blue-600 px-4 text-white`}>
|
||||
<span className="my-auto font-bold">BLUE Commander ({commandModeOptions.spawnPoints.blue} points)</span>
|
||||
</div>
|
||||
)}
|
||||
<div
|
||||
className="cursor-pointer rounded-full bg-blue-500 px-4 py-2 text-white"
|
||||
onClick={() => {
|
||||
getApp().getMap().setOption("tabletMode", !mapOptions.tabletMode);
|
||||
}}
|
||||
>
|
||||
{mapOptions.tabletMode ? <FaTabletScreenButton /> : <FaComputer />}
|
||||
</div>
|
||||
<div className={`flex h-fit flex-row items-center justify-start gap-1`}>
|
||||
<OlLockStateButton
|
||||
checked={!mapOptions.protectDCSUnits}
|
||||
@@ -213,7 +219,7 @@ export function Header() {
|
||||
.getMap()
|
||||
.setOption("cameraPluginMode", mapOptions.cameraPluginMode === "live" ? "map" : "live");
|
||||
}}
|
||||
></OlLabelToggle>
|
||||
/>
|
||||
<OlStateButton
|
||||
checked={mapOptions.cameraPluginEnabled}
|
||||
icon={faCamera}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import React, { useEffect, useState } from "react";
|
||||
import { AppStateChangedEvent, ContextActionChangedEvent, HideMenuEvent, HotgroupsChangedEvent, InfoPopupEvent } from "../../events";
|
||||
import { AppStateChangedEvent, ContextActionChangedEvent, HotgroupsChangedEvent, InfoPopupEvent } from "../../events";
|
||||
import { OlympusState } from "../../constants/constants";
|
||||
import { ContextAction } from "../../unit/contextaction";
|
||||
import { OlStateButton } from "../components/olstatebutton";
|
||||
@@ -13,7 +13,6 @@ export function HotGroupBar(props: {}) {
|
||||
|
||||
useEffect(() => {
|
||||
AppStateChangedEvent.on((state, subState) => setAppState(state));
|
||||
HideMenuEvent.on((hidden) => setMenuHidden(hidden));
|
||||
HotgroupsChangedEvent.on((hotgroups) => setHotgroups({ ...hotgroups }));
|
||||
}, []);
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import React, { useEffect, useState } from "react";
|
||||
import { AppStateChangedEvent, ContextActionChangedEvent, HideMenuEvent, InfoPopupEvent } from "../../events";
|
||||
import { AppStateChangedEvent, ContextActionChangedEvent, InfoPopupEvent } from "../../events";
|
||||
import { OlympusState } from "../../constants/constants";
|
||||
import { ContextAction } from "../../unit/contextaction";
|
||||
|
||||
@@ -7,35 +7,16 @@ export function InfoBar(props: {}) {
|
||||
const [messages, setMessages] = useState([] as string[]);
|
||||
const [appState, setAppState] = useState(OlympusState.NOT_INITIALIZED);
|
||||
const [contextAction, setContextAction] = useState(null as ContextAction | null);
|
||||
const [menuHidden, setMenuHidden] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
InfoPopupEvent.on((messages) => setMessages([...messages]));
|
||||
AppStateChangedEvent.on((state, subState) => setAppState(state));
|
||||
ContextActionChangedEvent.on((contextAction) => setContextAction(contextAction));
|
||||
HideMenuEvent.on((hidden) => setMenuHidden(hidden));
|
||||
}, []);
|
||||
|
||||
let topString = "";
|
||||
if (appState === OlympusState.UNIT_CONTROL) {
|
||||
if (contextAction === null) {
|
||||
topString = "top-36";
|
||||
} else {
|
||||
topString = "top-48";
|
||||
}
|
||||
} else {
|
||||
topString = "top-16";
|
||||
}
|
||||
|
||||
return (
|
||||
<div
|
||||
data-menuhidden={menuHidden || appState === OlympusState.IDLE}
|
||||
className={`
|
||||
absolute left-[50%]
|
||||
data-[menuhidden='false']:translate-x-[calc(200px-50%+2rem)]
|
||||
data-[menuhidden='true']:translate-x-[calc(-50%+2rem)]
|
||||
${topString}
|
||||
`}
|
||||
className={`absolute left-[50%] top-16`}
|
||||
>
|
||||
{messages.slice(Math.max(0, messages.length - 4), Math.max(0, messages.length)).map((message, idx) => {
|
||||
return (
|
||||
|
||||
@@ -436,7 +436,6 @@ export function SpawnMenu(props: { open: boolean; onClose: () => void; children?
|
||||
{!(blueprint === null) && (
|
||||
<UnitSpawnMenu
|
||||
blueprint={blueprint}
|
||||
spawnAtLocation={true}
|
||||
starredSpawns={starredSpawns}
|
||||
coalition={commandModeOptions.commandMode !== GAME_MASTER ? (commandModeOptions.commandMode === BLUE_COMMANDER ? "blue" : "red") : undefined}
|
||||
/>
|
||||
|
||||
@@ -3,19 +3,20 @@ import { ContextActionSet } from "../../unit/contextactionset";
|
||||
import { OlStateButton } from "../components/olstatebutton";
|
||||
import { getApp } from "../../olympusapp";
|
||||
import { ContextAction } from "../../unit/contextaction";
|
||||
import { CONTEXT_ACTION_COLORS } from "../../constants/constants";
|
||||
import { CONTEXT_ACTION_COLORS, MAP_OPTIONS_DEFAULTS } from "../../constants/constants";
|
||||
import { FaInfoCircle } from "react-icons/fa";
|
||||
import { FaChevronLeft, FaChevronRight } from "react-icons/fa6";
|
||||
import { FaChevronDown, FaChevronLeft, FaChevronRight, FaChevronUp } from "react-icons/fa6";
|
||||
import { OlympusState } from "../../constants/constants";
|
||||
import { AppStateChangedEvent, ContextActionChangedEvent, ContextActionSetChangedEvent, HideMenuEvent } from "../../events";
|
||||
import { AppStateChangedEvent, ContextActionChangedEvent, ContextActionSetChangedEvent, MapOptionsChangedEvent } from "../../events";
|
||||
|
||||
export function UnitControlBar(props: {}) {
|
||||
const [appState, setAppState] = useState(OlympusState.NOT_INITIALIZED);
|
||||
const [contextActionSet, setcontextActionSet] = useState(null as ContextActionSet | null);
|
||||
const [contextAction, setContextAction] = useState(null as ContextAction | null);
|
||||
const [scrolledLeft, setScrolledLeft] = useState(true);
|
||||
const [scrolledRight, setScrolledRight] = useState(false);
|
||||
const [scrolledTop, setScrolledTop] = useState(true);
|
||||
const [scrolledBottom, setScrolledBottom] = useState(false);
|
||||
const [menuHidden, setMenuHidden] = useState(false);
|
||||
const [mapOptions, setMapOptions] = useState(MAP_OPTIONS_DEFAULTS);
|
||||
|
||||
/* Initialize the "scroll" position of the element */
|
||||
var scrollRef = useRef(null);
|
||||
@@ -27,18 +28,18 @@ export function UnitControlBar(props: {}) {
|
||||
AppStateChangedEvent.on((state, subState) => setAppState(state));
|
||||
ContextActionSetChangedEvent.on((contextActionSet) => setcontextActionSet(contextActionSet));
|
||||
ContextActionChangedEvent.on((contextAction) => setContextAction(contextAction));
|
||||
HideMenuEvent.on((hidden) => setMenuHidden(hidden));
|
||||
MapOptionsChangedEvent.on((mapOptions) => setMapOptions({ ...mapOptions }));
|
||||
}, []);
|
||||
|
||||
function onScroll(el) {
|
||||
const sl = el.scrollLeft;
|
||||
const sr = el.scrollWidth - el.scrollLeft - el.clientWidth;
|
||||
const sl = el.scrollTop;
|
||||
const sr = el.scrollHeight - el.scrollTop - el.clientHeight;
|
||||
|
||||
sl < 1 && !scrolledLeft && setScrolledLeft(true);
|
||||
sl > 1 && scrolledLeft && setScrolledLeft(false);
|
||||
sl < 1 && !scrolledTop && setScrolledTop(true);
|
||||
sl > 1 && scrolledTop && setScrolledTop(false);
|
||||
|
||||
sr < 1 && !scrolledRight && setScrolledRight(true);
|
||||
sr > 1 && scrolledRight && setScrolledRight(false);
|
||||
sr < 1 && !scrolledBottom && setScrolledBottom(true);
|
||||
sr > 1 && scrolledBottom && setScrolledBottom(false);
|
||||
}
|
||||
|
||||
let reorderedActions: ContextAction[] = contextActionSet
|
||||
@@ -49,70 +50,68 @@ export function UnitControlBar(props: {}) {
|
||||
<>
|
||||
{appState === OlympusState.UNIT_CONTROL && contextActionSet && Object.keys(contextActionSet.getContextActions()).length > 0 && (
|
||||
<>
|
||||
<div
|
||||
data-menuhidden={menuHidden}
|
||||
className={`
|
||||
absolute left-[50%] top-16 flex max-w-[80%] gap-2 rounded-md
|
||||
bg-gray-200
|
||||
dark:bg-olympus-900
|
||||
data-[menuhidden='false']:translate-x-[calc(200px-50%+2rem)]
|
||||
data-[menuhidden='true']:translate-x-[calc(-50%+2rem)]
|
||||
`}
|
||||
>
|
||||
{!scrolledLeft && (
|
||||
<FaChevronLeft
|
||||
{mapOptions.tabletMode && (
|
||||
<>
|
||||
<div
|
||||
data-menuhidden={menuHidden}
|
||||
className={`
|
||||
absolute left-0 h-full w-6 rounded-lg px-2 py-3.5
|
||||
text-gray-200
|
||||
absolute right-2 top-16 flex max-h-[80%] gap-2 rounded-md
|
||||
bg-gray-200
|
||||
dark:bg-olympus-900
|
||||
`}
|
||||
/>
|
||||
)}
|
||||
<div className="flex gap-2 overflow-x-auto no-scrollbar p-2" onScroll={(ev) => onScroll(ev.target)} ref={scrollRef}>
|
||||
{reorderedActions.map((contextActionIt: ContextAction) => {
|
||||
return (
|
||||
<div className="flex flex-col gap-1">
|
||||
<OlStateButton
|
||||
key={contextActionIt.getId()}
|
||||
checked={contextActionIt === contextAction}
|
||||
icon={contextActionIt.getIcon()}
|
||||
tooltip={contextActionIt.getLabel()}
|
||||
buttonColor={CONTEXT_ACTION_COLORS[contextActionIt.getOptions().type ?? 0]}
|
||||
onClick={() => {
|
||||
if (contextActionIt.getOptions().executeImmediately) {
|
||||
contextActionIt.executeCallback(null, null);
|
||||
} else {
|
||||
contextActionIt !== contextAction ? getApp().getMap().setContextAction(contextActionIt) : getApp().getMap().setContextAction(null);
|
||||
}
|
||||
}}
|
||||
/>
|
||||
<div
|
||||
className={`
|
||||
rounded-sm bg-gray-400 text-center text-xs font-bold
|
||||
text-olympus-800
|
||||
`}
|
||||
>
|
||||
{(contextActionIt.getOptions().hotkey ?? "").replace("Key", "")}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
{!scrolledRight && (
|
||||
<FaChevronRight
|
||||
className={`
|
||||
absolute right-0 h-full w-6 rounded-lg px-2 py-3.5
|
||||
text-gray-200
|
||||
dark:bg-olympus-900
|
||||
`}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
{/*}
|
||||
>
|
||||
{!scrolledTop && (
|
||||
<FaChevronUp
|
||||
className={`
|
||||
absolute top-0 h-6 w-full rounded-lg px-2 py-3.5
|
||||
text-gray-200
|
||||
dark:bg-olympus-900
|
||||
`}
|
||||
/>
|
||||
)}
|
||||
<div className={`
|
||||
flex flex-col gap-2 overflow-y-auto no-scrollbar p-2
|
||||
`} onScroll={(ev) => onScroll(ev.target)} ref={scrollRef}>
|
||||
{reorderedActions.map((contextActionIt: ContextAction) => {
|
||||
return (
|
||||
<div className="flex flex-col gap-1">
|
||||
<OlStateButton
|
||||
key={contextActionIt.getId()}
|
||||
checked={contextActionIt === contextAction}
|
||||
icon={contextActionIt.getIcon()}
|
||||
tooltip={contextActionIt.getLabel()}
|
||||
buttonColor={CONTEXT_ACTION_COLORS[contextActionIt.getOptions().type ?? 0]}
|
||||
onClick={() => {
|
||||
if (contextActionIt.getOptions().executeImmediately) {
|
||||
contextActionIt.executeCallback(null, null);
|
||||
} else {
|
||||
contextActionIt !== contextAction
|
||||
? getApp().getMap().setContextAction(contextActionIt)
|
||||
: getApp().getMap().setContextAction(null);
|
||||
}
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
{!scrolledBottom && (
|
||||
<FaChevronDown
|
||||
className={`
|
||||
absolute bottom-0 h-6 w-full rounded-lg px-2 py-3.5
|
||||
text-gray-200
|
||||
dark:bg-olympus-900
|
||||
`}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
|
||||
{contextAction && (
|
||||
<div
|
||||
className={`
|
||||
absolute left-[50%] top-32 flex min-w-[300px]
|
||||
absolute left-[50%] top-16 flex min-w-[300px]
|
||||
translate-x-[calc(-50%+2rem)] items-center gap-2 rounded-md
|
||||
bg-gray-200 p-4
|
||||
dark:bg-olympus-800
|
||||
@@ -135,7 +134,6 @@ export function UnitControlBar(props: {}) {
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
{*/}
|
||||
</>
|
||||
)}
|
||||
</>
|
||||
|
||||
@@ -21,7 +21,6 @@ import { OlAccordion } from "../components/olaccordion";
|
||||
export function UnitSpawnMenu(props: {
|
||||
starredSpawns: { [key: string]: SpawnRequestTable };
|
||||
blueprint: UnitBlueprint;
|
||||
spawnAtLocation: boolean;
|
||||
airbase?: Airbase | null;
|
||||
coalition?: Coalition;
|
||||
}) {
|
||||
@@ -49,7 +48,7 @@ export function UnitSpawnMenu(props: {
|
||||
|
||||
/* When the menu is opened show the unit preview on the map as a cursor */
|
||||
useEffect(() => {
|
||||
if (props.spawnAtLocation && spawnRequestTable) {
|
||||
if (!props.airbase && spawnRequestTable) {
|
||||
/* Refresh the unique key identified */
|
||||
const newKey = hash(JSON.stringify(spawnRequestTable));
|
||||
setKey(newKey);
|
||||
@@ -67,7 +66,7 @@ export function UnitSpawnMenu(props: {
|
||||
|
||||
/* Callback and effect to update the quick access name in the input field */
|
||||
const updateQuickAccessName = useCallback(() => {
|
||||
if (props.spawnAtLocation) {
|
||||
if (!props.airbase) {
|
||||
/* If the spawn is starred, set the quick access name */
|
||||
if (key in props.starredSpawns && props.starredSpawns[key].quickAccessName) setQuickAccessName(props.starredSpawns[key].quickAccessName);
|
||||
else setQuickAccessName("No name");
|
||||
@@ -408,7 +407,7 @@ export function UnitSpawnMenu(props: {
|
||||
</OlAccordion>
|
||||
</div>
|
||||
)}
|
||||
{!props.spawnAtLocation && (
|
||||
{props.airbase && (
|
||||
<button
|
||||
type="button"
|
||||
className={`
|
||||
@@ -422,7 +421,7 @@ export function UnitSpawnMenu(props: {
|
||||
if (spawnRequestTable)
|
||||
getApp()
|
||||
.getUnitsManager()
|
||||
.spawnUnits(spawnRequestTable.category, [spawnRequestTable.unit], spawnRequestTable.coalition, false, props.airbase?.getName());
|
||||
.spawnUnits(spawnRequestTable.category, Array(spawnRequestTable.amount).fill(spawnRequestTable.unit), spawnRequestTable.coalition, false, props.airbase?.getName());
|
||||
}}
|
||||
>
|
||||
Spawn
|
||||
|
||||
@@ -28,7 +28,7 @@ import { AppStateChangedEvent, MapOptionsChangedEvent } from "../events";
|
||||
import { GameMasterMenu } from "./panels/gamemastermenu";
|
||||
import { InfoBar } from "./panels/infobar";
|
||||
import { HotGroupBar } from "./panels/hotgroupsbar";
|
||||
import { StarredSpawnContextMenu } from "./contextmenus/starredspawncontextmenu";
|
||||
import { SpawnContextMenu } from "./contextmenus/SpawnContextmenu";
|
||||
import { CoordinatesPanel } from "./panels/coordinatespanel";
|
||||
|
||||
export type OlympusUIState = {
|
||||
@@ -112,7 +112,7 @@ export function UI() {
|
||||
<HotGroupBar />
|
||||
|
||||
<MapContextMenu />
|
||||
<StarredSpawnContextMenu />
|
||||
<SpawnContextMenu />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
Reference in New Issue
Block a user