import React, { MutableRefObject, useCallback, useEffect, useRef, useState } from "react"; import { Menu } from "./components/menu"; import { Unit } from "../../unit/unit"; import { OlLabelToggle } from "../components/ollabeltoggle"; import { OlRangeSlider } from "../components/olrangeslider"; import { getApp } from "../../olympusapp"; import { OlButtonGroup, OlButtonGroupItem } from "../components/olbuttongroup"; import { OlCheckbox } from "../components/olcheckbox"; import { ROEs, alarmStates, UnitState, altitudeIncrements, emissionsCountermeasures, maxAltitudeValues, maxSpeedValues, reactionsToThreat, speedIncrements, } from "../../constants/constants"; import { OlToggle } from "../components/oltoggle"; import { OlCoalitionToggle } from "../components/olcoalitiontoggle"; import { olButtonsEmissionsAttack, olButtonsEmissionsDefend, olButtonsEmissionsFree, olButtonsEmissionsSilent, olButtonsIntensity1, olButtonsIntensity2, olButtonsIntensity3, olButtonsRoeDesignated, olButtonsRoeFree, olButtonsRoeHold, olButtonsRoeReturn, olButtonsScatter1, olButtonsScatter2, olButtonsScatter3, olButtonsThreatEvade, olButtonsThreatManoeuvre, olButtonsThreatNone, olButtonsThreatPassive, olButtonsVisibilityAircraft, olButtonsVisibilityDcs, olButtonsVisibilityGroundunit, olButtonsVisibilityGroundunitSam, olButtonsVisibilityHelicopter, olButtonsVisibilityHuman, olButtonsVisibilityNavyunit, olButtonsVisibilityOlympus, } from "../components/olicons"; import { Coalition } from "../../types/types"; import { convertROE, deepCopyTable, ftToM, knotsToMs, mToFt, msToKnots, zeroAppend } from "../../other/utils"; import { FaChevronLeft, FaCog, FaExclamationCircle, FaGasPump, FaQuestionCircle, FaSignal, FaTag } from "react-icons/fa"; import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; import { OlSearchBar } from "../components/olsearchbar"; import { OlDropdown, OlDropdownItem } from "../components/oldropdown"; import { FaRadio, FaVolumeHigh } from "react-icons/fa6"; import { OlNumberInput } from "../components/olnumberinput"; import { AlarmState, GeneralSettings, Radio, TACAN } from "../../interfaces"; import { OlStringInput } from "../components/olstringinput"; import { OlFrequencyInput } from "../components/olfrequencyinput"; import { UnitSink } from "../../audio/unitsink"; import { AudioManagerStateChangedEvent, SelectedUnitsChangedEvent, SelectionClearedEvent, UnitsUpdatedEvent } from "../../events"; import { faCog, IconDefinition } from "@fortawesome/free-solid-svg-icons"; import { OlExpandingTooltip } from "../components/olexpandingtooltip"; import { OlLocation } from "../components/ollocation"; import { OlStateButton } from "../components/olstatebutton"; export function UnitControlMenu(props: { open: boolean; onClose: () => void }) { function initializeUnitsData() { return { desiredAltitude: undefined as undefined | number, desiredAltitudeType: undefined as undefined | string, desiredSpeed: undefined as undefined | number, desiredSpeedType: undefined as undefined | string, ROE: undefined as undefined | string, reactionToThreat: undefined as undefined | string, emissionsCountermeasures: undefined as undefined | string, scenicAAA: undefined as undefined | boolean, missOnPurpose: undefined as undefined | boolean, shotsScatter: undefined as undefined | number, shotsIntensity: undefined as undefined | number, operateAs: undefined as undefined | Coalition, followRoads: undefined as undefined | boolean, isActiveAWACS: undefined as undefined | boolean, isActiveTanker: undefined as undefined | boolean, onOff: undefined as undefined | boolean, isAudioSink: undefined as undefined | boolean, radio: undefined as undefined | Radio, TACAN: undefined as undefined | TACAN, generalSettings: undefined as undefined | GeneralSettings, alarmState: undefined as undefined | AlarmState }; } const [selectedUnits, setSelectedUnits] = useState([] as Unit[]); const [audioManagerState, setAudioManagerState] = useState(false); const [selectedUnitsData, setSelectedUnitsData] = useState(initializeUnitsData); const [forcedUnitsData, setForcedUnitsData] = useState(initializeUnitsData); const [selectionFilter, setSelectionFilter] = useState({ control: { human: true, dcs: true, olympus: true, }, blue: { aircraft: true, helicopter: true, "groundunit-sam": true, groundunit: true, navyunit: true, }, neutral: { aircraft: true, helicopter: true, "groundunit-sam": true, groundunit: true, navyunit: true, }, red: { aircraft: true, helicopter: true, "groundunit-sam": true, groundunit: true, navyunit: true, }, }); const [selectionID, setSelectionID] = useState(null as null | number); const [searchBarRefState, setSearchBarRefState] = useState(null as MutableRefObject | null); const [filterString, setFilterString] = useState(""); const [showRadioSettings, setShowRadioSettings] = useState(false); const [showAdvancedSettings, setShowAdvancedSettings] = useState(false); const [activeRadioSettings, setActiveRadioSettings] = useState(null as null | { radio: Radio; TACAN: TACAN }); const [activeAdvancedSettings, setActiveAdvancedSettings] = useState(null as null | GeneralSettings); const [lastUpdateTime, setLastUpdateTime] = useState(0); const [showScenicModes, setShowScenicModes] = useState(false); const [showEngagementSettings, setShowEngagementSettings] = useState(false); const [barrelHeight, setBarrelHeight] = useState(0); const [muzzleVelocity, setMuzzleVelocity] = useState(0); const [aimTime, setAimTime] = useState(0); const [shotsToFire, setShotsToFire] = useState(0); const [shotsBaseInterval, setShotsBaseInterval] = useState(0); const [shotsBaseScatter, setShotsBaseScatter] = useState(0); const [engagementRange, setEngagementRange] = useState(0); const [targetingRange, setTargetingRange] = useState(0); const [aimMethodRange, setAimMethodRange] = useState(0); const [acquisitionRange, setAcquisitionRange] = useState(0); var searchBarRef = useRef(null); useEffect(() => { SelectedUnitsChangedEvent.on((units) => setSelectedUnits(units)); SelectionClearedEvent.on(() => setSelectedUnits([])); AudioManagerStateChangedEvent.on((state) => setAudioManagerState(state)); UnitsUpdatedEvent.on((units) => units.find((unit) => unit.getSelected()) && setLastUpdateTime(Date.now())); }, []); useEffect(() => { if (!searchBarRefState) setSearchBarRefState(searchBarRef); if (!props.open && selectionID !== null) setSelectionID(null); if (!props.open && filterString !== "") setFilterString(""); }); const updateData = useCallback(() => { const getters = { desiredAltitude: (unit: Unit) => Math.round(mToFt(unit.getDesiredAltitude())), desiredAltitudeType: (unit: Unit) => unit.getDesiredAltitudeType(), desiredSpeed: (unit: Unit) => Math.round(msToKnots(unit.getDesiredSpeed())), desiredSpeedType: (unit: Unit) => unit.getDesiredSpeedType(), ROE: (unit: Unit) => unit.getROE(), reactionToThreat: (unit: Unit) => unit.getReactionToThreat(), emissionsCountermeasures: (unit: Unit) => unit.getEmissionsCountermeasures(), scenicAAA: (unit: Unit) => unit.getState() === "scenic-aaa", missOnPurpose: (unit: Unit) => unit.getState() === "miss-on-purpose", shotsScatter: (unit: Unit) => unit.getShotsScatter(), shotsIntensity: (unit: Unit) => unit.getShotsIntensity(), operateAs: (unit: Unit) => unit.getOperateAs(), followRoads: (unit: Unit) => unit.getFollowRoads(), isActiveAWACS: (unit: Unit) => unit.getIsActiveAWACS(), isActiveTanker: (unit: Unit) => unit.getIsActiveTanker(), onOff: (unit: Unit) => unit.getOnOff(), radio: (unit: Unit) => unit.getRadio(), TACAN: (unit: Unit) => unit.getTACAN(), alarmState: (unit: Unit) => unit.getAlarmState(), generalSettings: (unit: Unit) => unit.getGeneralSettings(), isAudioSink: (unit: Unit) => { return ( getApp() ?.getAudioManager() .getSinks() .filter((sink) => { return sink instanceof UnitSink; }).length > 0 && getApp() ?.getAudioManager() .getSinks() .find((sink) => { return sink instanceof UnitSink && sink.getUnit() === unit; }) !== undefined ); }, } as { [key in keyof typeof selectedUnitsData]: (unit: Unit) => void }; var updatedData = {}; let anyForcedDataUpdated = false; Object.entries(getters).forEach(([key, getter]) => { let newDatum = getApp()?.getUnitsManager()?.getSelectedUnitsVariable(getter); if (forcedUnitsData[key] !== undefined) { if (newDatum === updatedData[key]) { anyForcedDataUpdated = true; forcedUnitsData[key] === undefined; } updatedData[key] = forcedUnitsData[key]; } else updatedData[key] = newDatum; }); setSelectedUnitsData(updatedData as typeof selectedUnitsData); if (anyForcedDataUpdated) setForcedUnitsData({ ...forcedUnitsData }); }, [forcedUnitsData]); useEffect(updateData, [selectedUnits, lastUpdateTime, forcedUnitsData]); useEffect(() => { setForcedUnitsData(initializeUnitsData); setShowRadioSettings(false); setShowAdvancedSettings(false); if (selectedUnits.length > 0) { setBarrelHeight(selectedUnits[0].getBarrelHeight()); setMuzzleVelocity(selectedUnits[0].getMuzzleVelocity()); setAimTime(selectedUnits[0].getAimTime()); setShotsToFire(selectedUnits[0].getShotsToFire()); setShotsBaseInterval(selectedUnits[0].getShotsBaseInterval()); setShotsBaseScatter(selectedUnits[0].getShotsBaseScatter()); setEngagementRange(selectedUnits[0].getEngagementRange()); setTargetingRange(selectedUnits[0].getTargetingRange()); setAimMethodRange(selectedUnits[0].getAimMethodRange()); setAcquisitionRange(selectedUnits[0].getAcquisitionRange()); } }, [selectedUnits]); /* Count how many units are selected of each type, divided by coalition */ var unitOccurences: { blue: { [key: string]: { label: string; occurences: number } }; red: { [key: string]: { label: string; occurences: number } }; neutral: { [key: string]: { label: string; occurences: number } }; } = { blue: {}, red: {}, neutral: {}, }; selectedUnits.forEach((unit) => { if (!(unit.getName() in unitOccurences[unit.getCoalition()])) unitOccurences[unit.getCoalition()][unit.getName()] = { occurences: 1, label: unit.getBlueprint()?.label }; else unitOccurences[unit.getCoalition()][unit.getName()].occurences++; }); const selectedCategories = getApp()?.getUnitsManager()?.getSelectedUnitsCategories() ?? []; const filteredUnits = Object.values(getApp()?.getUnitsManager()?.getUnits() ?? {}).filter( (unit) => unit.getUnitName().toLowerCase().indexOf(filterString.toLowerCase()) >= 0 || (unit.getBlueprint()?.label ?? "").toLowerCase()?.indexOf(filterString.toLowerCase()) >= 0 ); const everyUnitIsGround = selectedCategories.every((category) => { return category === "GroundUnit"; }); const everyUnitIsNavy = selectedCategories.every((category) => { return category === "NavyUnit"; }); const everyUnitIsHelicopter = selectedCategories.every((category) => { return category === "Helicopter"; }); /* Speed/altitude increments */ const minAltitude = 0; const minSpeed = 0; let maxAltitude = maxAltitudeValues.aircraft; let maxSpeed = maxSpeedValues.aircraft; let speedStep = speedIncrements.aircraft; let altitudeStep = altitudeIncrements.aircraft; if (everyUnitIsHelicopter) { maxAltitude = maxAltitudeValues.helicopter; maxSpeed = maxSpeedValues.helicopter; speedStep = speedIncrements.helicopter; altitudeStep = altitudeIncrements.helicopter; } else if (everyUnitIsGround) { maxSpeed = maxSpeedValues.groundunit; speedStep = speedIncrements.groundunit; } else if (everyUnitIsNavy) { maxSpeed = maxSpeedValues.navyunit; speedStep = speedIncrements.navyunit; } return ( 0 ? `Units selected (x${selectedUnits.length})` : `No units selected`} onClose={props.onClose} autohide={true} wiki={() => { return (

Unit selection tool

The unit control menu serves two purposes. If no unit is currently selected, it allows you to select units based on their category, coalition, and control mode. You can also select units based on their specific type by using the search input.

Unit control tool

If units are selected, the menu will display the selected units and allow you to control their altitude, speed, rules of engagement, and other settings.
The available controls depend on what type of unit is selected. Only controls applicable to every selected unit will be displayed, so make sure to refine your selection.{" "}
{" "} You will be able to inspect the current values of the controls, e.g. the desired altitude, rules of engagement and so on. However, if multiple units are selected, you will only see the values of controls that are set to be the same for each selected unit.
{" "} For example, if two airplanes are selected and they both have been instructed to fly at 1000ft, you will see the altitude slider set at that value. But if one airplane is set to fly at 1000ft and the other at 2000ft, you will see the slider display 'Different values'.
If at that point you move the slider, you will instruct both airplanes to fly at the same altitude.
{" "} If a single unit is selected, you will also be able to see additional info on the unit, like its fuel level, position and altitude, tasking, and available ammunition.{" "}
); }} > <> {/* ============== Selection tool START ============== */} {selectedUnits.length == 0 && (
Selection tool
The selection tools allows you to select units depending on their category, coalition, and control mode. You can also select units depending on their specific type by using the search input.
{selectionID === null && ( <>
Control mode
{Object.entries({ human: ["Human", olButtonsVisibilityHuman], olympus: ["Olympus controlled", olButtonsVisibilityOlympus], dcs: ["From DCS mission", olButtonsVisibilityDcs], }).map((entry, idx) => { return (
{entry[1][0] as string} { selectionFilter["control"][entry[0]] = !selectionFilter["control"][entry[0]]; setSelectionFilter(deepCopyTable(selectionFilter)); }} toggled={selectionFilter["control"][entry[0]]} />
); })}
Types and coalitions
)} {selectionID === null && ( )} {selectionID === null && Object.entries({ aircraft: [olButtonsVisibilityAircraft, "Aircrafts"], helicopter: [olButtonsVisibilityHelicopter, "Helicopters"], "groundunit-sam": [olButtonsVisibilityGroundunitSam, "SAMs"], groundunit: [olButtonsVisibilityGroundunit, "Ground units"], navyunit: [olButtonsVisibilityNavyunit, "Navy units"], }).map((entry, idx) => { return ( {["blue", "neutral", "red"].map((coalition) => { return ( ); })} ); })} {selectionID === null && ( )}
BLUE NEUTRAL RED
{" "}
{entry[1][1] as string}
{ selectionFilter[coalition][entry[0]] = !selectionFilter[coalition][entry[0]]; setSelectionFilter(deepCopyTable(selectionFilter)); }} />
value)} onChange={() => { const newValue = !Object.values(selectionFilter["blue"]).some((value) => value); Object.keys(selectionFilter["blue"]).forEach((key) => { selectionFilter["blue"][key] = newValue; }); setSelectionFilter(deepCopyTable(selectionFilter)); }} /> value)} onChange={() => { const newValue = !Object.values(selectionFilter["neutral"]).some((value) => value); Object.keys(selectionFilter["neutral"]).forEach((key) => { selectionFilter["neutral"][key] = newValue; }); setSelectionFilter(deepCopyTable(selectionFilter)); }} /> value)} onChange={() => { const newValue = !Object.values(selectionFilter["red"]).some((value) => value); Object.keys(selectionFilter["red"]).forEach((key) => { selectionFilter["red"][key] = newValue; }); setSelectionFilter(deepCopyTable(selectionFilter)); }} />
{ setFilterString(value); selectionID && setSelectionID(null); }} text={selectionID ? (getApp().getUnitsManager().getUnitByID(selectionID)?.getUnitName() ?? "") : filterString} />
{filterString !== "" && filteredUnits.length > 0 && filteredUnits.map((unit) => { return ( { setSelectionID(unit.ID); }} >
{ unit.setHighlighted(true); }} onMouseLeave={() => { unit.setHighlighted(false); }} > {unit.getUnitName()} ({unit.getBlueprint()?.label ?? ""})
); })} {filteredUnits.length == 0 && No results}
)} {/* ============== Selection tool END ============== */} {/* */} {/* */} {/* */} {/* */} {/* */} {/* */} <> {/* ============== Unit control menu START ============== */} {selectedUnits.length > 0 && ( <> {/* ============== Units list START ============== */}
{ <> {["blue", "red", "neutral"].map((coalition) => { return Object.keys(unitOccurences[coalition]).map((name, idx) => { return (
{unitOccurences[coalition][name].label} x{unitOccurences[coalition][name].occurences}
); }); })} }
{/* ============== Units list END ============== */} {/* ============== Unit basic options START ============== */} <> {!showRadioSettings && !showAdvancedSettings && (
{/* ============== Altitude selector START ============== */} {selectedCategories.every((category) => { return ["Aircraft", "Helicopter"].includes(category); }) && (
Altitude {selectedUnitsData.desiredAltitude !== undefined ? Intl.NumberFormat("en-US").format(selectedUnitsData.desiredAltitude) + " FT" : "Different values"}
{ getApp() .getUnitsManager() .setAltitudeType(selectedUnitsData.desiredAltitudeType === "ASL" ? "AGL" : "ASL", null, () => setForcedUnitsData({ ...forcedUnitsData, desiredAltitudeType: selectedUnitsData.desiredAltitudeType === "ASL" ? "AGL" : "ASL", }) ); }} tooltip={() => ( )} tooltipRelativeToParent={true} />
{ let value = Number(ev.target.value); getApp() .getUnitsManager() .setAltitude(ftToM(value), null, () => setForcedUnitsData({ ...forcedUnitsData, desiredAltitude: value, }) ); }} value={selectedUnitsData.desiredAltitude} min={minAltitude} max={maxAltitude} step={altitudeStep} />
)} {/* ============== Altitude selector END ============== */} {/* ============== Airspeed selector START ============== */}
Speed {selectedUnitsData.desiredSpeed !== undefined ? selectedUnitsData.desiredSpeed + " KTS" : "Different values"}
{!(everyUnitIsGround || everyUnitIsNavy) && ( { getApp() .getUnitsManager() .setSpeedType(selectedUnitsData.desiredSpeedType === "CAS" ? "GS" : "CAS", null, () => setForcedUnitsData({ ...forcedUnitsData, desiredSpeedType: selectedUnitsData.desiredSpeedType === "CAS" ? "GS" : "CAS", }) ); }} tooltip={() => ( )} tooltipRelativeToParent={true} /> )}
{ let value = Number(ev.target.value); getApp() .getUnitsManager() .setSpeed(knotsToMs(value), null, () => setForcedUnitsData({ ...forcedUnitsData, desiredSpeed: value, }) ); }} value={selectedUnitsData.desiredSpeed} min={minSpeed} max={maxSpeed} step={speedStep} />
{/* ============== Airspeed selector END ============== */} {/* ============== Rules of Engagement START ============== */} {!(selectedUnits.length === 1 && selectedUnits[0].isTanker()) && !(selectedUnits.length === 1 && selectedUnits[0].isAWACS()) && (
Rules of engagement (
Sets the rule of engagement of the unit, in order:
{" "} {" "} Hold fire: The unit will not shoot in any circumstance
{" "} {" "} Return fire: The unit will not fire unless fired upon
{" "} {" "}
{" "} Fire on target: The unit will not fire unless fired upon{" "}

or

{" "} ordered to do so{" "}
{" "} {" "} Free: The unit will fire at any detected enemy in range
Currently, DCS blue and red ground units do not respect{" "} {" "} and{" "} {" "} rules of engagement, so be careful, they may start shooting when you don't want them to. Use neutral units for finer control.
} /> )} tooltipRelativeToParent={true} > {[olButtonsRoeHold, olButtonsRoeReturn, olButtonsRoeDesignated, olButtonsRoeFree].map((icon, idx) => { return ( { getApp() .getUnitsManager() .setROE(ROEs[convertROE(idx)], null, () => setForcedUnitsData({ ...forcedUnitsData, ROE: ROEs[convertROE(idx)], }) ); }} active={selectedUnitsData.ROE === ROEs[convertROE(idx)]} icon={icon} /> ); })}
)} {/* ============== Rules of Engagement END ============== */} {/* ============== Alarm state selector START ============== */} {selectedUnitsData.alarmState &&
Alarm State (
Sets the alarm state of the unit, in order:
{" "} {" "}
{" "} Auto: The unit will use its sensors to engage based on its ROE.
{" "} Green: The unit will not engage with its sensors in any circumstances. The unit will be able to move.
{" "} Red: The unit will be actively searching for target with its sensors. For some units, this will deploy the radar and make the unit not able to move.
} /> )} tooltipRelativeToParent={true} > {[olButtonsRoeHold, olButtonsRoeReturn, olButtonsRoeDesignated].map((icon, idx) => { const getAlarmStateByIdx = (idx) => { switch (idx) { case 0: return AlarmState.AUTO; case 1: return AlarmState.GREEN; case 2: return AlarmState.RED; } } return ( { getApp() .getUnitsManager() .setAlarmState(idx, null, () => setForcedUnitsData({ ...forcedUnitsData, alarmState: getAlarmStateByIdx(idx), }) ); }} active={selectedUnitsData.alarmState === getAlarmStateByIdx(idx)} icon={icon} /> ); })} } {/* ============== Alarm state selector END ============== */} {selectedCategories.every((category) => { return ["Aircraft", "Helicopter"].includes(category); }) && ( <> {/* ============== Threat Reaction START ============== */}
Threat reaction (
Sets the reaction to threat of the unit, in order:
{" "} {" "} No reaction: The unit will not react in any circumstance
{" "} {" "} Passive: The unit will use counter-measures, but will not alter its course
{" "} {" "} Manouevre: The unit will try to evade the threat using manoeuvres, but no counter-measures
{" "} {" "} Full evasion: the unit will try to evade the threat both manoeuvering and using counter-measures
} /> )} tooltipRelativeToParent={true} > {[olButtonsThreatNone, olButtonsThreatPassive, olButtonsThreatManoeuvre, olButtonsThreatEvade].map((icon, idx) => { return ( { getApp() .getUnitsManager() .setReactionToThreat(reactionsToThreat[idx], null, () => setForcedUnitsData({ ...forcedUnitsData, reactionToThreat: reactionsToThreat[idx], }) ); }} active={selectedUnitsData.reactionToThreat === reactionsToThreat[idx]} icon={icon} /> ); })} {/* ============== Threat Reaction END ============== */} {/* ============== Radar and ECM START ============== */}
Radar and ECM (
Sets the units radar and Electronic Counter Measures (jamming) use policy, in order:
{" "} {" "} Radio silence: No radar or ECM will be used
{" "} {" "} Defensive: The unit will turn radar and ECM on only when threatened
{" "} {" "} Attack: The unit will use radar and ECM when engaging other units
{" "} {" "} Free: the unit will use the radar and ECM all the time
} /> )} tooltipRelativeToParent={true} tooltipPosition="above" > {[olButtonsEmissionsSilent, olButtonsEmissionsDefend, olButtonsEmissionsAttack, olButtonsEmissionsFree].map((icon, idx) => { return ( { getApp() .getUnitsManager() .setEmissionsCountermeasures(emissionsCountermeasures[idx], null, () => setForcedUnitsData({ ...forcedUnitsData, emissionsCountermeasures: emissionsCountermeasures[idx], }) ); }} active={selectedUnitsData.emissionsCountermeasures === emissionsCountermeasures[idx]} icon={icon} /> ); })} {/* ============== Radar and ECM END ============== */} )} {/* ============== Tanker and AWACS available button START ============== */} {getApp() ?.getUnitsManager() ?.getSelectedUnitsVariable((unit) => { return unit.isTanker(); }) && (
Make tanker available { if ( selectedUnitsData.isActiveAWACS !== undefined && selectedUnitsData.TACAN !== undefined && selectedUnitsData.radio !== undefined && selectedUnitsData.generalSettings !== undefined ) getApp() .getUnitsManager() .setAdvancedOptions( !selectedUnitsData.isActiveTanker, selectedUnitsData.isActiveAWACS, selectedUnitsData.TACAN, selectedUnitsData.radio, selectedUnitsData.generalSettings, null, () => setForcedUnitsData({ ...forcedUnitsData, isActiveTanker: !selectedUnitsData.isActiveTanker, }) ); }} tooltip={() => ( )} tooltipRelativeToParent={true} />
)} {getApp() ?.getUnitsManager() ?.getSelectedUnitsVariable((unit) => { return unit.isAWACS(); }) && (
Make AWACS available { if ( selectedUnitsData.isActiveTanker !== undefined && selectedUnitsData.TACAN !== undefined && selectedUnitsData.radio !== undefined && selectedUnitsData.generalSettings !== undefined ) getApp() .getUnitsManager() .setAdvancedOptions( selectedUnitsData.isActiveTanker, !selectedUnitsData.isActiveAWACS, selectedUnitsData.TACAN, selectedUnitsData.radio, selectedUnitsData.generalSettings, null, () => setForcedUnitsData({ ...forcedUnitsData, isActiveAWACS: !selectedUnitsData.isActiveAWACS, }) ); }} tooltip={() => ( )} tooltipRelativeToParent={true} />
)} {/* ============== Tanker and AWACS available button END ============== */} {/* ============== Radio settings buttons START ============== */} {selectedUnits.length === 1 && (selectedUnits[0].isTanker() || selectedUnits[0].isAWACS()) && (
)} {/* ============== Radio settings buttons END ============== */} {/* ============== Advanced settings buttons START ============== */} {selectedUnits.length === 1 && !selectedUnits[0].isTanker() && !selectedUnits[0].isAWACS() && ["Aircraft", "Helicopter"].includes(selectedUnits[0].getCategory()) && (
)} {/* ============== Advanced settings buttons END ============== */} {selectedCategories.every((category) => { return ["GroundUnit", "NavyUnit"].includes(category); }) && ( <>
Scenic modes setShowScenicModes(!showScenicModes)} />
{showScenicModes && (
Currently, DCS blue and red ground units do not respect their rules of engagement, so be careful, they may start shooting when you don't want them to. Use neutral units for finer control, then use the "Operate as" toggle to switch their "side".
)}
{showScenicModes && ( <> {/* ============== Scenic AAA toggle START ============== */}
Scenic AAA mode { if (selectedUnitsData.scenicAAA) { getApp() .getUnitsManager() .stop(null, () => setForcedUnitsData({ ...forcedUnitsData, missOnPurpose: false, scenicAAA: false, }) ); } else { getApp() .getUnitsManager() .scenicAAA(null, () => setForcedUnitsData({ ...forcedUnitsData, missOnPurpose: false, scenicAAA: true, }) ); } }} tooltip={() => ( )} tooltipRelativeToParent={true} />
{/* ============== Scenic AAA toggle END ============== */} {/* ============== Miss on purpose toggle START ============== */}
Miss on purpose mode { if (selectedUnitsData.missOnPurpose) { getApp() .getUnitsManager() .stop(null, () => setForcedUnitsData({ ...forcedUnitsData, scenicAAA: false, missOnPurpose: false, }) ); } else { getApp() .getUnitsManager() .missOnPurpose(null, () => setForcedUnitsData({ ...forcedUnitsData, scenicAAA: false, missOnPurpose: true, }) ); } }} tooltip={() => ( )} tooltipRelativeToParent={true} />
{/* ============== Miss on purpose toggle END ============== */}
{/* ============== Shots scatter START ============== */}
Shots scatter {[olButtonsScatter1, olButtonsScatter2, olButtonsScatter3].map((icon, idx) => { return ( { getApp() .getUnitsManager() .setShotsScatter(idx + 1, null, () => setForcedUnitsData({ ...forcedUnitsData, shotsScatter: idx + 1, }) ); }} active={selectedUnitsData.shotsScatter === idx + 1} icon={icon} /> ); })}
{/* ============== Shots scatter END ============== */} {/* ============== Shots intensity START ============== */}
Shots intensity {[olButtonsIntensity1, olButtonsIntensity2, olButtonsIntensity3].map((icon, idx) => { return ( { getApp() .getUnitsManager() .setShotsIntensity(idx + 1, null, () => setForcedUnitsData({ ...forcedUnitsData, shotsIntensity: idx + 1, }) ); }} active={selectedUnitsData.shotsIntensity === idx + 1} icon={icon} /> ); })}
{/* ============== Shots intensity END ============== */} setShowEngagementSettings(!showEngagementSettings)} icon={faCog} >
{/* ============== Operate as toggle START ============== */} {selectedUnits.every((unit) => unit.getCoalition() === "neutral") && (
Operate as { getApp() .getUnitsManager() .setOperateAs(selectedUnitsData.operateAs === "blue" ? "red" : "blue", null, () => setForcedUnitsData({ ...forcedUnitsData, operateAs: selectedUnitsData.operateAs === "blue" ? "red" : "blue", }) ); }} tooltip={() => ( )} tooltipRelativeToParent={true} tooltipPosition="above" />
)} {/* ============== Operate as toggle END ============== */} {showEngagementSettings && (
Barrel height:{" "}
{ setBarrelHeight(Number(ev.target.value)); }} onIncrease={() => { setBarrelHeight(barrelHeight + 0.1); }} onDecrease={() => { setBarrelHeight(barrelHeight - 0.1); }} >
m
Muzzle velocity:{" "}
{ setMuzzleVelocity(Number(ev.target.value)); }} onIncrease={() => { setMuzzleVelocity(muzzleVelocity + 10); }} onDecrease={() => { setMuzzleVelocity(muzzleVelocity - 10); }} >
m/s
Aim time:{" "}
{ setAimTime(Number(ev.target.value)); }} onIncrease={() => { setAimTime(aimTime + 0.1); }} onDecrease={() => { setAimTime(aimTime - 0.1); }} >
s
Shots to fire:{" "}
{ setShotsToFire(Number(ev.target.value)); }} onIncrease={() => { setShotsToFire(shotsToFire + 1); }} onDecrease={() => { setShotsToFire(shotsToFire - 1); }} >
Shots base interval:{" "}
{ setShotsBaseInterval(Number(ev.target.value)); }} onIncrease={() => { setShotsBaseInterval(shotsBaseInterval + 0.1); }} onDecrease={() => { setShotsBaseInterval(shotsBaseInterval - 0.1); }} >
s
Shots base scatter:{" "}
{ setShotsBaseScatter(Number(ev.target.value)); }} onIncrease={() => { setShotsBaseScatter(shotsBaseScatter + 0.1); }} onDecrease={() => { setShotsBaseScatter(shotsBaseScatter - 0.1); }} >
deg
Engagement range:{" "}
{ setEngagementRange(Number(ev.target.value)); }} onIncrease={() => { setEngagementRange(engagementRange + 100); }} onDecrease={() => { setEngagementRange(engagementRange - 100); }} >
m
Targeting range:{" "}
{ setTargetingRange(Number(ev.target.value)); }} onIncrease={() => { setTargetingRange(targetingRange + 100); }} onDecrease={() => { setTargetingRange(targetingRange - 100); }} >
m
Aim method range:{" "}
{ setAimMethodRange(Number(ev.target.value)); }} onIncrease={() => { setAimMethodRange(aimMethodRange + 100); }} onDecrease={() => { setAimMethodRange(aimMethodRange - 100); }} >
m
Acquisition range:{" "}
{ setAcquisitionRange(Number(ev.target.value)); }} onIncrease={() => { setAcquisitionRange(acquisitionRange + 100); }} onDecrease={() => { setAcquisitionRange(acquisitionRange - 100); }} >
m
)} )}
{/* ============== Follow roads toggle START ============== */}
Follow roads { getApp() .getUnitsManager() .setFollowRoads(!selectedUnitsData.followRoads, null, () => setForcedUnitsData({ ...forcedUnitsData, followRoads: !selectedUnitsData.followRoads, }) ); }} tooltip={() => ( )} tooltipRelativeToParent={true} tooltipPosition="above" />
{/* ============== Follow roads toggle END ============== */} {/* ============== Unit active toggle START ============== */}
Unit active { getApp() .getUnitsManager() .setOnOff(!selectedUnitsData.onOff, null, () => setForcedUnitsData({ ...forcedUnitsData, onOff: !selectedUnitsData.onOff, }) ); }} tooltip={() => ( )} tooltipRelativeToParent={true} tooltipPosition="above" />
{/* ============== Unit active toggle END ============== */} )} {/* ============== Audio sink toggle START ============== */}
Loudspeakers {audioManagerState ? ( { selectedUnits.forEach((unit) => { if (!selectedUnitsData.isAudioSink) { getApp()?.getAudioManager().addUnitSink(unit); setForcedUnitsData({ ...forcedUnitsData, isAudioSink: true, }); } else { let sink = getApp() ?.getAudioManager() .getSinks() .find((sink) => { return sink instanceof UnitSink && sink.getUnit() === unit; }); if (sink !== undefined) getApp()?.getAudioManager().removeSink(sink); setForcedUnitsData({ ...forcedUnitsData, isAudioSink: false, }); } }); }} tooltip={() => ( )} tooltipRelativeToParent={true} tooltipPosition="above" /> ) : (
Enable audio with{" "} {" "} first
)}
{/* ============== Audio sink toggle END ============== */} )} {/* ============== Radio settings START ============== */} {showRadioSettings && (
Radio settings
Callsign
<> {selectedUnits[0].isAWACS() && ( <> {["Overlord", "Magic", "Wizard", "Focus", "Darkstar"].map((name, idx) => { return ( { if (activeRadioSettings) activeRadioSettings.radio.callsign = idx + 1; setActiveRadioSettings(deepCopyTable(activeRadioSettings)); }} > {name} ); })} )} <> {selectedUnits[0].isTanker() && ( <> {["Texaco", "Arco", "Shell"].map((name, idx) => { return ( { if (activeRadioSettings) activeRadioSettings.radio.callsign = idx + 1; setActiveRadioSettings(deepCopyTable(activeRadioSettings)); }} > {name} ); })} )}
-
{ if (activeRadioSettings) activeRadioSettings.radio.callsignNumber = Math.max(Math.min(Number(e.target.value), 9), 1); setActiveRadioSettings(deepCopyTable(activeRadioSettings)); }} onDecrease={() => { if (activeRadioSettings) activeRadioSettings.radio.callsignNumber = Math.max(Math.min(Number(activeRadioSettings.radio.callsignNumber - 1), 9), 1); setActiveRadioSettings(deepCopyTable(activeRadioSettings)); }} onIncrease={() => { if (activeRadioSettings) activeRadioSettings.radio.callsignNumber = Math.max(Math.min(Number(activeRadioSettings.radio.callsignNumber + 1), 9), 1); setActiveRadioSettings(deepCopyTable(activeRadioSettings)); }} value={activeRadioSettings ? activeRadioSettings.radio.callsignNumber : 1} >
TACAN
{ if (activeRadioSettings) activeRadioSettings.TACAN.channel = Math.max(Math.min(Number(e.target.value), 126), 1); setActiveRadioSettings(deepCopyTable(activeRadioSettings)); }} onDecrease={() => { if (activeRadioSettings) activeRadioSettings.TACAN.channel = Math.max(Math.min(Number(activeRadioSettings.TACAN.channel - 1), 126), 1); setActiveRadioSettings(deepCopyTable(activeRadioSettings)); }} onIncrease={() => { if (activeRadioSettings) activeRadioSettings.TACAN.channel = Math.max(Math.min(Number(activeRadioSettings.TACAN.channel + 1), 126), 1); setActiveRadioSettings(deepCopyTable(activeRadioSettings)); }} value={activeRadioSettings ? activeRadioSettings.TACAN.channel : 1} > { if (activeRadioSettings) activeRadioSettings.TACAN.XY = "X"; setActiveRadioSettings(deepCopyTable(activeRadioSettings)); }} > X { if (activeRadioSettings) activeRadioSettings.TACAN.XY = "Y"; setActiveRadioSettings(deepCopyTable(activeRadioSettings)); }} > Y { if (activeRadioSettings) { activeRadioSettings.TACAN.callsign = e.target.value; if (activeRadioSettings.TACAN.callsign.length > 3) activeRadioSettings.TACAN.callsign = activeRadioSettings.TACAN.callsign.slice(0, 3); } setActiveRadioSettings(deepCopyTable(activeRadioSettings)); }} />
Enable TACAN{" "} { if (activeRadioSettings) activeRadioSettings.TACAN.isOn = !activeRadioSettings.TACAN.isOn; setActiveRadioSettings(deepCopyTable(activeRadioSettings)); }} />
Radio frequency
{ if (activeRadioSettings) { activeRadioSettings.radio.frequency = value; setActiveRadioSettings(deepCopyTable(activeRadioSettings)); } }} />
)} {/* ============== Radio settings END ============== */} {/* ============== Advanced settings START ============== */} {showAdvancedSettings && (
Radio settings
Prohibit AA { setActiveAdvancedSettings({ ...(activeAdvancedSettings as GeneralSettings), prohibitAA: !activeAdvancedSettings?.prohibitAA }); }} toggled={activeAdvancedSettings?.prohibitAA} />
Prohibit AG { setActiveAdvancedSettings({ ...(activeAdvancedSettings as GeneralSettings), prohibitAG: !activeAdvancedSettings?.prohibitAG }); }} toggled={activeAdvancedSettings?.prohibitAG} />
Prohibit Jettison { setActiveAdvancedSettings({ ...(activeAdvancedSettings as GeneralSettings), prohibitJettison: !activeAdvancedSettings?.prohibitJettison, }); }} toggled={activeAdvancedSettings?.prohibitJettison} />
Prohibit afterburner { setActiveAdvancedSettings({ ...(activeAdvancedSettings as GeneralSettings), prohibitAfterburner: !activeAdvancedSettings?.prohibitAfterburner, }); }} toggled={activeAdvancedSettings?.prohibitAfterburner} />
)} {/* ============== Advanced settings END ============== */} {/* ============== Unit basic options END ============== */} <> {/* ============== Fuel/payload/radio section START ============== */} {selectedUnits.length === 1 && (
{selectedUnits[0].getUnitName()}
40 && `bg-green-700`} ${ selectedUnits[0].getFuel() > 10 && selectedUnits[0].getFuel() <= 40 && `bg-yellow-700` } ${selectedUnits[0].getFuel() <= 10 && `bg-red-700`} px-2 py-1 text-sm font-bold text-white `} > {selectedUnits[0].getFuel()}%
{selectedUnits[0].getTask()}
{([UnitState.SIMULATE_FIRE_FIGHT, UnitState.MISS_ON_PURPOSE, UnitState.SCENIC_AAA] as string[]).includes(selectedUnits[0].getState()) && (
Time to next tasking: {zeroAppend(selectedUnits[0].getTimeToNextTasking(), 0, true, 2)}s
)}
{Math.round(mToFt(selectedUnits[0].getPosition().alt ?? 0))} ft
{selectedUnits[0].isControlledByOlympus() && (selectedUnits[0].isTanker() || selectedUnits[0].isAWACS()) && ( <> {/* ============== Radio section START ============== */}
{`${selectedUnits[0].isTanker() ? ["Texaco", "Arco", "Shell"][selectedUnits[0].getRadio().callsign - 1] : ["Overlord", "Magic", "Wizard", "Focus", "Darkstar"][selectedUnits[0].getRadio().callsign - 1]}-${selectedUnits[0].getRadio().callsignNumber}`}
{`${(selectedUnits[0].getRadio().frequency / 1000000).toFixed(3)} MHz`}
{selectedUnits[0].getTACAN().isOn ? `${selectedUnits[0].getTACAN().channel}${selectedUnits[0].getTACAN().XY} ${selectedUnits[0].getTACAN().callsign}` : "TACAN OFF"}
{/* ============== Radio section END ============== */}
)} {/* ============== Payload section START ============== */} {!selectedUnits[0].isTanker() && !selectedUnits[0].isAWACS() && selectedUnits[0].getAmmo().map((ammo, idx) => { return (
{ammo.quantity}
{ammo.name}
); })} {/* ============== Payload section END ============== */}
)} {/* ============== Fuel/payload/radio section END ============== */} )} {/* ============== Unit control menu END ============== */}
); }