Added spawn context menu and controls rework

This commit is contained in:
Davide Passoni
2024-11-19 17:45:46 +01:00
parent 430d8db15d
commit 38f6788fa8
34 changed files with 1568 additions and 726 deletions

View File

@@ -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>
);
}

View File

@@ -113,7 +113,6 @@ export function MapContextMenu(props: {}) {
contextActionIt.executeCallback(unit, null);
}
}
getApp().setState(OlympusState.UNIT_CONTROL);
}}
>
<FontAwesomeIcon className="my-auto" icon={contextActionIt.getIcon()} />

View 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>
</>
)}
</>
);
}

View File

@@ -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>
</>
)}
</>
);
}

View File

@@ -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}
/>

View 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>
);
}

View 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>
);
}

View File

@@ -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}

View File

@@ -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>}

View File

@@ -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}

View File

@@ -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 }));
}, []);

View File

@@ -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 (

View File

@@ -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}
/>

View File

@@ -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>
)}
{*/}
</>
)}
</>

View File

@@ -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

View File

@@ -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>
);