mirror of
https://github.com/Pax1601/DCSOlympus.git
synced 2025-10-29 16:56:34 +00:00
Readded camera plugin, hotgroups, fixed smoke spawning
This commit is contained in:
@@ -319,7 +319,7 @@ export const IADSDensities: { [key: string]: number } = {
|
||||
"Radar (EWR)": 0.05,
|
||||
};
|
||||
|
||||
export const MAP_OPTIONS_DEFAULTS = {
|
||||
export const MAP_OPTIONS_DEFAULTS: MapOptions = {
|
||||
hideGroupMembers: true,
|
||||
hideUnitsShortRangeRings: true,
|
||||
showUnitContacts: true,
|
||||
@@ -332,7 +332,11 @@ export const MAP_OPTIONS_DEFAULTS = {
|
||||
showMinimap: false,
|
||||
protectDCSUnits: true,
|
||||
keepRelativePositions: true,
|
||||
} as MapOptions;
|
||||
cameraPluginPort: 3003,
|
||||
cameraPluginRatio: 1,
|
||||
cameraPluginEnabled: false,
|
||||
cameraPluginMode: 'map'
|
||||
};
|
||||
|
||||
export const MAP_HIDDEN_TYPES_DEFAULTS = {
|
||||
human: false,
|
||||
|
||||
@@ -95,6 +95,19 @@ export class InfoPopupEvent {
|
||||
}
|
||||
}
|
||||
|
||||
export class HideMenuEvent {
|
||||
static on(callback: (hidden: boolean) => void) {
|
||||
document.addEventListener(this.name, (ev: CustomEventInit) => {
|
||||
callback(ev.detail.hidden);
|
||||
});
|
||||
}
|
||||
|
||||
static dispatch(hidden: boolean) {
|
||||
document.dispatchEvent(new CustomEvent(this.name, {detail: {hidden}}));
|
||||
console.log(`Event ${this.name} dispatched`);
|
||||
}
|
||||
}
|
||||
|
||||
/************** Map events ***************/
|
||||
export class HiddenTypesChangedEvent {
|
||||
static on(callback: (hiddenTypes: MapHiddenTypes) => void) {
|
||||
@@ -277,6 +290,19 @@ export class UnitContextMenuRequestEvent {
|
||||
}
|
||||
}
|
||||
|
||||
export class HotgroupsChangedEvent {
|
||||
static on(callback: (hotgroups: {[key: number]: number}) => void) {
|
||||
document.addEventListener(this.name, (ev: CustomEventInit) => {
|
||||
callback(ev.detail.hotgroups);
|
||||
});
|
||||
}
|
||||
|
||||
static dispatch(hotgroups: {[key: number]: number}) {
|
||||
document.dispatchEvent(new CustomEvent(this.name, {detail: {hotgroups}}));
|
||||
console.log(`Event ${this.name} dispatched`);
|
||||
}
|
||||
}
|
||||
|
||||
/************** Command mode events ***************/
|
||||
export class CommandModeOptionsChangedEvent {
|
||||
static on(callback: (options: CommandModeOptions) => void) {
|
||||
|
||||
@@ -218,10 +218,12 @@ export class Map extends L.Map {
|
||||
if (unit.getSelected()) this.#moveDestinationPreviewMarkers();
|
||||
});
|
||||
|
||||
MapOptionsChangedEvent.on((options) => {
|
||||
MapOptionsChangedEvent.on((options: MapOptions) => {
|
||||
this.getContainer().toggleAttribute("data-hide-labels", !options.showUnitLabels);
|
||||
//this.#cameraControlPort = options[DCS_LINK_PORT] as number;
|
||||
//this.#cameraZoomRatio = 50 / (20 + (options[DCS_LINK_RATIO] as number));
|
||||
this.#cameraControlPort = options.cameraPluginPort;
|
||||
this.#cameraZoomRatio = 50 / (20 + options.cameraPluginRatio);
|
||||
this.#slaveDCSCamera = options.cameraPluginEnabled;
|
||||
this.#cameraControlMode = options.cameraPluginMode;
|
||||
|
||||
if (this.#slaveDCSCamera) {
|
||||
this.#broadcastPosition();
|
||||
@@ -379,6 +381,13 @@ export class Map extends L.Map {
|
||||
|
||||
setEffectRequestTable(effectRequestTable: EffectRequestTable) {
|
||||
this.#effectRequestTable = effectRequestTable;
|
||||
if (getApp().getState() === OlympusState.SPAWN && getApp().getSubState() === SpawnSubState.SPAWN_EFFECT) {
|
||||
this.#currentEffectMarker?.removeFrom(this);
|
||||
this.#currentEffectMarker = null;
|
||||
if (this.#effectRequestTable?.type === "smoke")
|
||||
this.#currentEffectMarker = new SmokeMarker(new L.LatLng(0, 0), this.#effectRequestTable.smokeColor ?? "white");
|
||||
this.#currentEffectMarker?.addTo(this);
|
||||
}
|
||||
}
|
||||
|
||||
setContextActionSet(contextActionSet: ContextActionSet | null) {
|
||||
@@ -784,28 +793,6 @@ export class Map extends L.Map {
|
||||
return this.#previousZoom;
|
||||
}
|
||||
|
||||
setSlaveDCSCamera(newSlaveDCSCamera: boolean) {
|
||||
this.#slaveDCSCamera = newSlaveDCSCamera;
|
||||
let button = document.getElementById("camera-link-control");
|
||||
button?.classList.toggle("off", !newSlaveDCSCamera);
|
||||
if (this.#slaveDCSCamera) {
|
||||
this.#broadcastPosition();
|
||||
window.setTimeout(() => {
|
||||
this.#broadcastPosition();
|
||||
}, 500); // DCS does not always apply the altitude correctly at the first set when changing map type
|
||||
}
|
||||
}
|
||||
|
||||
setCameraControlMode(newCameraControlMode: string) {
|
||||
this.#cameraControlMode = newCameraControlMode;
|
||||
if (this.#slaveDCSCamera) {
|
||||
this.#broadcastPosition();
|
||||
window.setTimeout(() => {
|
||||
this.#broadcastPosition();
|
||||
}, 500); // DCS does not always apply the altitude correctly at the first set when changing map type
|
||||
}
|
||||
}
|
||||
|
||||
increaseCameraZoom() {
|
||||
//const slider = document.querySelector(`label[title="${DCS_LINK_RATIO}"] input`);
|
||||
//if (slider instanceof HTMLInputElement) {
|
||||
|
||||
@@ -50,7 +50,7 @@ export class OlympusApp {
|
||||
constructor() {
|
||||
SelectedUnitsChangedEvent.on((selectedUnits) => {
|
||||
if (selectedUnits.length > 0) this.setState(OlympusState.UNIT_CONTROL);
|
||||
else this.getState() === OlympusState.UNIT_CONTROL && this.setState(OlympusState.IDLE)
|
||||
else this.getState() === OlympusState.UNIT_CONTROL && this.setState(OlympusState.IDLE);
|
||||
});
|
||||
}
|
||||
|
||||
@@ -144,11 +144,13 @@ export class OlympusApp {
|
||||
}
|
||||
|
||||
setState(state: OlympusState, subState: OlympusSubState = NO_SUBSTATE) {
|
||||
this.#state = state;
|
||||
this.#subState = subState;
|
||||
if (state !== this.#state || subState !== this.#subState) {
|
||||
this.#state = state;
|
||||
this.#subState = subState;
|
||||
|
||||
console.log(`App state set to ${state}, substate ${subState}`);
|
||||
AppStateChangedEvent.dispatch(state, subState);
|
||||
console.log(`App state set to ${state}, substate ${subState}`);
|
||||
AppStateChangedEvent.dispatch(state, subState);
|
||||
}
|
||||
}
|
||||
|
||||
getState() {
|
||||
@@ -165,6 +167,6 @@ export class OlympusApp {
|
||||
setTimeout(() => {
|
||||
this.#infoMessages.shift();
|
||||
InfoPopupEvent.dispatch(this.#infoMessages);
|
||||
}, 5000)
|
||||
}, 5000);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -22,6 +22,10 @@ export type MapOptions = {
|
||||
showMinimap: boolean;
|
||||
protectDCSUnits: boolean;
|
||||
keepRelativePositions: boolean;
|
||||
cameraPluginPort: number;
|
||||
cameraPluginRatio: number;
|
||||
cameraPluginEnabled: boolean;
|
||||
cameraPluginMode: string;
|
||||
};
|
||||
|
||||
export type MapHiddenTypes = {
|
||||
|
||||
@@ -8,7 +8,7 @@ export function OlStateButton(props: {
|
||||
className?: string;
|
||||
buttonColor?: string | null;
|
||||
checked: boolean;
|
||||
icon: IconProp;
|
||||
icon?: IconProp;
|
||||
tooltip: string;
|
||||
onClick: () => void;
|
||||
children?: JSX.Element | JSX.Element[];
|
||||
@@ -20,14 +20,16 @@ export function OlStateButton(props: {
|
||||
(props.className ?? "") +
|
||||
`
|
||||
h-[40px] w-[40px] flex-none rounded-md text-lg font-medium
|
||||
dark:bg-olympus-600 dark:text-gray-300 dark:hover:bg-olympus-300
|
||||
dark:bg-olympus-600 dark:text-gray-300
|
||||
`;
|
||||
|
||||
let textColor = "white";
|
||||
if (props.checked && props.buttonColor == "white") {
|
||||
textColor = "#243141"
|
||||
if ((props.checked || hover) && props.buttonColor == "white") {
|
||||
textColor = "#243141";
|
||||
}
|
||||
|
||||
const opacity = (hover && !props.checked) ? "AA" : "FF";
|
||||
|
||||
return (
|
||||
<>
|
||||
<button
|
||||
@@ -41,7 +43,7 @@ export function OlStateButton(props: {
|
||||
className={className}
|
||||
style={{
|
||||
border: props.buttonColor ? "2px solid " + props.buttonColor : "0px solid transparent",
|
||||
background: props.checked ? (props.buttonColor? props.buttonColor: "#3b82f6"): "#243141",
|
||||
background: props.checked || hover ? (props.buttonColor ? props.buttonColor : "#3b82f6" + opacity) : "#243141" + opacity,
|
||||
}}
|
||||
onMouseEnter={() => {
|
||||
setHover(true);
|
||||
@@ -51,7 +53,7 @@ export function OlStateButton(props: {
|
||||
}}
|
||||
>
|
||||
<div className="m-auto flex w-fit content-center justify-center gap-2">
|
||||
<FontAwesomeIcon icon={props.icon} className="m-auto" style={{color: textColor}} />
|
||||
{props.icon && <FontAwesomeIcon icon={props.icon} className="m-auto" style={{ color: textColor }} />}
|
||||
{props.children}
|
||||
</div>
|
||||
</button>
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
import { faArrowLeft, faClose } from "@fortawesome/free-solid-svg-icons";
|
||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||
import React, { useState } from "react";
|
||||
import React, { useEffect, useState } from "react";
|
||||
import { FaChevronDown, FaChevronUp } from "react-icons/fa";
|
||||
import { HideMenuEvent } from "../../../events";
|
||||
|
||||
export function Menu(props: {
|
||||
title: string;
|
||||
@@ -15,7 +16,11 @@ export function Menu(props: {
|
||||
const [hide, setHide] = useState(true);
|
||||
|
||||
if (!props.open && hide) setHide(false);
|
||||
|
||||
|
||||
useEffect(() => {
|
||||
HideMenuEvent.dispatch(hide)
|
||||
}, [hide])
|
||||
|
||||
return (
|
||||
<div
|
||||
data-open={props.open}
|
||||
|
||||
@@ -62,7 +62,7 @@ export function EffectSpawnMenu(props: { effect: string }) {
|
||||
setSmokeColor(optionSmokeColor);
|
||||
}}
|
||||
tooltip=""
|
||||
borderColor={optionSmokeColor}
|
||||
buttonColor={optionSmokeColor}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
|
||||
@@ -61,177 +61,187 @@ export function Header() {
|
||||
}
|
||||
|
||||
return (
|
||||
<nav
|
||||
<nav
|
||||
className={`
|
||||
z-10 flex w-full gap-4 border-gray-200 bg-gray-300 px-3 drop-shadow-md
|
||||
align-center
|
||||
dark:border-gray-800 dark:bg-olympus-900
|
||||
`}
|
||||
>
|
||||
<img src="images/icon.png" className={`my-auto h-10 w-10 rounded-md p-0`}></img>
|
||||
{!scrolledLeft && (
|
||||
<FaChevronLeft
|
||||
className={`
|
||||
z-10 flex w-full gap-4 border-gray-200 bg-gray-300 px-3
|
||||
drop-shadow-md align-center
|
||||
dark:border-gray-800 dark:bg-olympus-900
|
||||
absolute left-14 h-full w-6 rounded-lg px-2 py-3.5 text-gray-200
|
||||
dark:bg-olympus-900
|
||||
`}
|
||||
/>
|
||||
)}
|
||||
<div
|
||||
className={`
|
||||
my-2 flex w-full items-center gap-3 overflow-x-scroll no-scrollbar
|
||||
`}
|
||||
onScroll={(ev) => onScroll(ev.target)}
|
||||
ref={scrollRef}
|
||||
>
|
||||
<div
|
||||
className={`
|
||||
mr-auto hidden flex-none flex-row items-center justify-start gap-6
|
||||
lg:flex
|
||||
`}
|
||||
>
|
||||
<img
|
||||
src="images/icon.png"
|
||||
className={`my-auto h-10 w-10 rounded-md p-0`}
|
||||
></img>
|
||||
{!scrolledLeft && (
|
||||
<FaChevronLeft
|
||||
<div className="flex flex-col items-start">
|
||||
<div
|
||||
className={`
|
||||
absolute left-14 h-full w-6 rounded-lg px-2 py-3.5 text-gray-200
|
||||
dark:bg-olympus-900
|
||||
pt-1 text-xs text-gray-800
|
||||
dark:text-gray-400
|
||||
`}
|
||||
/>
|
||||
)}
|
||||
>
|
||||
Connected to
|
||||
</div>
|
||||
<div
|
||||
className={`
|
||||
flex items-center justify-center gap-2 text-sm font-extrabold
|
||||
text-gray-800
|
||||
dark:text-gray-200
|
||||
`}
|
||||
>
|
||||
{IP}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{commandModeOptions.commandMode === BLUE_COMMANDER && (
|
||||
<div
|
||||
className={`
|
||||
my-2 flex w-full items-center gap-3 overflow-x-scroll no-scrollbar
|
||||
`}
|
||||
onScroll={(ev) => onScroll(ev.target)}
|
||||
ref={scrollRef}
|
||||
>
|
||||
<div
|
||||
className={`
|
||||
mr-auto hidden flex-none flex-row items-center justify-start
|
||||
gap-6
|
||||
lg:flex
|
||||
`}
|
||||
>
|
||||
<div className="flex flex-col items-start">
|
||||
<div
|
||||
className={`
|
||||
pt-1 text-xs text-gray-800
|
||||
dark:text-gray-400
|
||||
`}
|
||||
>
|
||||
Connected to
|
||||
</div>
|
||||
<div
|
||||
className={`
|
||||
flex items-center justify-center gap-2 text-sm
|
||||
font-extrabold text-gray-800
|
||||
dark:text-gray-200
|
||||
`}
|
||||
>
|
||||
{IP}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{commandModeOptions.commandMode === BLUE_COMMANDER && <div className={`
|
||||
flex h-full rounded-md bg-blue-600 px-4 text-white
|
||||
`}><span className="my-auto font-bold">BLUE Commander ({commandModeOptions.spawnPoints.blue} points)</span></div>}
|
||||
<div
|
||||
className={`flex h-fit flex-row items-center justify-start gap-1`}
|
||||
>
|
||||
<OlLockStateButton
|
||||
checked={!mapOptions.protectDCSUnits}
|
||||
onClick={() => {
|
||||
getApp().getMap().setOption("protectDCSUnits", !mapOptions.protectDCSUnits);
|
||||
}}
|
||||
tooltip="Lock/unlock protected units (from scripted mission)"
|
||||
/>
|
||||
<OlRoundStateButton
|
||||
checked={audioEnabled}
|
||||
onClick={() => {
|
||||
audioEnabled ? getApp().getAudioManager().stop() : getApp().getAudioManager().start();
|
||||
setAudioEnabled(!audioEnabled);
|
||||
}}
|
||||
tooltip="Enable/disable audio and radio backend"
|
||||
icon={faVolumeHigh}
|
||||
/>
|
||||
</div>
|
||||
<div className={`h-8 w-0 border-l-[2px] border-gray-700`}></div>
|
||||
<div
|
||||
className={`flex h-fit flex-row items-center justify-start gap-1`}
|
||||
>
|
||||
{Object.entries({
|
||||
human: olButtonsVisibilityHuman,
|
||||
olympus: olButtonsVisibilityOlympus,
|
||||
dcs: olButtonsVisibilityDcs,
|
||||
}).map((entry) => {
|
||||
return (
|
||||
<OlRoundStateButton
|
||||
key={entry[0]}
|
||||
onClick={() => {
|
||||
getApp().getMap().setHiddenType(entry[0], !mapHiddenTypes[entry[0]]);
|
||||
}}
|
||||
checked={!mapHiddenTypes[entry[0]]}
|
||||
icon={entry[1]}
|
||||
tooltip={"Hide/show " + entry[0] + " units"}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
<div className={`h-8 w-0 border-l-[2px] border-gray-700`}></div>
|
||||
<div
|
||||
className={`flex h-fit flex-row items-center justify-start gap-1`}
|
||||
>
|
||||
<OlRoundStateButton
|
||||
onClick={() => getApp().getMap().setHiddenType("blue", !mapHiddenTypes["blue"])}
|
||||
checked={!mapHiddenTypes["blue"]}
|
||||
icon={faFlag}
|
||||
className={"!text-blue-500"}
|
||||
tooltip={"Hide/show blue units"}
|
||||
/>
|
||||
<OlRoundStateButton
|
||||
onClick={() => getApp().getMap().setHiddenType("red", !mapHiddenTypes["red"])}
|
||||
checked={!mapHiddenTypes["red"]}
|
||||
icon={faFlag}
|
||||
className={"!text-red-500"}
|
||||
tooltip={"Hide/show red units"}
|
||||
/>
|
||||
<OlRoundStateButton
|
||||
onClick={() => getApp().getMap().setHiddenType("neutral", !mapHiddenTypes["neutral"])}
|
||||
checked={!mapHiddenTypes["neutral"]}
|
||||
icon={faFlag}
|
||||
className={"!text-gray-500"}
|
||||
tooltip={"Hide/show neutral units"}
|
||||
/>
|
||||
</div>
|
||||
<div className={`h-8 w-0 border-l-[2px] border-gray-700`}></div>
|
||||
<div
|
||||
className={`flex h-fit flex-row items-center justify-start gap-1`}
|
||||
>
|
||||
{Object.entries({
|
||||
aircraft: olButtonsVisibilityAircraft,
|
||||
helicopter: olButtonsVisibilityHelicopter,
|
||||
"groundunit-sam": olButtonsVisibilityGroundunitSam,
|
||||
groundunit: olButtonsVisibilityGroundunit,
|
||||
navyunit: olButtonsVisibilityNavyunit,
|
||||
airbase: olButtonsVisibilityAirbase,
|
||||
dead: faSkull,
|
||||
}).map((entry) => {
|
||||
return (
|
||||
<OlRoundStateButton
|
||||
key={entry[0]}
|
||||
onClick={() => {
|
||||
getApp().getMap().setHiddenType(entry[0], !mapHiddenTypes[entry[0]]);
|
||||
}}
|
||||
checked={!mapHiddenTypes[entry[0]]}
|
||||
icon={entry[1]}
|
||||
tooltip={"Hide/show " + entry[0] + " units"}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
|
||||
<OlLabelToggle toggled={false} leftLabel={"Live"} rightLabel={"Map"} onClick={() => {}}></OlLabelToggle>
|
||||
<OlStateButton checked={false} icon={faCamera} onClick={() => {}} tooltip="Activate/deactivate camera plugin" />
|
||||
<OlDropdown label={mapSource} className="w-60">
|
||||
{mapSources.map((source) => {
|
||||
return (
|
||||
<OlDropdownItem key={source} onClick={() => getApp().getMap().setLayerName(source)}>
|
||||
<div className="truncate">{source}</div>
|
||||
</OlDropdownItem>
|
||||
);
|
||||
})}
|
||||
</OlDropdown>
|
||||
`}
|
||||
>
|
||||
<span className="my-auto font-bold">BLUE Commander ({commandModeOptions.spawnPoints.blue} points)</span>
|
||||
</div>
|
||||
{!scrolledRight && (
|
||||
<FaChevronRight
|
||||
className={`
|
||||
absolute right-0 h-full w-6 rounded-lg px-2 py-3.5 text-gray-200
|
||||
dark:bg-olympus-900
|
||||
`}
|
||||
/>
|
||||
)}
|
||||
</nav>
|
||||
)}
|
||||
<div className={`flex h-fit flex-row items-center justify-start gap-1`}>
|
||||
<OlLockStateButton
|
||||
checked={!mapOptions.protectDCSUnits}
|
||||
onClick={() => {
|
||||
getApp().getMap().setOption("protectDCSUnits", !mapOptions.protectDCSUnits);
|
||||
}}
|
||||
tooltip="Lock/unlock protected units (from scripted mission)"
|
||||
/>
|
||||
<OlRoundStateButton
|
||||
checked={audioEnabled}
|
||||
onClick={() => {
|
||||
audioEnabled ? getApp().getAudioManager().stop() : getApp().getAudioManager().start();
|
||||
setAudioEnabled(!audioEnabled);
|
||||
}}
|
||||
tooltip="Enable/disable audio and radio backend"
|
||||
icon={faVolumeHigh}
|
||||
/>
|
||||
</div>
|
||||
<div className={`h-8 w-0 border-l-[2px] border-gray-700`}></div>
|
||||
<div className={`flex h-fit flex-row items-center justify-start gap-1`}>
|
||||
{Object.entries({
|
||||
human: olButtonsVisibilityHuman,
|
||||
olympus: olButtonsVisibilityOlympus,
|
||||
dcs: olButtonsVisibilityDcs,
|
||||
}).map((entry) => {
|
||||
return (
|
||||
<OlRoundStateButton
|
||||
key={entry[0]}
|
||||
onClick={() => {
|
||||
getApp().getMap().setHiddenType(entry[0], !mapHiddenTypes[entry[0]]);
|
||||
}}
|
||||
checked={!mapHiddenTypes[entry[0]]}
|
||||
icon={entry[1]}
|
||||
tooltip={"Hide/show " + entry[0] + " units"}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
<div className={`h-8 w-0 border-l-[2px] border-gray-700`}></div>
|
||||
<div className={`flex h-fit flex-row items-center justify-start gap-1`}>
|
||||
<OlRoundStateButton
|
||||
onClick={() => getApp().getMap().setHiddenType("blue", !mapHiddenTypes["blue"])}
|
||||
checked={!mapHiddenTypes["blue"]}
|
||||
icon={faFlag}
|
||||
className={"!text-blue-500"}
|
||||
tooltip={"Hide/show blue units"}
|
||||
/>
|
||||
<OlRoundStateButton
|
||||
onClick={() => getApp().getMap().setHiddenType("red", !mapHiddenTypes["red"])}
|
||||
checked={!mapHiddenTypes["red"]}
|
||||
icon={faFlag}
|
||||
className={"!text-red-500"}
|
||||
tooltip={"Hide/show red units"}
|
||||
/>
|
||||
<OlRoundStateButton
|
||||
onClick={() => getApp().getMap().setHiddenType("neutral", !mapHiddenTypes["neutral"])}
|
||||
checked={!mapHiddenTypes["neutral"]}
|
||||
icon={faFlag}
|
||||
className={"!text-gray-500"}
|
||||
tooltip={"Hide/show neutral units"}
|
||||
/>
|
||||
</div>
|
||||
<div className={`h-8 w-0 border-l-[2px] border-gray-700`}></div>
|
||||
<div className={`flex h-fit flex-row items-center justify-start gap-1`}>
|
||||
{Object.entries({
|
||||
aircraft: olButtonsVisibilityAircraft,
|
||||
helicopter: olButtonsVisibilityHelicopter,
|
||||
"groundunit-sam": olButtonsVisibilityGroundunitSam,
|
||||
groundunit: olButtonsVisibilityGroundunit,
|
||||
navyunit: olButtonsVisibilityNavyunit,
|
||||
airbase: olButtonsVisibilityAirbase,
|
||||
dead: faSkull,
|
||||
}).map((entry) => {
|
||||
return (
|
||||
<OlRoundStateButton
|
||||
key={entry[0]}
|
||||
onClick={() => {
|
||||
getApp().getMap().setHiddenType(entry[0], !mapHiddenTypes[entry[0]]);
|
||||
}}
|
||||
checked={!mapHiddenTypes[entry[0]]}
|
||||
icon={entry[1]}
|
||||
tooltip={"Hide/show " + entry[0] + " units"}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
|
||||
<OlLabelToggle
|
||||
toggled={mapOptions.cameraPluginMode === "live"}
|
||||
leftLabel={"Live"}
|
||||
rightLabel={"Map"}
|
||||
onClick={() => {
|
||||
getApp()
|
||||
.getMap()
|
||||
.setOption("cameraPluginMode", mapOptions.cameraPluginMode === "live" ? "map" : "live");
|
||||
}}
|
||||
></OlLabelToggle>
|
||||
<OlStateButton
|
||||
checked={mapOptions.cameraPluginEnabled}
|
||||
icon={faCamera}
|
||||
onClick={() => {
|
||||
getApp().getMap().setOption("cameraPluginEnabled", !mapOptions.cameraPluginEnabled);
|
||||
}}
|
||||
tooltip="Activate/deactivate camera plugin"
|
||||
/>
|
||||
<OlDropdown label={mapSource} className="w-60">
|
||||
{mapSources.map((source) => {
|
||||
return (
|
||||
<OlDropdownItem key={source} onClick={() => getApp().getMap().setLayerName(source)}>
|
||||
<div className="truncate">{source}</div>
|
||||
</OlDropdownItem>
|
||||
);
|
||||
})}
|
||||
</OlDropdown>
|
||||
</div>
|
||||
{!scrolledRight && (
|
||||
<FaChevronRight
|
||||
className={`
|
||||
absolute right-0 h-full w-6 rounded-lg px-2 py-3.5 text-gray-200
|
||||
dark:bg-olympus-900
|
||||
`}
|
||||
/>
|
||||
)}
|
||||
</nav>
|
||||
);
|
||||
}
|
||||
|
||||
53
frontend/react/src/ui/panels/hotgroupsbar.tsx
Normal file
53
frontend/react/src/ui/panels/hotgroupsbar.tsx
Normal file
@@ -0,0 +1,53 @@
|
||||
import React, { useEffect, useState } from "react";
|
||||
import { AppStateChangedEvent, ContextActionChangedEvent, HideMenuEvent, HotgroupsChangedEvent, InfoPopupEvent } 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";
|
||||
|
||||
export function HotGroupBar(props: {}) {
|
||||
const [hotgroups, setHotgroups] = useState({} as { [key: number]: number });
|
||||
const [appState, setAppState] = useState(OlympusState.NOT_INITIALIZED);
|
||||
const [menuHidden, setMenuHidden] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
AppStateChangedEvent.on((state, subState) => setAppState(state));
|
||||
HideMenuEvent.on((hidden) => setMenuHidden(hidden));
|
||||
HotgroupsChangedEvent.on((hotgroups) => setHotgroups({ ...hotgroups }));
|
||||
}, []);
|
||||
|
||||
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)]
|
||||
`}
|
||||
>
|
||||
{Object.entries(hotgroups).map(([hotgroup, counter]) => {
|
||||
return (
|
||||
<div className="flex flex-col content-center gap-2">
|
||||
<div
|
||||
className={`
|
||||
mx-auto aspect-square rotate-45 rounded-sm bg-olympus-900
|
||||
text-center text-xs font-bold text-gray-200
|
||||
`}
|
||||
>
|
||||
<div className="relative -rotate-45">{hotgroup}</div>
|
||||
</div>
|
||||
|
||||
<OlStateButton checked={false} onClick={() => {getApp().getUnitsManager().selectUnitsByHotgroup(parseInt(hotgroup))}} tooltip="">
|
||||
<span
|
||||
className={`text-sm text-white`}
|
||||
>
|
||||
{counter}
|
||||
</span>
|
||||
</OlStateButton>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -1,5 +1,5 @@
|
||||
import React, { useEffect, useState } from "react";
|
||||
import { AppStateChangedEvent, ContextActionChangedEvent, InfoPopupEvent } from "../../events";
|
||||
import { AppStateChangedEvent, ContextActionChangedEvent, HideMenuEvent, InfoPopupEvent } from "../../events";
|
||||
import { OlympusState } from "../../constants/constants";
|
||||
import { ContextAction } from "../../unit/contextaction";
|
||||
|
||||
@@ -7,17 +7,19 @@ export function InfoBar(props: {}) {
|
||||
const [messages, setMessages] = useState([] as string[]);
|
||||
const [appState, setAppState] = useState(OlympusState.NOT_INITIALIZED);
|
||||
const [contextAction, setContextAction] = useState(null as ContextAction | null);
|
||||
const [menuHidden, setMenuHidden] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
InfoPopupEvent.on((messages) => setMessages([...messages]));
|
||||
AppStateChangedEvent.on((state, subState) => setAppState(state));
|
||||
ContextActionChangedEvent.on((contextAction) => setContextAction(contextAction));
|
||||
HideMenuEvent.on((hidden) => setMenuHidden(hidden));
|
||||
}, []);
|
||||
|
||||
let topString = "";
|
||||
if (appState === OlympusState.UNIT_CONTROL) {
|
||||
if (contextAction === null) {
|
||||
topString = "top-32";
|
||||
topString = "top-36";
|
||||
} else {
|
||||
topString = "top-48";
|
||||
}
|
||||
@@ -27,19 +29,21 @@ export function InfoBar(props: {}) {
|
||||
|
||||
return (
|
||||
<div
|
||||
data-menuhidden={menuHidden || appState === OlympusState.IDLE}
|
||||
className={`
|
||||
absolute left-[50%]
|
||||
data-[menuhidden='false']:translate-x-[calc(200px-50%+2rem)]
|
||||
data-[menuhidden='true']:translate-x-[calc(-50%+2rem)]
|
||||
${topString}
|
||||
flex w-[400px] max-w-[80%] translate-x-[calc(-50%+2rem)]
|
||||
`}
|
||||
>
|
||||
{messages.map((message, idx) => {
|
||||
{messages.slice(Math.max(0, messages.length - 4), Math.max(0, messages.length)).map((message, idx) => {
|
||||
return (
|
||||
<div
|
||||
className={`
|
||||
absolute left-0 w-full gap-2 rounded-md bg-olympus-800/90 px-4
|
||||
py-2 text-center text-sm text-white backdrop-blur-lg
|
||||
backdrop-grayscale
|
||||
absolute w-fit translate-x-[-50%] gap-2 text-nowrap rounded-full
|
||||
bg-olympus-800/90 px-4 py-2 text-center text-sm text-white
|
||||
shadow-md backdrop-blur-lg backdrop-grayscale
|
||||
`}
|
||||
style={{ top: `${idx * 20}px` }}
|
||||
>
|
||||
|
||||
@@ -199,35 +199,49 @@ export function OptionsMenu(props: { open: boolean; onClose: () => void; childre
|
||||
</kbd>
|
||||
</div>
|
||||
|
||||
{/*
|
||||
<hr className="w-auto m-2 my-1 bg-gray-700 border-[1px] dark:border-olympus-500"></hr>
|
||||
<div className="flex flex-col content-center items-start justify-between p-2 gap-2">
|
||||
|
||||
<hr className={`
|
||||
m-2 my-1 w-auto border-[1px] bg-gray-700
|
||||
dark:border-olympus-500
|
||||
`}></hr>
|
||||
<div className={`
|
||||
flex flex-col content-center items-start justify-between gap-2 p-2
|
||||
`}>
|
||||
<div className="flex flex-col">
|
||||
<span className="font-normal dark:text-white">DCS Camera Zoom Scaling</span>
|
||||
<span className="dark:text-blue-500 font-bold"> x5
|
||||
</span>
|
||||
<span className={`
|
||||
font-normal
|
||||
dark:text-white
|
||||
`}>DCS Camera Zoom Scaling</span>
|
||||
|
||||
</div>
|
||||
<OlRangeSlider
|
||||
onChange={() => { }}
|
||||
value={5}
|
||||
min={1}
|
||||
max={10}
|
||||
step={2}
|
||||
onChange={(ev) => {
|
||||
getApp().getMap().setOption("cameraPluginRatio", parseInt(ev.target.value))
|
||||
}}
|
||||
value={mapOptions.cameraPluginRatio}
|
||||
min={0}
|
||||
max={100}
|
||||
step={1}
|
||||
/>
|
||||
</div>
|
||||
<div className="flex flex-col content-center items-start justify-between p-2 gap-2">
|
||||
<span className="font-normal dark:text-white">DCS Camera Port</span>
|
||||
<div className={`
|
||||
flex flex-col content-center items-start justify-between gap-2 p-2
|
||||
`}>
|
||||
<span className={`
|
||||
font-normal
|
||||
dark:text-white
|
||||
`}>DCS Camera Port</span>
|
||||
<div className="flex">
|
||||
<OlNumberInput
|
||||
value={3004}
|
||||
value={mapOptions.cameraPluginPort}
|
||||
min={0}
|
||||
max={9999}
|
||||
onDecrease={() => { }}
|
||||
onIncrease={() => { }}
|
||||
onChange={(ev) => { }}
|
||||
onDecrease={() => { getApp().getMap().setOption("cameraPluginPort", mapOptions.cameraPluginPort - 1) }}
|
||||
onIncrease={() => { getApp().getMap().setOption("cameraPluginPort", mapOptions.cameraPluginPort + 1) }}
|
||||
onChange={(ev) => { getApp().getMap().setOption("cameraPluginPort", ev.target.value)}}
|
||||
/>
|
||||
</div>
|
||||
</div> */}
|
||||
</div>
|
||||
</div>
|
||||
</Menu>
|
||||
);
|
||||
|
||||
@@ -7,7 +7,7 @@ import { CONTEXT_ACTION_COLORS } from "../../constants/constants";
|
||||
import { FaInfoCircle } from "react-icons/fa";
|
||||
import { FaChevronLeft, FaChevronRight } from "react-icons/fa6";
|
||||
import { OlympusState } from "../../constants/constants";
|
||||
import { AppStateChangedEvent, ContextActionChangedEvent, ContextActionSetChangedEvent } from "../../events";
|
||||
import { AppStateChangedEvent, ContextActionChangedEvent, ContextActionSetChangedEvent, HideMenuEvent } from "../../events";
|
||||
|
||||
export function UnitControlBar(props: {}) {
|
||||
const [appState, setAppState] = useState(OlympusState.NOT_INITIALIZED);
|
||||
@@ -15,6 +15,7 @@ export function UnitControlBar(props: {}) {
|
||||
const [contextAction, setContextAction] = useState(null as ContextAction | null);
|
||||
const [scrolledLeft, setScrolledLeft] = useState(true);
|
||||
const [scrolledRight, setScrolledRight] = useState(false);
|
||||
const [menuHidden, setMenuHidden] = useState(false);
|
||||
|
||||
/* Initialize the "scroll" position of the element */
|
||||
var scrollRef = useRef(null);
|
||||
@@ -26,6 +27,7 @@ export function UnitControlBar(props: {}) {
|
||||
AppStateChangedEvent.on((state, subState) => setAppState(state));
|
||||
ContextActionSetChangedEvent.on((contextActionSet) => setcontextActionSet(contextActionSet));
|
||||
ContextActionChangedEvent.on((contextAction) => setContextAction(contextAction));
|
||||
HideMenuEvent.on((hidden) => setMenuHidden(hidden));
|
||||
}, []);
|
||||
|
||||
function onScroll(el) {
|
||||
@@ -48,10 +50,13 @@ export function UnitControlBar(props: {}) {
|
||||
{appState === OlympusState.UNIT_CONTROL && contextActionSet && Object.keys(contextActionSet.getContextActions()).length > 0 && (
|
||||
<>
|
||||
<div
|
||||
data-menuhidden={menuHidden}
|
||||
className={`
|
||||
absolute left-[50%] top-16 flex max-w-[80%]
|
||||
translate-x-[calc(-50%+2rem)] gap-2 rounded-md bg-gray-200
|
||||
absolute left-[50%] top-16 flex max-w-[80%] gap-2 rounded-md
|
||||
bg-gray-200
|
||||
dark:bg-olympus-900
|
||||
data-[menuhidden='false']:translate-x-[calc(200px-50%+2rem)]
|
||||
data-[menuhidden='true']:translate-x-[calc(-50%+2rem)]
|
||||
`}
|
||||
>
|
||||
{!scrolledLeft && (
|
||||
|
||||
@@ -37,6 +37,7 @@ import { JTACMenu } from "./panels/jtacmenu";
|
||||
import { AppStateChangedEvent, MapOptionsChangedEvent } from "../events";
|
||||
import { GameMasterMenu } from "./panels/gamemastermenu";
|
||||
import { InfoBar } from "./panels/infobar";
|
||||
import { HotGroupBar } from "./panels/hotgroupsbar";
|
||||
|
||||
export type OlympusUIState = {
|
||||
mainMenuVisible: boolean;
|
||||
@@ -161,6 +162,7 @@ export function UI() {
|
||||
<MapContextMenu />
|
||||
<SideBar />
|
||||
<InfoBar />
|
||||
<HotGroupBar />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -29,6 +29,7 @@ import { ContextActionSet } from "./contextactionset";
|
||||
import {
|
||||
CommandModeOptionsChangedEvent,
|
||||
ContactsUpdatedEvent,
|
||||
HotgroupsChangedEvent,
|
||||
InfoPopupEvent,
|
||||
SelectedUnitsChangedEvent,
|
||||
SelectionClearedEvent,
|
||||
@@ -1057,7 +1058,22 @@ export class UnitsManager {
|
||||
if (units === null) units = this.getSelectedUnits();
|
||||
units.forEach((unit: Unit) => unit.setHotgroup(hotgroup));
|
||||
this.#showActionMessage(units, `added to hotgroup ${hotgroup}`);
|
||||
//(getApp().getPanelsManager().get("hotgroup") as HotgroupPanel).refreshHotgroups();
|
||||
|
||||
let hotgroups: {[key: number]: number} = {};
|
||||
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;
|
||||
}
|
||||
}
|
||||
}
|
||||
HotgroupsChangedEvent.dispatch(hotgroups)
|
||||
}
|
||||
|
||||
/** Delete the selected units
|
||||
|
||||
Reference in New Issue
Block a user