DCSOlympus/frontend/react/src/ui/panels/unitspawnmenu.tsx
2025-10-26 16:34:27 +01:00

1351 lines
85 KiB
TypeScript

import React, { useState, useEffect, useCallback, useRef } 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, UnitSpawnTable } from "../../interfaces";
import { OlStateButton } from "../components/olstatebutton";
import { Coalition } from "../../types/types";
import { getApp } from "../../olympusapp";
import { deepCopyTable, deg2rad, ftToM, hash, mode, normalizeAngle } 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 { AppStateChangedEvent, CustomLoadoutsUpdatedEvent, SetLoadoutWizardBlueprintEvent, SpawnHeadingChangedEvent } from "../../events";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { FaMagic, FaQuestionCircle } from "react-icons/fa";
import { OlExpandingTooltip } from "../components/olexpandingtooltip";
import { LoadoutViewer } from "./components/loadoutviewer";
enum OpenAccordion {
NONE,
LOADOUT,
UNIT_SUMMARY,
ADVANCED_OPTIONS,
LOADOUT_WIZARD,
}
export function UnitSpawnMenu(props: {
visible: boolean;
compact: boolean;
starredSpawns: { [key: string]: SpawnRequestTable };
blueprint: UnitBlueprint | null;
airbase?: Airbase | null;
latlng?: LatLng | null;
coalition?: Coalition;
onBack?: () => void;
}) {
/* Compute the min and max values depending on the unit type */
const minNumber = 1;
const maxNumber = groupUnitCount[props.blueprint?.category ?? "aircraft"];
const minAltitude = minAltitudeValues[props.blueprint?.category ?? "aircraft"];
const maxAltitude = maxAltitudeValues[props.blueprint?.category ?? "aircraft"];
const altitudeStep = altitudeIncrements[props.blueprint?.category ?? "aircraft"];
/* State initialization */
const [appState, setAppState] = useState(OlympusState.NOT_INITIALIZED);
const [spawnCoalition, setSpawnCoalition] = useState("blue" as Coalition);
const [spawnNumber, setSpawnNumber] = useState(1);
const [spawnRole, setSpawnRole] = useState("");
const [spawnLoadout, setSpawnLoadout] = useState(null as null | LoadoutBlueprint);
const [spawnAltitude, setSpawnAltitude] = useState((maxAltitude - minAltitude) / 2);
const [spawnAltitudeType, setSpawnAltitudeType] = useState(false);
const [spawnLiveryID, setSpawnLiveryID] = useState("");
const [spawnSkill, setSpawnSkill] = useState("High");
const [quickAccessName, setQuickAccessName] = useState("Preset 1");
const [key, setKey] = useState("");
const [spawnRequestTable, setSpawnRequestTable] = useState(null as null | SpawnRequestTable);
const [openAccordion, setOpenAccordion] = useState(OpenAccordion.NONE);
const [showLoadout, setShowLoadout] = useState(false);
useEffect(() => {
setAppState(getApp()?.getState());
AppStateChangedEvent.on((state, subState) => setAppState(state));
CustomLoadoutsUpdatedEvent.on((unitName, loadout) => {
setSpawnRole(loadout.roles[0]);
setSpawnLoadout(loadout);
});
}, []);
useEffect(() => {
setSpawnRole("");
setSpawnLoadout(null);
setSpawnLiveryID("");
}, [props.blueprint]);
/* When the menu is opened show the unit preview on the map as a cursor */
const setSpawnRequestTableCallback = useCallback(() => {
if (spawnRequestTable) {
/* Refresh the unique key identified */
const tempTable = deepCopyTable(spawnRequestTable);
delete tempTable.quickAccessName;
delete tempTable.unit.location;
delete tempTable.unit.altitude;
const newKey = hash(JSON.stringify(spawnRequestTable));
setKey(newKey);
getApp()?.getMap()?.setSpawnRequestTable(spawnRequestTable);
if (!props.airbase && !props.latlng && appState === OlympusState.SPAWN) getApp().setState(OlympusState.SPAWN, SpawnSubState.SPAWN_UNIT);
}
}, [spawnRequestTable, appState]);
useEffect(setSpawnRequestTableCallback, [spawnRequestTable]);
/* Callback and effect to update the quick access name of the starredSpawn */
const updateStarredSpawnQuickAccessName = useCallback(() => {
if (key in props.starredSpawns) props.starredSpawns[key].quickAccessName = quickAccessName;
}, [props.starredSpawns, key, quickAccessName]);
useEffect(updateStarredSpawnQuickAccessName, [quickAccessName]);
/* Callback and effect to update the quick access name in the input field */
const updateQuickAccessName = useCallback(() => {
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(`Preset ${Object.keys(props.starredSpawns).length + 1}`);
}
}, [props.starredSpawns, key]);
useEffect(updateQuickAccessName, [key]);
/* Callback and effect to update the spawn request table */
const updateSpawnRequestTable = useCallback(() => {
if (props.blueprint !== null) {
const loadoutCode = spawnLoadout ? (props.blueprint.loadouts?.find((loadout) => loadout.name === spawnLoadout.name)?.code ?? "") : "";
const loadoutPayload = spawnLoadout
? (props.blueprint.loadouts?.find((loadout) => loadout.name === spawnLoadout.name)?.payload ?? undefined)
: undefined;
const unitTable: UnitSpawnTable = {
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: loadoutCode,
};
if (loadoutPayload) unitTable.payload = loadoutPayload;
setSpawnRequestTable({
category: props.blueprint?.category,
unit: unitTable,
amount: spawnNumber,
coalition: spawnCoalition,
});
}
}, [props.blueprint, props.latlng, spawnAltitude, spawnLoadout, spawnCoalition, spawnNumber, spawnLiveryID, spawnSkill]);
useEffect(updateSpawnRequestTable, [props.blueprint, props.latlng, spawnAltitude, spawnLoadout, spawnCoalition, spawnNumber, spawnLiveryID, spawnSkill]);
/* Effect to update the coalition if it is forced externally */
useEffect(() => {
if (props.coalition) setSpawnCoalition(props.coalition);
}, [props.coalition]);
/* Effect to update the initial altitude when the blueprint changes */
useEffect(() => {
setSpawnAltitude((maxAltitude - minAltitude) / 2);
}, [props.blueprint]);
/* Heading compass */
const [compassAngle, setCompassAngle] = useState(0);
const compassRef = useRef<HTMLImageElement>(null);
const updateSpawnRequestTableHeading = useCallback(() => {
getApp()?.getMap().setSpawnHeading(compassAngle);
}, [compassAngle]);
useEffect(updateSpawnRequestTableHeading, [compassAngle]);
useEffect(() => {
SpawnHeadingChangedEvent.on((heading) => {
setCompassAngle(heading);
});
}, []);
useEffect(() => {
setCompassAngle(getApp()?.getMap().getSpawnHeading() ?? 0);
}, [appState]);
const handleMouseDown = (e: React.MouseEvent) => {
e.preventDefault();
const onMouseMove = (e: MouseEvent) => {
if (compassRef.current) {
const rect = compassRef.current.getBoundingClientRect();
const centerX = rect.left + rect.width / 2;
const centerY = rect.top + rect.height / 2;
const angle = Math.atan2(e.clientY - centerY, e.clientX - centerX) * (180 / Math.PI);
setCompassAngle(Math.round(normalizeAngle(angle + 90)));
}
};
const onMouseUp = () => {
document.removeEventListener("mousemove", onMouseMove);
document.removeEventListener("mouseup", onMouseUp);
};
document.addEventListener("mousemove", onMouseMove);
document.addEventListener("mouseup", onMouseUp);
};
/* Get a list of all the roles */
const roles: string[] = [];
props.blueprint?.loadouts?.forEach((loadout) => {
loadout.roles.forEach((role) => {
!roles.includes(role) && roles.push(role);
});
});
/* Initialize the role */
let allRoles = props.blueprint?.loadouts?.flatMap((loadout) => loadout.roles).filter((role) => role !== "No task");
// If there are loadouts with Custom role, include Custom in the role selection
const hasCustomRoleLoadouts = props.blueprint?.loadouts?.some((loadout) => loadout.roles.includes("Custom"));
if (hasCustomRoleLoadouts && allRoles) allRoles.push("Custom");
// If there are custom loadouts, select "Custom" as the main role
let mainRole = hasCustomRoleLoadouts ? "Custom" : roles[0];
if (allRoles !== undefined) mainRole = mode(allRoles);
spawnRole === "" && roles.length > 0 && setSpawnRole(mainRole);
// Filter the loadouts based on the selected role
const filteredLoadouts = props.blueprint?.loadouts?.filter((loadout) => loadout.roles.includes(spawnRole));
// Order the loadouts so that custom loadouts appear first and "Empty loadout" appears last
if (filteredLoadouts) {
filteredLoadouts.sort((a, b) => {
if (a.isCustom && !b.isCustom) return -1;
if (!a.isCustom && b.isCustom) return 1;
if (a.name === "Empty loadout") return 1;
if (b.name === "Empty loadout") return -1;
return 0;
});
}
/* Effect to reset the loadout if the role changes */
useEffect(() => {
// If the current loadout is not in the filtered loadouts, reset it
if (spawnLoadout && filteredLoadouts && !filteredLoadouts.includes(spawnLoadout)) {
setSpawnLoadout(null);
}
}, [spawnRole, props.blueprint]);
/* Initialize the loadout */
const initializeLoadout = useCallback(() => {
if (spawnLoadout && filteredLoadouts && filteredLoadouts.includes(spawnLoadout)) return;
if (filteredLoadouts && filteredLoadouts.length > 0) {
if (filteredLoadouts.filter((loadout) => loadout.name !== "Empty loadout").length > 0)
setSpawnLoadout(filteredLoadouts.filter((loadout) => loadout.name !== "Empty loadout")[0]);
else setSpawnLoadout(filteredLoadouts[0]);
}
}, [filteredLoadouts, spawnLoadout]);
useEffect(initializeLoadout, [filteredLoadouts]);
return (
<>
{props.compact ? (
<>
{props.visible && (
<div
className={`
flex max-h-[800px] flex-col overflow-auto
`}
>
<div className="mr-2">
<div className="flex h-fit flex-col gap-2">
<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-nowrap text-sm
text-white
`}
>
Quick access:
</div>
<OlStringInput
onChange={(e) => {
setQuickAccessName(e.target.value);
}}
value={quickAccessName}
/>
<OlStateButton
onClick={() => {
if (spawnRequestTable) {
spawnRequestTable.unit.heading = compassAngle;
if (key in props.starredSpawns) getApp().getMap().removeStarredSpawnRequestTable(key);
else getApp().getMap().addStarredSpawnRequestTable(key, spawnRequestTable, quickAccessName);
}
}}
tooltip={() => (
<OlExpandingTooltip
title="Add this spawn to quick access"
content="Enter a name and click on the button to later be able to quickly respawn a unit with the same configuration."
/>
)}
checked={key in props.starredSpawns}
icon={faStar}
></OlStateButton>
</div>
{["aircraft", "helicopter"].includes(props.blueprint?.category ?? "aircraft") && (
<>
{!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-gray-400
`}
>
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)}
tooltip={() => (
<OlExpandingTooltip
title="Select altitude type"
content="If AGL is selected, the aircraft will be spawned at the selected altitude above the ground. If ASL is selected, the aircraft will be spawned at the selected altitude above sea level."
/>
)}
tooltipRelativeToParent={true}
/>
</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-gray-400
`}
>
Role
</span>
<OlDropdown
label={spawnRole}
className="w-64"
tooltip={() => (
<OlExpandingTooltip
title="Role of the spawned unit"
content="This selection has no effect on what the spawned unit will actually perform, and you will have total control on it. However, it is used to filter what loadouts are available."
/>
)}
tooltipRelativeToParent={true}
>
{roles.map((role) => {
return (
<OlDropdownItem
onClick={() => {
setSpawnRole(role);
}}
className={`
w-full
`}
key={role}
>
{role}
</OlDropdownItem>
);
})}
</OlDropdown>
</div>
<div
className={`
flex flex-col content-center
justify-between gap-2
`}
>
<span
className={`
my-auto font-normal
dark:text-gray-400
`}
>
Loadout
</span>
<OlDropdown
label={spawnLoadout ? spawnLoadout.name : "Default"}
className={`w-full`}
tooltip={() => (
<OlExpandingTooltip
title="Unit loadout"
content="Selects the loadout of the spawned unit. Depending on the selection, the unit will be equipped with different weapons and equipment."
/>
)}
tooltipRelativeToParent={true}
>
{filteredLoadouts?.map((loadout) => {
return (
<OlDropdownItem
key={loadout.name}
onClick={() => {
setSpawnLoadout(loadout);
}}
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
`}
>
{loadout.name}
</div>
</span>
</OlDropdownItem>
);
})}
</OlDropdown>
</div>
<div>
<button
type="button"
className={`
flex w-full justify-center
gap-2 rounded-md
border-[1px] p-2
align-middle text-sm
dark:text-white
hover:bg-white/10
`}
onClick={() => {
SetLoadoutWizardBlueprintEvent.dispatch(props.blueprint!);
getApp().setState(OlympusState.SPAWN, SpawnSubState.LOADOUT_WIZARD);
}}
>
<FaMagic className="my-auto" />
Loadout wizard
</button>
</div>
</>
)}
<OlAccordion
onClick={() => {
setOpenAccordion(
openAccordion === OpenAccordion.ADVANCED_OPTIONS ? OpenAccordion.NONE : OpenAccordion.ADVANCED_OPTIONS
);
}}
open={openAccordion === OpenAccordion.ADVANCED_OPTIONS}
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-gray-400
`}
>
Livery
</span>
<OlDropdown
label={
props.blueprint?.liveries ? (props.blueprint?.liveries[spawnLiveryID]?.name ?? "Default") : "No livery"
}
className={`w-64`}
tooltip={() => (
<OlExpandingTooltip
title="Unit livery"
content="Selects the livery of the spawned unit. This is a purely cosmetic option."
/>
)}
tooltipRelativeToParent={true}
>
{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
`}
key={id}
>
<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-gray-400
`}
>
Skill
</span>
<OlDropdown
label={spawnSkill}
className={`w-64`}
tooltip={() => (
<OlExpandingTooltip
title="Unit skill"
content="Selects the skill of the spawned unit. Depending on the selection, the unit will be more precise and effective at its mission. Usually a lower level is selected to generate a more forgiving mission."
/>
)}
tooltipRelativeToParent={true}
>
{["Average", "Good", "High", "Excellent"].map((skill) => {
return (
<OlDropdownItem
onClick={() => {
setSpawnSkill(skill);
}}
className={`
w-full
`}
key={skill}
>
<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>
<div
className={`
my-5 flex justify-between
`}
>
<div
className={`
my-auto flex flex-col gap-2
`}
>
<span>Spawn heading</span>
<div
className={`
flex gap-1 text-sm
text-gray-400
`}
>
<FaQuestionCircle
className={`
my-auto
`}
/>{" "}
<div
className={`
my-auto
`}
>
Drag to change
</div>
</div>
</div>
<OlNumberInput
className={"my-auto"}
min={0}
max={360}
onChange={(ev) => {
setCompassAngle(Number(ev.target.value));
}}
onDecrease={() => {
setCompassAngle(normalizeAngle(compassAngle - 1));
}}
onIncrease={() => {
setCompassAngle(normalizeAngle(compassAngle + 1));
}}
value={compassAngle}
tooltip={() => (
<OlExpandingTooltip
title="Spawn heading"
content="This controls the direction the unit will face when spanwned. This is important for units that take longer to change direction, like ships. Air units and helicopters will enter a orbit with its major axis aligned with the spawn heading. Drag the compass to change the heading."
/>
)}
tooltipRelativeToParent={true}
/>
<div
className={`
relative mr-3 h-[60px]
w-[60px]
`}
>
<img
className="absolute"
ref={compassRef}
onMouseDown={handleMouseDown}
src={"images/others/arrow_background.png"}
></img>
<img
className="absolute left-0"
ref={compassRef}
onMouseDown={handleMouseDown}
src={"images/others/arrow.png"}
style={{
width: "60px",
height: "60px",
transform: `rotate(${compassAngle}deg)`,
cursor: "pointer",
}}
></img>
</div>
</div>
</OlAccordion>
</div>
<OlAccordion
onClick={() => {
setOpenAccordion(openAccordion === OpenAccordion.UNIT_SUMMARY ? OpenAccordion.NONE : OpenAccordion.UNIT_SUMMARY);
}}
open={openAccordion === OpenAccordion.UNIT_SUMMARY}
title="Unit summary"
>
{props.blueprint ? <OlUnitSummary blueprint={props.blueprint} coalition={spawnCoalition} /> : <span></span>}
</OlAccordion>
{spawnLoadout && spawnLoadout.items.length > 0 && (
<OlAccordion
onClick={() => {
setShowLoadout(!showLoadout);
}}
open={showLoadout}
title="Loadout"
>
<LoadoutViewer spawnLoadout={spawnLoadout} />
</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) {
spawnRequestTable.unit.heading = deg2rad(compassAngle);
getApp()
.getUnitsManager()
.spawnUnits(
spawnRequestTable.category,
Array(spawnRequestTable.amount)
.fill(spawnRequestTable.unit)
.map((unit, index) => {
return {
...unit,
location: new LatLng(
unit.location.lat +
(spawnRequestTable?.category === "groundunit" ? 0.00025 * index : 0.005 * index),
unit.location.lng
),
heading: unit.heading || 0,
};
}),
spawnRequestTable.coalition,
false,
props.airbase?.getName() ?? undefined
);
}
getApp().setState(OlympusState.IDLE);
}}
>
Spawn
</button>
)}
</div>
</div>
)}
</>
) : (
<>
{" "}
{props.visible && (
<div className="flex flex-col">
{props.blueprint && <OlUnitSummary blueprint={props.blueprint} coalition={spawnCoalition} />}
<div
className={`
flex h-fit flex-col gap-2 px-5 pb-8 pt-6
`}
>
<div
className={`
inline-flex w-full flex-row content-center
justify-between gap-2
`}
>
<div className="my-auto text-white">Quick access: </div>
<OlStringInput
onChange={(e) => {
setQuickAccessName(e.target.value);
}}
value={quickAccessName}
/>
<OlStateButton
onClick={() => {
if (spawnRequestTable) {
spawnRequestTable.unit.heading = compassAngle;
if (key in props.starredSpawns) getApp().getMap().removeStarredSpawnRequestTable(key);
else getApp().getMap().addStarredSpawnRequestTable(key, spawnRequestTable, quickAccessName);
}
}}
tooltip={() => (
<OlExpandingTooltip
title="Add this spawn to quick access"
content="Enter a name and click on the button to later be able to quickly respawn a unit with the same configuration."
/>
)}
tooltipRelativeToParent={true}
checked={key in props.starredSpawns}
icon={faStar}
></OlStateButton>
</div>
<div
className={`
inline-flex w-full flex-row content-center
justify-between gap-2
`}
>
{!props.coalition && (
<>
<div
className={`
my-auto mr-2 text-white
`}
>
Coalition:
</div>
<OlCoalitionToggle
coalition={spawnCoalition}
onClick={() => {
spawnCoalition === "blue" && setSpawnCoalition("neutral");
spawnCoalition === "neutral" && setSpawnCoalition("red");
spawnCoalition === "red" && setSpawnCoalition("blue");
}}
tooltip={() => (
<OlExpandingTooltip
title="Unit coalition"
content="Toggle between blue, neutral and red coalitions. Neutral coalition must be used to employ scenic functions like miss on purpose."
/>
)}
tooltipRelativeToParent={true}
/>
</>
)}
<div className="my-auto ml-auto text-white">Units: </div>
<OlNumberInput
className={"ml-2"}
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))));
}}
tooltip={() => (
<OlExpandingTooltip
title="Select number of units"
content="This is how many units of this type will be spawned. If more than one unit is spawned, a DCS group will be created: this means that the units will be spawned in a formation, and you will not be able to control them singularly. The entire group will act as a single entity."
/>
)}
tooltipRelativeToParent={true}
/>
</div>
{["aircraft", "helicopter"].includes(props.blueprint?.category ?? "aircraft") && (
<>
{!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-gray-400
`}
>
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)}
tooltip={() => (
<OlExpandingTooltip
title="Select altitude type"
content="If AGL is selected, the aircraft will be spawned at the selected altitude above the ground. If ASL is selected, the aircraft will be spawned at the selected altitude above sea level."
/>
)}
tooltipRelativeToParent={true}
/>
</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-gray-400
`}
>
Role
</span>
<OlDropdown
label={spawnRole}
className="w-64"
tooltip={() => (
<OlExpandingTooltip
title="Role of the spawned unit"
content="This selection has no effect on what the spawned unit will actually perform, and you will have total control on it. However, it is used to filter what loadouts are available."
/>
)}
tooltipRelativeToParent={true}
>
{roles.map((role) => {
return (
<OlDropdownItem
key={role}
onClick={() => {
setSpawnRole(role);
}}
className={`w-full`}
>
{role}
</OlDropdownItem>
);
})}
</OlDropdown>
</div>
</>
)}
<div
className={`
flex content-center justify-between gap-2
`}
>
<span
className={`
my-auto font-normal
dark:text-gray-400
`}
>
Livery
</span>
<OlDropdown
label={props.blueprint?.liveries ? (props.blueprint?.liveries[spawnLiveryID]?.name ?? "Default") : "No livery"}
className={`w-64`}
tooltip={() => (
<OlExpandingTooltip
title="Unit livery"
content="Selects the livery of the spawned unit. This is a purely cosmetic option."
/>
)}
tooltipRelativeToParent={true}
>
{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
key={id}
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-gray-400
`}
>
Skill
</span>
<OlDropdown
label={spawnSkill}
className={`w-64`}
tooltip={() => (
<OlExpandingTooltip
title="Unit skill"
content="Selects the skill of the spawned unit. Depending on the selection, the unit will be more precise and effective at its mission. Usually a lower level is selected to generate a more forgiving mission."
/>
)}
tooltipRelativeToParent={true}
>
{["Average", "Good", "High", "Excellent"].map((skill) => {
return (
<OlDropdownItem
key={skill}
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
className={`
flex flex-col content-center
justify-between gap-2
`}
>
<span
className={`
my-auto font-normal
dark:text-gray-400
`}
>
Loadout
</span>
<OlDropdown
label={spawnLoadout ? spawnLoadout.name : "Default"}
className={`w-full`}
tooltip={() => (
<OlExpandingTooltip
title="Unit loadout"
content="Selects the loadout of the spawned unit. Depending on the selection, the unit will be equipped with different weapons and equipment."
/>
)}
tooltipRelativeToParent={true}
>
{filteredLoadouts?.map((loadout) => {
return (
<OlDropdownItem
key={loadout.name}
onClick={() => {
setSpawnLoadout(loadout);
}}
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">{loadout.name}</div>
</span>
</OlDropdownItem>
);
})}
</OlDropdown>
</div>
<div>
<button
type="button"
className={`
flex w-full justify-center gap-2
rounded-md border-[1px] p-2
align-middle text-sm
dark:text-white
hover:bg-white/10
`}
onClick={() => {
SetLoadoutWizardBlueprintEvent.dispatch(props.blueprint!);
getApp().setState(OlympusState.SPAWN, SpawnSubState.LOADOUT_WIZARD);
}}
>
<FaMagic className="my-auto" />
Loadout wizard
</button>
</div>
<div className="my-5 flex justify-between">
<div className="my-auto flex flex-col gap-2">
<span className="text-white">Spawn heading</span>
<div
className={`
flex gap-1 text-sm text-gray-400
`}
>
<FaQuestionCircle
className={`
my-auto
`}
/>{" "}
<div className={`my-auto`}>Drag to change</div>
</div>
</div>
<OlNumberInput
className={"my-auto"}
min={0}
max={360}
onChange={(ev) => {
setCompassAngle(Number(ev.target.value));
}}
onDecrease={() => {
setCompassAngle(normalizeAngle(compassAngle - 1));
}}
onIncrease={() => {
setCompassAngle(normalizeAngle(compassAngle + 1));
}}
value={compassAngle}
tooltip={() => (
<OlExpandingTooltip
title="Spawn heading"
content="This controls the direction the unit will face when spanwned. This is important for units that take longer to change direction, like ships. Air units and helicopters will enter a orbit with its major axis aligned with the spawn heading. Drag the compass to change the heading."
/>
)}
tooltipRelativeToParent={true}
tooltipPosition="above"
/>
<div
className={`
relative mr-3 h-[60px] w-[60px]
`}
>
<img
className="absolute"
ref={compassRef}
onMouseDown={handleMouseDown}
src={"/images/others/arrow_background.png"}
></img>
<img
className="absolute left-0"
ref={compassRef}
onMouseDown={handleMouseDown}
src={"/images/others/arrow.png"}
style={{
width: "60px",
height: "60px",
transform: `rotate(${compassAngle}deg)`,
cursor: "pointer",
}}
></img>
</div>
</div>
<div className="text-gray-200">Loadout</div>
{spawnLoadout && <LoadoutViewer spawnLoadout={spawnLoadout} />}
</div>
{props.airbase && (
<button
type="button"
className={`
m-2 rounded-lg bg-blue-700 px-5 py-2.5
text-sm font-medium text-white
dark:bg-blue-600 dark:hover:bg-blue-700
dark:focus:ring-blue-800
focus:outline-none focus:ring-4
focus:ring-blue-300
hover:bg-blue-800
`}
onClick={() => {
if (spawnRequestTable) {
getApp()
.getUnitsManager()
.spawnUnits(
spawnRequestTable.category,
Array(spawnRequestTable.amount).fill(spawnRequestTable.unit),
spawnRequestTable.coalition,
false,
props.airbase?.getName()
);
}
}}
>
Spawn
</button>
)}
</div>
)}
</>
)}
</>
);
}