Started adding AWACS panel

This commit is contained in:
Davide Passoni 2024-11-23 17:18:16 +01:00
parent 897afb2fef
commit 1791eaa37d
23 changed files with 344 additions and 69 deletions

View File

@ -12,6 +12,7 @@ import {
faPlaneArrival,
faRoute,
faTrash,
faWifi,
faXmarksLines,
} from "@fortawesome/free-solid-svg-icons";
import { Unit } from "../unit/unit";
@ -260,7 +261,14 @@ export const mapBounds = {
};
export const defaultMapMirrors = {};
export const defaultMapLayers = {};
export const defaultMapLayers = {
"AWACS": {
"urlTemplate": 'https://abcd.basemaps.cartocdn.com/dark_nolabels/{z}/{x}/{y}{r}.png',
"minZoom": 1,
"maxZoom": 19,
"attribution": `&copy; <a href="https://www.openstreetmap.org/copyright">OpenStreetMap</a> contributors &copy; <a href="https://carto.com/attributions">CARTO</a>'`
},
};
export enum OlympusState {
NOT_INITIALIZED = "Not initialized",
@ -272,6 +280,7 @@ export enum OlympusState {
SPAWN_CONTEXT = "Spawn context",
DRAW = "Draw",
JTAC = "JTAC",
AWACS = "AWACS",
OPTIONS = "Options",
AUDIO = "Audio",
AIRBASE = "Airbase",
@ -340,6 +349,9 @@ export const MAP_OPTIONS_DEFAULTS: MapOptions = {
cameraPluginEnabled: false,
cameraPluginMode: "map",
tabletMode: false,
showUnitBullseyes: false,
showUnitBRAA: false,
AWACSMode: false
};
export const MAP_HIDDEN_TYPES_DEFAULTS = {
@ -459,7 +471,6 @@ export namespace ContextActions {
getApp().getUnitsManager().stop(units);
},
{
executeImmediately: true,
type: ContextActionType.MOVE,
code: "Space",
}
@ -506,7 +517,6 @@ export namespace ContextActions {
getApp().getUnitsManager().delete(false);
},
{
executeImmediately: true,
type: ContextActionType.DELETE,
code: "Delete",
ctrlKey: false,
@ -526,7 +536,6 @@ export namespace ContextActions {
UnitExplosionRequestEvent.dispatch(units);
},
{
executeImmediately: true,
type: ContextActionType.DELETE,
code: "Delete",
ctrlKey: false,
@ -544,7 +553,7 @@ export namespace ContextActions {
(units: Unit[]) => {
getApp().getMap().centerOnUnit(units[0]);
},
{ executeImmediately: true, type: ContextActionType.OTHER, code: "KeyM", ctrlKey: false, shiftKey: false, altKey: false }
{ type: ContextActionType.OTHER, code: "KeyM", ctrlKey: false, shiftKey: false, altKey: false }
);
export const REFUEL = new ContextAction(
@ -556,7 +565,7 @@ export namespace ContextActions {
(units: Unit[]) => {
getApp().getUnitsManager().refuel(units);
},
{ executeImmediately: true, type: ContextActionType.ADMIN, code: "KeyR", ctrlKey: false, shiftKey: false, altKey: false }
{ type: ContextActionType.ADMIN, code: "KeyR", ctrlKey: false, shiftKey: false, altKey: false }
);
export const FOLLOW = new ContextAction(
@ -643,7 +652,7 @@ export namespace ContextActions {
(units: Unit[], _1, _2) => {
getApp().getUnitsManager().createGroup(units);
},
{ executeImmediately: true, type: ContextActionType.OTHER, code: "KeyG", ctrlKey: false, shiftKey: false, altKey: false }
{ type: ContextActionType.OTHER, code: "KeyG", ctrlKey: false, shiftKey: false, altKey: false }
);
export const ATTACK = new ContextAction(
@ -687,4 +696,16 @@ export namespace ContextActions {
},
{ type: ContextActionType.ADMIN, code: "KeyX", ctrlKey: false, shiftKey: false }
);
export const SET_AWACS_REFERENCE = new ContextAction(
"set-awacs-reference",
"Set AWACS reference",
"Set unit as AWACS reference",
faWifi,
ContextActionTarget.NONE,
(units: Unit[], _1, _2) => {
getApp().getUnitsManager().setAWACSReference(units[0].ID);
},
{ type: ContextActionType.ADMIN, code: "KeyU", ctrlKey: false, shiftKey: false, altKey: false }
);
}

View File

@ -151,6 +151,19 @@ export class ModalEvent {
}
/************** Map events ***************/
export class MouseMovedEvent {
static on(callback: (latlng: LatLng, elevation: number) => void) {
document.addEventListener(this.name, (ev: CustomEventInit) => {
callback(ev.detail.latlng, ev.detail.elevation);
});
}
static dispatch(latlng: LatLng, elevation?: number) {
document.dispatchEvent(new CustomEvent(this.name, { detail: { latlng, elevation } }));
// Logging disabled since periodic
}
}
export class HiddenTypesChangedEvent {
static on(callback: (hiddenTypes: MapHiddenTypes) => void) {
document.addEventListener(this.name, (ev: CustomEventInit) => {
@ -346,13 +359,13 @@ export class SpawnContextMenuRequestEvent {
}
export class HotgroupsChangedEvent {
static on(callback: (hotgroups: { [key: number]: number }) => void) {
static on(callback: (hotgroups: { [key: number]: Unit[] }) => void) {
document.addEventListener(this.name, (ev: CustomEventInit) => {
callback(ev.detail.hotgroups);
});
}
static dispatch(hotgroups: { [key: number]: number }) {
static dispatch(hotgroups: { [key: number]: Unit[] }) {
document.dispatchEvent(new CustomEvent(this.name, { detail: { hotgroups } }));
console.log(`Event ${this.name} dispatched`);
}
@ -371,15 +384,15 @@ export class StarredSpawnsChangedEvent {
}
}
export class MouseMovedEvent {
static on(callback: (latlng: LatLng, elevation: number) => void) {
export class AWACSReferenceChangedEvent {
static on(callback: (unit: Unit | null) => void) {
document.addEventListener(this.name, (ev: CustomEventInit) => {
callback(ev.detail.latlng, ev.detail.elevation);
callback(ev.detail);
});
}
static dispatch(latlng: LatLng, elevation?: number) {
document.dispatchEvent(new CustomEvent(this.name, { detail: { latlng, elevation } }));
static dispatch(unit: Unit | null) {
document.dispatchEvent(new CustomEvent(this.name, { detail: unit }));
// Logging disabled since periodic
}
}
@ -461,7 +474,7 @@ export class BullseyesDataChanged {
});
}
static dispatch(bullseyes: { [name: string]: Bullseye } ) {
static dispatch(bullseyes: { [name: string]: Bullseye }) {
document.dispatchEvent(new CustomEvent(this.name, { detail: { bullseyes } }));
// Logging disabled since periodic
}

View File

@ -130,6 +130,8 @@ export interface ObjectIconOptions {
showSummary: boolean;
showCallsign: boolean;
rotateToHeading: boolean;
showBullseyes: boolean;
showBRAA: boolean;
}
export interface GeneralSettings {

View File

@ -207,7 +207,7 @@ export class Map extends L.Map {
/* Custom touch events for touchscreen support */
L.DomEvent.on(this.getContainer(), "touchstart", this.#onMouseDown, this);
L.DomEvent.on(this.getContainer(), "touchend", this.#onMouseUp, this);
L.DomEvent.on(this.getContainer(), 'wheel', this.#onMouseWheel, this);
L.DomEvent.on(this.getContainer(), "wheel", this.#onMouseWheel, this);
/* Event listeners */
AppStateChangedEvent.on((state, subState) => this.#onStateChanged(state, subState));
@ -227,6 +227,8 @@ export class Map extends L.Map {
MapOptionsChangedEvent.on((options: MapOptions) => {
this.getContainer().toggleAttribute("data-hide-labels", !options.showUnitLabels);
this.getContainer().toggleAttribute("data-hide-bullseyes", !options.showUnitBullseyes);
this.getContainer().toggleAttribute("data-hide-BRAA", !options.showUnitBRAA);
this.#cameraControlPort = options.cameraPluginPort;
this.#cameraZoomRatio = 50 / (20 + options.cameraPluginRatio);
this.#slaveDCSCamera = options.cameraPluginEnabled;
@ -400,7 +402,7 @@ export class Map extends L.Map {
const contextActionSet = this.getContextActionSet();
if (this.getContextAction() === null || contextAction !== this.getContextAction()) {
if (getApp().getState() === OlympusState.UNIT_CONTROL && contextActionSet && contextAction.getId() in contextActionSet.getContextActions()) {
if (contextAction.getOptions().executeImmediately) contextAction.executeCallback(null, null);
if (contextAction.getTarget() === ContextActionTarget.NONE) contextAction.executeCallback(null, null);
else this.setContextAction(contextAction);
}
} else {

View File

@ -100,6 +100,7 @@
font-weight: var(--unit-font-weight);
line-height: normal;
position: absolute;
font-size: 12px;
}
[data-object|="unit-groundunit"] .unit-short-label {
@ -398,3 +399,53 @@
.ol-temporary-marker {
opacity: 0.5;
}
/*** Unit summary ***/
[data-object|="unit"] .unit-tactical {
color: white;
column-gap: 6px;
display: flex;
flex-wrap: wrap;
font-size: 11px;
font-weight: bold;
justify-content: left;
line-height: 12px;
pointer-events: none;
position: absolute;
row-gap: 1px;
left: 100%;
width: fit-content;
}
[data-hide-bullseyes] [data-object|="unit"] .unit-bullseyes {
display: none;
}
[data-hide-BRAA] [data-object|="unit"] .unit-braa {
display: none;
}
[data-object|="unit"] .unit-blue-bullseye {
text-shadow:
-1px -1px 0 #00F,
1px -1px 0 #00F,
-1px 1px 0 #00F,
1px 1px 0 #00F;
}
[data-object|="unit"] .unit-red-bullseye {
text-shadow:
-1px -1px 0 #F00,
1px -1px 0 #F00,
-1px 1px 0 #F00,
1px 1px 0 #F00;
}
[data-object|="unit"] .unit-braa {
text-shadow:
-1px -1px 0 #000,
1px -1px 0 #000,
-1px 1px 0 #000,
1px 1px 0 #000;
}

View File

@ -180,4 +180,4 @@
path.leaflet-interactive:focus {
outline: none;
}
}

View File

@ -393,3 +393,7 @@ export function wait(time) {
setTimeout(resolve, time);
});
}
export function computeBearingRangeString(latlng1, latlng2) {
return `${bearing(latlng1.lat, latlng1.lng, latlng2.lat, latlng2.lng).toFixed()}/${(latlng1.distanceTo(latlng2) / 1852).toFixed(0)}`;
}

View File

@ -26,6 +26,9 @@ export type MapOptions = {
cameraPluginEnabled: boolean;
cameraPluginMode: string;
tabletMode: boolean;
showUnitBullseyes: false;
showUnitBRAA: false;
AWACSMode: false;
};
export type MapHiddenTypes = {

View File

@ -10,6 +10,7 @@ import {
ContextActionChangedEvent,
ContextActionSetChangedEvent,
MapContextMenuRequestEvent,
SelectedUnitsChangedEvent,
SelectionClearedEvent,
UnitContextMenuRequestEvent,
} from "../../events";
@ -24,6 +25,7 @@ export function MapContextMenu(props: {}) {
const [yPosition, setYPosition] = useState(0);
const [latLng, setLatLng] = useState(null as null | LatLng);
const [unit, setUnit] = useState(null as null | Unit);
const [selectedUnits, setSelectedUnits] = useState([] as Unit[]);
var contentRef = useRef(null);
@ -45,6 +47,7 @@ export function MapContextMenu(props: {}) {
setXPosition(getApp().getMap().getContainer().offsetLeft + containerPoint.x);
setYPosition(getApp().getMap().getContainer().offsetTop + containerPoint.y);
});
SelectedUnitsChangedEvent.on((selectedUnits) => setSelectedUnits([...selectedUnits]));
}, []);
useEffect(() => {
@ -70,7 +73,13 @@ export function MapContextMenu(props: {}) {
let reorderedActions: ContextAction[] = contextActionSet
? Object.values(
contextActionSet.getContextActions(appSubState === UnitControlSubState.MAP_CONTEXT_MENU ? ContextActionTarget.POINT : ContextActionTarget.UNIT)
contextActionSet.getContextActions(
selectedUnits.length === 1 && unit === selectedUnits[0]
? ContextActionTarget.NONE
: appSubState === UnitControlSubState.MAP_CONTEXT_MENU
? ContextActionTarget.POINT
: ContextActionTarget.UNIT
)
).sort((a: ContextAction, b: ContextAction) => (a.getOptions().type < b.getOptions().type ? -1 : 1))
: [];
@ -104,7 +113,7 @@ export function MapContextMenu(props: {}) {
${colorString}
`}
onClick={() => {
if (contextActionIt.getOptions().executeImmediately) {
if (contextActionIt.getTarget() === ContextActionTarget.NONE) {
contextActionIt.executeCallback(null, null);
} else {
if (appSubState === UnitControlSubState.MAP_CONTEXT_MENU) {
@ -113,10 +122,13 @@ export function MapContextMenu(props: {}) {
contextActionIt.executeCallback(unit, null);
}
window.setTimeout(() => {
if (getApp().getSubState() === UnitControlSubState.MAP_CONTEXT_MENU || getApp().getSubState() === UnitControlSubState.UNIT_CONTEXT_MENU) {
getApp().setState(OlympusState.UNIT_CONTROL)
if (
getApp().getSubState() === UnitControlSubState.MAP_CONTEXT_MENU ||
getApp().getSubState() === UnitControlSubState.UNIT_CONTEXT_MENU
) {
getApp().setState(OlympusState.UNIT_CONTROL);
}
}, 200)
}, 200);
}
}}
>

View File

@ -0,0 +1,108 @@
import React, { useEffect, useState } from "react";
import { Menu } from "./components/menu";
import { OlToggle } from "../components/oltoggle";
import { MAP_OPTIONS_DEFAULTS } from "../../constants/constants";
import { AWACSReferenceChangedEvent as AWACSReferenceUnitChangedEvent, HotgroupsChangedEvent, MapOptionsChangedEvent } from "../../events";
import { getApp } from "../../olympusapp";
import { OlCoalitionToggle } from "../components/olcoalitiontoggle";
import { Coalition } from "../../types/types";
import { FaQuestionCircle } from "react-icons/fa";
import { Unit } from "../../unit/unit";
export function AWACSMenu(props: { open: boolean; onClose: () => void; children?: JSX.Element | JSX.Element[] }) {
const [callsign, setCallsign] = useState("Magic");
const [mapOptions, setMapOptions] = useState(MAP_OPTIONS_DEFAULTS);
const [coalition, setCoalition] = useState("blue" as Coalition);
const [hotgroups, setHotgroups] = useState({} as { [key: number]: Unit[] });
const [referenceUnit, setReferenceUnit] = useState(null as Unit | null);
useEffect(() => {
MapOptionsChangedEvent.on((mapOptions) => setMapOptions({ ...mapOptions }));
HotgroupsChangedEvent.on((hotgroups) => setHotgroups({ ...hotgroups }));
AWACSReferenceUnitChangedEvent.on((unit) => setReferenceUnit(unit));
}, []);
const enemyGroups = Object.values(hotgroups).filter((hotgroup) => {
return hotgroup.every((unit) => unit.getCoalition() !== coalition)
})
return (
<Menu title={"AWACS Tools"} open={props.open} onClose={props.onClose} showBackButton={false} canBeHidden={true}>
<div
className={`
flex flex-col gap-4 p-4 font-normal text-gray-800
dark:text-white
`}
>
<>
<div className="flex content-center gap-4">
<FaQuestionCircle
className={`my-auto min-h-5 min-w-5 text-sm text-gray-500`}
/>
<div className="flex flex-col gap-1 text-sm text-gray-500">
<p>1 Use the coalition toggle to change your coalition as AWACS.</p>
<p>2 Set a friendly unit as reference by right clicking on it and selecting "Set AWACS reference".</p>
<p>3 Set enemy unit hotgroups to automatically create picture and tactical calls to read on radio for your CAP.</p>
</div>
</div>
<div className="flex gap-4">
<span className="my-auto min-w-32 text-nowrap">Callsign</span>
<input
className={`
block h-10 w-full border-[2px] bg-gray-50 py-2.5 text-center
text-sm text-gray-900
dark:border-gray-700 dark:bg-olympus-600 dark:text-white
dark:placeholder-gray-400 dark:focus:border-blue-700
dark:focus:ring-blue-700
focus:border-blue-700 focus:ring-blue-500
`}
value={callsign}
onChange={(ev) => setCallsign(ev.target.value)}
></input>
<OlCoalitionToggle
onClick={() => {
coalition === "blue" && setCoalition("neutral");
coalition === "neutral" && setCoalition("red");
coalition === "red" && setCoalition("blue");
}}
coalition={coalition}
/>
</div>
<div className="flex flex-col gap-2">
<div className="flex gap-2">
<OlToggle
onClick={() => {
getApp().getMap().setOption("showUnitBullseyes", !mapOptions.showUnitBullseyes);
}}
toggled={mapOptions.showUnitBullseyes}
/>{" "}
Show units Bullseye position
</div>
<div className="flex gap-2">
<OlToggle
onClick={() => {
getApp().getMap().setOption("showUnitBRAA", !mapOptions.showUnitBRAA);
}}
toggled={mapOptions.showUnitBRAA}
/>
Show units BRAA from reference unit
</div>
</div>
<div className="mt-4">
{
referenceUnit ? <>
{
enemyGroups.length == 0 ? <>
No enemy or neutral hotgroup
</>:<>
</>
}
</>:<>No reference unit selected</>
}
</div>
</>
</div>
</Menu>
);
}

View File

@ -28,9 +28,9 @@ export function CoordinatesPanel(props: {}) {
return (
<div
className={`
absolute bottom-[20px] right-[310px] flex w-[380px] flex-col
items-center justify-between gap-2 rounded-lg bg-gray-200 p-3 text-sm
backdrop-blur-lg backdrop-grayscale
absolute bottom-[20px] right-[310px] flex min-h-12 w-[380px] flex-col
items-center justify-between gap-2 rounded-lg bg-gray-200 px-3 py-3
text-sm backdrop-blur-lg backdrop-grayscale
dark:bg-olympus-800/90 dark:text-gray-200
`}
>

View File

@ -35,7 +35,8 @@ export function Header() {
MapOptionsChangedEvent.on((mapOptions) => setMapOptions({ ...mapOptions }));
MapSourceChangedEvent.on((source) => setMapSource(source));
ConfigLoadedEvent.on((config: OlympusConfig) => {
var sources = Object.keys(config.frontend.mapMirrors).concat(Object.keys(config.frontend.mapLayers));
var sources = Object.keys(config.frontend.mapMirrors).concat(Object.keys(config.frontend.mapLayers)).concat(getApp().getMap().getLayers());
setMapSources(sources);
});
CommandModeOptionsChangedEvent.on((commandModeOptions) => {

View File

@ -1,15 +1,13 @@
import React, { useEffect, useState } from "react";
import { AppStateChangedEvent, ContextActionChangedEvent, HotgroupsChangedEvent, InfoPopupEvent } from "../../events";
import { AppStateChangedEvent, HotgroupsChangedEvent } from "../../events";
import { OlympusState } from "../../constants/constants";
import { ContextAction } from "../../unit/contextaction";
import { OlStateButton } from "../components/olstatebutton";
import { faUserGroup } from "@fortawesome/free-solid-svg-icons";
import { getApp } from "../../olympusapp";
import { Unit } from "../../unit/unit";
export function HotGroupBar(props: {}) {
const [hotgroups, setHotgroups] = useState({} as { [key: number]: number });
const [hotgroups, setHotgroups] = useState({} as { [key: number]: Unit[] });
const [appState, setAppState] = useState(OlympusState.NOT_INITIALIZED);
const [menuHidden, setMenuHidden] = useState(false);
useEffect(() => {
AppStateChangedEvent.on((state, subState) => setAppState(state));
@ -18,14 +16,11 @@ export function HotGroupBar(props: {}) {
return (
<div
data-menuhidden={menuHidden || appState === OlympusState.IDLE}
className={`
absolute bottom-16 left-[50%] flex gap-2
data-[menuhidden='false']:translate-x-[calc(200px-50%+2rem)]
data-[menuhidden='true']:translate-x-[calc(-50%+2rem)]
absolute bottom-24 left-[50%] flex translate-x-[calc(-50%+2rem)] gap-2
`}
>
{Object.entries(hotgroups).map(([hotgroup, counter]) => {
{Object.entries(hotgroups).map(([hotgroup, units]) => {
return (
<div className="flex flex-col content-center gap-2">
<div
@ -37,11 +32,13 @@ export function HotGroupBar(props: {}) {
<div className="relative -rotate-45">{hotgroup}</div>
</div>
<OlStateButton checked={false} onClick={() => {getApp().getUnitsManager().selectUnitsByHotgroup(parseInt(hotgroup))}} tooltip="">
<OlStateButton checked={false} onClick={() => {getApp().getUnitsManager().selectUnitsByHotgroup(parseInt(hotgroup))}} tooltip="Select units of this hotgroup" className={`
min-h-12 min-w-12
`}>
<span
className={`text-sm text-white`}
className={`text-white`}
>
{counter}
{units.length}
</span>
</OlStateButton>
</div>

View File

@ -58,7 +58,7 @@ export function MiniMapPanel(props: {}) {
${mapOptions.showMinimap ? `bottom-[188px]` : `bottom-[20px]`}
flex w-[288px] items-center justify-between
${mapOptions.showMinimap ? `rounded-t-lg` : `rounded-lg`}
bg-gray-200 p-3 text-sm backdrop-blur-lg backdrop-grayscale
h-12 bg-gray-200 px-3 text-sm backdrop-blur-lg backdrop-grayscale
dark:bg-olympus-800/90 dark:text-gray-200
`}
>

View File

@ -2,7 +2,7 @@ import React, { useEffect, useState } from "react";
import { AudioSinksChangedEvent } from "../../events";
import { AudioSink } from "../../audio/audiosink";
import { RadioSink } from "../../audio/radiosink";
import { FaJetFighter, FaRadio } from "react-icons/fa6";
import { FaJetFighter, FaRadio, FaVolumeHigh } from "react-icons/fa6";
import { OlStateButton } from "../components/olstatebutton";
import { UnitSink } from "../../audio/unitsink";
@ -19,13 +19,12 @@ export function RadiosSummaryPanel(props: {}) {
<div
className={`
absolute bottom-[20px] right-[700px] flex w-fit flex-col
items-center justify-between gap-2 rounded-lg bg-gray-200 p-3
text-sm backdrop-blur-lg backdrop-grayscale
dark:bg-olympus-800/90 dark:text-gray-200
items-center justify-between gap-2 rounded-lg bg-transparent text-sm
text-gray-200
`}
>
<div className="flex w-full items-center justify-between gap-2">
<FaRadio className="text-xl" />
{audioSinks.filter((audioSinks) => audioSinks instanceof RadioSink).length > 0 &&
audioSinks
.filter((audioSinks) => audioSinks instanceof RadioSink)
@ -43,15 +42,14 @@ export function RadiosSummaryPanel(props: {}) {
}}
tooltip="Click to talk, lights up when receiving"
buttonColor={radioSink.getReceiving() ? "white" : null}
className="min-h-12 min-w-12"
>
<span className={`font-bold text-gray-200`}>{idx + 1}</span>
<span className={`text-gray-200`}><FaRadio className={`
-translate-x-2 translate-y-1
`} /> <div className="translate-x-2 font-bold">{idx + 1}</div></span>
</OlStateButton>
);
})}
{audioSinks.filter((audioSinks) => audioSinks instanceof UnitSink).length > 0 && <FaJetFighter className={`
text-xl
`} />}
{audioSinks.filter((audioSinks) => audioSinks instanceof UnitSink).length > 0 &&
audioSinks
.filter((audioSinks) => audioSinks instanceof UnitSink)
@ -68,8 +66,11 @@ export function RadiosSummaryPanel(props: {}) {
unitSink.setPtt(false);
}}
tooltip="Click to talk"
className="min-h-12 min-w-12"
>
<span className={`font-bold text-gray-200`}>{idx + 1}</span>
<span className={`text-gray-200`}><FaVolumeHigh className={`
-translate-x-2 translate-y-1
`} /> <div className="translate-x-2 font-bold">{idx + 1}</div></span>
</OlStateButton>
);
})}

View File

@ -1,6 +1,6 @@
import React, { useEffect, useState } from "react";
import { OlStateButton } from "../components/olstatebutton";
import { faGamepad, faPencil, faEllipsisV, faCog, faQuestionCircle, faPlusSquare, faVolumeHigh, faJ, faCrown } from "@fortawesome/free-solid-svg-icons";
import { faGamepad, faPencil, faEllipsisV, faCog, faQuestionCircle, faPlusSquare, faVolumeHigh, faJ, faCrown, faA } from "@fortawesome/free-solid-svg-icons";
import { getApp } from "../../olympusapp";
import { NO_SUBSTATE, OlympusState, OlympusSubState, SpawnSubState } from "../../constants/constants";
import { AppStateChangedEvent } from "../../events";
@ -65,13 +65,21 @@ export function SideBar() {
icon={faVolumeHigh}
tooltip="Hide/show audio menu"
></OlStateButton>
<OlStateButton
{/*}<OlStateButton
onClick={() => {
getApp().setState(appState !== OlympusState.JTAC ? OlympusState.JTAC : OlympusState.IDLE);
}}
checked={appState === OlympusState.JTAC}
icon={faJ}
tooltip="Hide/show JTAC menu"
></OlStateButton>{*/}
<OlStateButton
onClick={() => {
getApp().setState(appState !== OlympusState.AWACS ? OlympusState.AWACS : OlympusState.IDLE);
}}
checked={appState === OlympusState.AWACS}
icon={faA}
tooltip="Hide/show AWACS menu"
></OlStateButton>
<OlStateButton
onClick={() => {

View File

@ -3,9 +3,8 @@ import { ContextActionSet } from "../../unit/contextactionset";
import { OlStateButton } from "../components/olstatebutton";
import { getApp } from "../../olympusapp";
import { ContextAction } from "../../unit/contextaction";
import { CONTEXT_ACTION_COLORS, MAP_OPTIONS_DEFAULTS } from "../../constants/constants";
import { FaInfoCircle } from "react-icons/fa";
import { FaChevronDown, FaChevronLeft, FaChevronRight, FaChevronUp } from "react-icons/fa6";
import { CONTEXT_ACTION_COLORS, ContextActionTarget, MAP_OPTIONS_DEFAULTS } from "../../constants/constants";
import { FaChevronDown,FaChevronUp } from "react-icons/fa6";
import { OlympusState } from "../../constants/constants";
import { AppStateChangedEvent, ContextActionChangedEvent, ContextActionSetChangedEvent, MapOptionsChangedEvent } from "../../events";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
@ -83,7 +82,7 @@ export function UnitControlBar(props: {}) {
tooltip={contextActionIt.getLabel()}
buttonColor={CONTEXT_ACTION_COLORS[contextActionIt.getOptions().type ?? 0]}
onClick={() => {
if (contextActionIt.getOptions().executeImmediately) {
if (contextActionIt.getTarget() === ContextActionTarget.NONE) {
contextActionIt.executeCallback(null, null);
} else {
contextActionIt !== contextAction

View File

@ -12,7 +12,6 @@ import {
emissionsCountermeasures,
maxAltitudeValues,
maxSpeedValues,
minAltitudeValues,
reactionsToThreat,
speedIncrements,
} from "../../constants/constants";

View File

@ -31,6 +31,7 @@ import { HotGroupBar } from "./panels/hotgroupsbar";
import { SpawnContextMenu } from "./contextmenus/spawncontextmenu";
import { CoordinatesPanel } from "./panels/coordinatespanel";
import { RadiosSummaryPanel } from "./panels/radiossummarypanel";
import { AWACSMenu } from "./panels/awacsmenu";
export type OlympusUIState = {
mainMenuVisible: boolean;
@ -93,7 +94,8 @@ export function UI() {
open={appState === OlympusState.UNIT_CONTROL && appSubState === UnitControlSubState.UNIT_EXPLOSION_MENU}
onClose={() => getApp().setState(OlympusState.IDLE)}
/>
<JTACMenu open={appState === OlympusState.JTAC} onClose={() => getApp().setState(OlympusState.IDLE)} />
{/*}<JTACMenu open={appState === OlympusState.JTAC} onClose={() => getApp().setState(OlympusState.IDLE)} />{*/}
<AWACSMenu open={appState === OlympusState.AWACS} onClose={() => getApp().setState(OlympusState.IDLE)} />
<MiniMapPanel />
<ControlsPanel />

View File

@ -4,7 +4,6 @@ import { LatLng } from "leaflet";
import { ContextActionTarget, ContextActionType } from "../constants/constants";
export interface ContextActionOptions {
executeImmediately?: boolean;
type: ContextActionType;
code: string | null;
shiftKey?: boolean;
@ -32,7 +31,6 @@ export class ContextAction {
this.#callback = callback;
this.#icon = icon;
this.#options = {
executeImmediately: false,
...options,
};
}

View File

@ -21,7 +21,7 @@ export class ContextActionSet {
}
getContextActions(targetFilter?: ContextActionTarget) {
if (targetFilter) {
if (targetFilter !== undefined) {
var filteredContextActionSet = new ContextActionSet();
Object.keys(this.#contextActions).forEach((key) => {
if (this.#contextActions[key].getTarget() === targetFilter) filteredContextActionSet[key] = this.#contextActions[key];

View File

@ -969,6 +969,27 @@ export abstract class Unit extends CustomMarker {
el.appendChild(summary);
}
var tactical = document.createElement("div");
tactical.classList.add("unit-tactical");
if (iconOptions.showBullseyes) {
var bullseyes = document.createElement("div");
bullseyes.classList.add("unit-bullseyes");
var blueBullseye = document.createElement("div");
blueBullseye.classList.add("unit-blue-bullseye");
var redBullseye = document.createElement("div");
redBullseye.classList.add("unit-red-bullseye");
bullseyes.appendChild(blueBullseye);
bullseyes.appendChild(redBullseye);
tactical.appendChild(bullseyes);
}
if (iconOptions.showBRAA) {
var BRAA = document.createElement("div");
BRAA.classList.add("unit-braa");
tactical.appendChild(BRAA);
}
el.appendChild(tactical)
this.getElement()?.appendChild(el);
}
@ -1490,6 +1511,21 @@ export abstract class Unit extends CustomMarker {
const hotgroupEl = element.querySelector(".unit-hotgroup-id") as HTMLElement;
if (hotgroupEl) hotgroupEl.innerText = String(this.#hotgroup);
}
/* Set bullseyes positions */
const bullseyes = getApp().getMissionManager().getBullseyes();
const blueBullseye = `${bearing(bullseyes[2].getLatLng().lat, bullseyes[2].getLatLng().lng, this.getLatLng().lat, this.getLatLng().lng).toFixed()}/${(bullseyes[2].getLatLng().distanceTo(this.getLatLng()) / 1852).toFixed(0)}`
const redBullseye = `${bearing(bullseyes[1].getLatLng().lat, bullseyes[1].getLatLng().lng, this.getLatLng().lat, this.getLatLng().lng).toFixed()}/${(bullseyes[1].getLatLng().distanceTo(this.getLatLng()) / 1852).toFixed(0)}`
if (element.querySelector(".unit-blue-bullseye")) (<HTMLElement>element.querySelector(".unit-blue-bullseye")).innerText = `${blueBullseye}`;
if (element.querySelector(".unit-red-bullseye")) (<HTMLElement>element.querySelector(".unit-red-bullseye")).innerText = `${redBullseye}`;
/* Set BRAA */
const reference = getApp().getUnitsManager().getAWACSReference();
if (reference && reference !== this) {
const BRAA = `${bearing(reference.getLatLng().lat, reference.getLatLng().lng, this.getLatLng().lat, this.getLatLng().lng).toFixed()}/${(reference.getLatLng().distanceTo(this.getLatLng()) / 1852).toFixed(0)}`
if (element.querySelector(".unit-braa")) (<HTMLElement>element.querySelector(".unit-braa")).innerText = `${BRAA}`;
}
else if (element.querySelector(".unit-braa")) (<HTMLElement>element.querySelector(".unit-braa")).innerText = ``;
}
/* Set vertical offset for altitude stacking */
@ -1770,7 +1806,9 @@ export abstract class AirUnit extends Unit {
showSummary: belongsToCommandedCoalition || this.getDetectionMethods().some((value) => [VISUAL, OPTIC, RADAR, IRST, DLINK].includes(value)),
showCallsign: belongsToCommandedCoalition,
rotateToHeading: false,
};
showBullseyes: true,
showBRAA: true,
} as ObjectIconOptions;
}
appendContextActions(contextActionSet: ContextActionSet) {
@ -1783,6 +1821,7 @@ export abstract class AirUnit extends Unit {
/* Context actions that require a target unit */
contextActionSet.addContextAction(this, ContextActions.ATTACK);
contextActionSet.addContextAction(this, ContextActions.FOLLOW);
contextActionSet.addContextAction(this, ContextActions.SET_AWACS_REFERENCE)
if (this.canTargetPoint()) {
/* Context actions that require a target position */
@ -1858,7 +1897,9 @@ export class GroundUnit extends Unit {
showSummary: false,
showCallsign: belongsToCommandedCoalition,
rotateToHeading: false,
};
showBullseyes: false,
showBRAA: false,
} as ObjectIconOptions;
}
appendContextActions(contextActionSet: ContextActionSet) {
@ -1927,7 +1968,9 @@ export class NavyUnit extends Unit {
showSummary: false,
showCallsign: belongsToCommandedCoalition,
rotateToHeading: false,
};
showBullseyes: false,
showBRAA: false,
} as ObjectIconOptions;
}
appendContextActions(contextActionSet: ContextActionSet) {

View File

@ -23,6 +23,7 @@ import { UnitDataFileImport } from "./importexport/unitdatafileimport";
import { CoalitionCircle } from "../map/coalitionarea/coalitioncircle";
import { ContextActionSet } from "./contextactionset";
import {
AWACSReferenceChangedEvent,
CommandModeOptionsChangedEvent,
ContactsUpdatedEvent,
HotgroupsChangedEvent,
@ -49,6 +50,7 @@ export class UnitsManager {
#unitDataImport!: UnitDataFileImport;
#unitDatabase: UnitDatabase;
#protectionCallback: (units: Unit[]) => void = (units) => {};
#AWACSReference: Unit | null = null;
constructor() {
this.#unitDatabase = new UnitDatabase();
@ -1125,15 +1127,15 @@ export class UnitsManager {
units.forEach((unit: Unit) => unit.setHotgroup(hotgroup));
this.#showActionMessage(units, `added to hotgroup ${hotgroup}`);
let hotgroups: { [key: number]: number } = {};
let hotgroups: { [key: number]: Unit[] } = {};
for (let ID in this.#units) {
const unit = this.#units[ID];
if (unit.getAlive() && !unit.getHuman()) {
const hotgroup = unit.getHotgroup();
if (hotgroup) {
if (!(hotgroup in hotgroups)) {
hotgroups[hotgroup] = 1;
} else hotgroups[hotgroup] += 1;
hotgroups[hotgroup] = [unit];
} else hotgroups[hotgroup].push(unit);
}
}
}
@ -1544,6 +1546,15 @@ export class UnitsManager {
this.#protectionCallback(this.getSelectedUnits());
}
setAWACSReference(ID) {
this.#AWACSReference = this.#units[ID] ?? null;
AWACSReferenceChangedEvent.dispatch(this.#AWACSReference)
}
getAWACSReference() {
return this.#AWACSReference;
}
/***********************************************/
#onUnitSelection(unit: Unit) {
if (this.getSelectedUnits().length > 0) {