mirror of
https://github.com/Pax1601/DCSOlympus.git
synced 2025-10-29 16:56:34 +00:00
More work on AWACS mode
This commit is contained in:
parent
a92d4403d7
commit
b4841872ca
@ -36,6 +36,7 @@
|
||||
},
|
||||
"devDependencies": {
|
||||
"@eslint/js": "^9.6.0",
|
||||
"@turf/clusters": "^7.1.0",
|
||||
"@types/node": "^22.5.1",
|
||||
"@types/react": "^18.2.66",
|
||||
"@types/react-dom": "^18.2.22",
|
||||
|
||||
@ -1,5 +1,5 @@
|
||||
import { LatLng, LatLngBounds } from "leaflet";
|
||||
import { MapOptions } from "../types/types";
|
||||
import { Coalition, MapOptions } from "../types/types";
|
||||
import { CommandModeOptions } from "../interfaces";
|
||||
import { ContextAction } from "../unit/contextaction";
|
||||
import {
|
||||
@ -30,6 +30,8 @@ export const SELECT_TOLERANCE_PX = 5;
|
||||
export const SHORT_PRESS_MILLISECONDS = 200;
|
||||
export const DEBOUNCE_MILLISECONDS = 200;
|
||||
|
||||
export const TRAIL_LENGTH = 10;
|
||||
|
||||
export const UNITS_URI = "units";
|
||||
export const WEAPONS_URI = "weapons";
|
||||
export const LOGS_URI = "logs";
|
||||
@ -349,9 +351,8 @@ export const MAP_OPTIONS_DEFAULTS: MapOptions = {
|
||||
cameraPluginEnabled: false,
|
||||
cameraPluginMode: "map",
|
||||
tabletMode: false,
|
||||
showUnitBullseyes: false,
|
||||
showUnitBRAA: false,
|
||||
AWACSMode: false
|
||||
AWACSMode: false,
|
||||
AWACSCoalition: "blue"
|
||||
};
|
||||
|
||||
export const MAP_HIDDEN_TYPES_DEFAULTS = {
|
||||
|
||||
@ -130,8 +130,6 @@ export interface ObjectIconOptions {
|
||||
showSummary: boolean;
|
||||
showCallsign: boolean;
|
||||
rotateToHeading: boolean;
|
||||
showBullseyes: boolean;
|
||||
showBRAA: boolean;
|
||||
}
|
||||
|
||||
export interface GeneralSettings {
|
||||
|
||||
@ -228,8 +228,6 @@ export class Map extends L.Map {
|
||||
MapOptionsChangedEvent.on((options: MapOptions) => {
|
||||
this.getContainer().toggleAttribute("data-awacs-mode", options.AWACSMode);
|
||||
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;
|
||||
@ -242,6 +240,8 @@ export class Map extends L.Map {
|
||||
}, 500); // DCS does not always apply the altitude correctly at the first set when changing map type
|
||||
}
|
||||
|
||||
if (options.AWACSMode && this.#layerName !== "AWACS") this.setLayerName("AWACS");
|
||||
|
||||
this.updateMinimap();
|
||||
});
|
||||
|
||||
|
||||
@ -1,6 +1,5 @@
|
||||
/*** Unit marker elements ***/
|
||||
[data-object|="unit"] {
|
||||
align-items: center;
|
||||
cursor: pointer;
|
||||
display: flex;
|
||||
height: 100%;
|
||||
@ -93,17 +92,6 @@
|
||||
display: none;
|
||||
}
|
||||
|
||||
[data-awacs-mode] [data-is-selected] {
|
||||
animation: blinker 0.5s linear infinite;
|
||||
}
|
||||
|
||||
@keyframes blinker {
|
||||
50% {
|
||||
opacity: 30%;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/*** Basic colours ***/
|
||||
[data-coalition="blue"] .unit-icon svg > *:first-child {
|
||||
fill: var(--unit-background-blue);
|
||||
@ -140,6 +128,22 @@
|
||||
stroke: var(--unit-background-neutral) !important;
|
||||
}
|
||||
|
||||
[data-awacs-mode] [data-is-selected] .unit-icon svg {
|
||||
stroke: #FF0 !important;
|
||||
}
|
||||
|
||||
[data-awacs-mode] [data-is-selected] .unit-vvi {
|
||||
background-color: #FF0 !important;
|
||||
}
|
||||
|
||||
[data-awacs-mode] [data-is-selected] .unit-summary {
|
||||
color: #FF0 !important;
|
||||
}
|
||||
|
||||
[data-awacs-mode] [data-is-selected] .unit-summary::after {
|
||||
background-color: #FF0 !important;
|
||||
}
|
||||
|
||||
/*** Cursors ***/
|
||||
[data-is-dead],
|
||||
[data-object|="unit-missile"],
|
||||
@ -155,6 +159,7 @@
|
||||
line-height: normal;
|
||||
position: absolute;
|
||||
font-size: 12px;
|
||||
translate: 0px 18px;
|
||||
}
|
||||
|
||||
[data-object|="unit-groundunit"] .unit-short-label {
|
||||
@ -169,7 +174,7 @@
|
||||
display: none;
|
||||
height: var(--unit-health-height);
|
||||
position: absolute;
|
||||
translate: var(--unit-health-x) var(--unit-health-y);
|
||||
translate: var(--unit-health-x) calc(25px + var(--unit-health-y));
|
||||
width: var(--unit-health-width);
|
||||
}
|
||||
|
||||
@ -181,7 +186,7 @@
|
||||
display: none;
|
||||
height: var(--unit-fuel-height);
|
||||
position: absolute;
|
||||
translate: var(--unit-fuel-x) var(--unit-fuel-y);
|
||||
translate: var(--unit-fuel-x) calc(25px + var(--unit-fuel-y));
|
||||
width: var(--unit-fuel-width);
|
||||
}
|
||||
|
||||
@ -198,7 +203,7 @@
|
||||
display: none;
|
||||
height: fit-content;
|
||||
position: absolute;
|
||||
translate: var(--unit-ammo-x) var(--unit-ammo-y);
|
||||
translate: var(--unit-ammo-x) calc(25px + var(--unit-ammo-y));
|
||||
width: fit-content;
|
||||
}
|
||||
|
||||
@ -217,7 +222,7 @@
|
||||
flex-wrap: wrap;
|
||||
font-size: 11px;
|
||||
font-weight: bold;
|
||||
justify-content: right;
|
||||
justify-content: start;
|
||||
line-height: 12px;
|
||||
pointer-events: none;
|
||||
position: absolute;
|
||||
@ -227,24 +232,89 @@
|
||||
1px -1px 0 #000,
|
||||
-1px 1px 0 #000,
|
||||
1px 1px 0 #000;
|
||||
right: 120%;
|
||||
width: fit-content;
|
||||
width: 100px;
|
||||
translate: 80px 10px;
|
||||
}
|
||||
|
||||
[data-awacs-mode] [data-object|="unit"] .unit-summary {
|
||||
top: -40px;
|
||||
[data-awacs-mode] [data-object|="unit"] .unit-summary.cluster-north {
|
||||
translate: 50px -45px;
|
||||
}
|
||||
|
||||
[data-awacs-mode] .unit-summary::before {
|
||||
[data-awacs-mode] [data-object|="unit"] .unit-summary.cluster-north-east {
|
||||
translate: 76px -32px;
|
||||
}
|
||||
|
||||
[data-awacs-mode] [data-object|="unit"] .unit-summary.cluster-east {
|
||||
translate: 95px 7px;
|
||||
}
|
||||
|
||||
[data-awacs-mode] [data-object|="unit"] .unit-summary.cluster-south-east {
|
||||
translate: 79px 50px;
|
||||
}
|
||||
|
||||
[data-awacs-mode] [data-object|="unit"] .unit-summary.cluster-south {
|
||||
translate: 50px 63px;
|
||||
}
|
||||
|
||||
[data-awacs-mode] [data-object|="unit"] .unit-summary.cluster-south-west {
|
||||
translate: -68px 50px;
|
||||
}
|
||||
|
||||
[data-awacs-mode] [data-object|="unit"] .unit-summary.cluster-west {
|
||||
translate: -80px 7px;
|
||||
}
|
||||
|
||||
[data-awacs-mode] [data-object|="unit"] .unit-summary.cluster-north-west {
|
||||
translate: -69px -35px;
|
||||
}
|
||||
|
||||
[data-awacs-mode] .unit-summary::after {
|
||||
content: " ";
|
||||
background-color: white;
|
||||
width: 40px;
|
||||
height: 1px;
|
||||
transform: rotate(45deg);
|
||||
left: 40px;
|
||||
top: 50px;
|
||||
position: relative;
|
||||
position: absolute;
|
||||
z-index: -1;
|
||||
transform-origin: 0% 0%;
|
||||
top: 30px
|
||||
}
|
||||
|
||||
[data-awacs-mode] [data-object|="unit"] .unit-summary.cluster-north::after {
|
||||
transform: rotate(90deg);
|
||||
}
|
||||
|
||||
[data-awacs-mode] [data-object|="unit"] .unit-summary.cluster-north-east::after {
|
||||
transform: rotate(135deg);
|
||||
translate: 2px;
|
||||
}
|
||||
|
||||
[data-awacs-mode] [data-object|="unit"] .unit-summary.cluster-east::after {
|
||||
transform: rotate(180deg);
|
||||
translate: -5px -12px;
|
||||
}
|
||||
|
||||
[data-awacs-mode] [data-object|="unit"] .unit-summary.cluster-south-east::after {
|
||||
transform: rotate(225deg);
|
||||
translate: -2px -28px;
|
||||
}
|
||||
|
||||
[data-awacs-mode] [data-object|="unit"] .unit-summary.cluster-south::after {
|
||||
transform: rotate(270deg);
|
||||
translate: -0px -28px;
|
||||
}
|
||||
|
||||
[data-awacs-mode] [data-object|="unit"] .unit-summary.cluster-south-west::after {
|
||||
transform: rotate(315deg);
|
||||
translate: 90px -28px;
|
||||
}
|
||||
|
||||
[data-awacs-mode] [data-object|="unit"] .unit-summary.cluster-west::after {
|
||||
translate: 90px -12px;
|
||||
}
|
||||
|
||||
[data-awacs-mode] [data-object|="unit"] .unit-summary.cluster-north-west::after {
|
||||
transform: rotate(45deg);
|
||||
translate: 90px;
|
||||
}
|
||||
|
||||
[data-hide-labels] [data-object|="unit"] .unit-summary {
|
||||
@ -262,10 +332,11 @@
|
||||
transform-origin: right;
|
||||
white-space: nowrap;
|
||||
width: 80px;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
[data-object|="unit"]:hover .unit-summary .unit-callsign {
|
||||
direction: rtl;
|
||||
direction: ltr;
|
||||
overflow: visible;
|
||||
}
|
||||
|
||||
@ -470,58 +541,13 @@
|
||||
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;
|
||||
|
||||
right: 120%;
|
||||
top: -7px;
|
||||
width: fit-content;
|
||||
}
|
||||
|
||||
[data-object|="unit"] .unit-bullseyes {
|
||||
display: flex;
|
||||
line-height: 12px;
|
||||
flex-wrap: wrap;
|
||||
row-gap: 2px;
|
||||
}
|
||||
|
||||
[data-hide-labels] [data-object|="unit"] .unit-tactical {
|
||||
.unit-bullseye, .unit-braa {
|
||||
display: none;
|
||||
}
|
||||
|
||||
[data-hide-bullseyes] [data-object|="unit"] .unit-bullseyes {
|
||||
display: none;
|
||||
}
|
||||
|
||||
[data-hide-BRAA] [data-object|="unit"] .unit-braa {
|
||||
display: none;
|
||||
}
|
||||
|
||||
[data-object|="unit"] .unit-bullseyes {
|
||||
text-shadow:
|
||||
-1px -1px 0 #000,
|
||||
1px -1px 0 #000,
|
||||
-1px 1px 0 #000,
|
||||
1px 1px 0 #000;
|
||||
}
|
||||
|
||||
[data-object|="unit"] .unit-braa {
|
||||
text-shadow:
|
||||
-1px -1px 0 #000,
|
||||
1px -1px 0 #000,
|
||||
-1px 1px 0 #000,
|
||||
1px 1px 0 #000;
|
||||
[data-awacs-mode] .unit-bullseye,
|
||||
[data-awacs-mode] .unit-braa {
|
||||
display: inline;
|
||||
}
|
||||
|
||||
[data-awacs-mode] [data-object|="unit"] .unit-selected-spotlight,
|
||||
|
||||
@ -56,8 +56,7 @@ export class TemporaryUnitMarker extends CustomMarker {
|
||||
var unitIcon = document.createElement("div");
|
||||
unitIcon.classList.add("unit-icon");
|
||||
var img = document.createElement("img");
|
||||
|
||||
img.src = `/vite/images/units/${blueprint.markerFile ?? blueprint.category}.svg`;
|
||||
img.src = `/vite/images/units/map/${getApp().getMap().getOptions().AWACSMode ? "awacs" : "normal"}/${this.#coalition}/${blueprint.markerFile ?? blueprint.category}.svg`;
|
||||
img.onload = () => SVGInjector(img);
|
||||
unitIcon.appendChild(img);
|
||||
unitIcon.toggleAttribute("data-rotate-to-heading", false);
|
||||
|
||||
@ -14,7 +14,7 @@ import {
|
||||
reactionsToThreat,
|
||||
} from "../constants/constants";
|
||||
import { AirbasesData, BullseyesData, CommandModeOptions, GeneralSettings, MissionData, Radio, ServerRequestOptions, ServerStatus, TACAN } from "../interfaces";
|
||||
import { ServerStatusUpdatedEvent } from "../events";
|
||||
import { MapOptionsChangedEvent, ServerStatusUpdatedEvent } from "../events";
|
||||
|
||||
export class ServerManager {
|
||||
#connected: boolean = false;
|
||||
@ -28,6 +28,7 @@ export class ServerManager {
|
||||
#serverIsPaused: boolean = false;
|
||||
#intervals: number[] = [];
|
||||
#requests: { [key: string]: XMLHttpRequest } = {};
|
||||
#updateMode = "normal"; // normal or awacs
|
||||
|
||||
constructor() {
|
||||
this.#lastUpdateTimes[UNITS_URI] = Date.now();
|
||||
@ -44,6 +45,16 @@ export class ServerManager {
|
||||
},
|
||||
code: "Enter"
|
||||
})
|
||||
|
||||
MapOptionsChangedEvent.on((mapOptions) => {
|
||||
if (this.#updateMode === "normal" && mapOptions.AWACSMode) {
|
||||
this.#updateMode = "awacs";
|
||||
this.startUpdate();
|
||||
} else if (this.#updateMode === "awacs" && !mapOptions.AWACSMode) {
|
||||
this.#updateMode = "normal";
|
||||
this.startUpdate();
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
setUsername(newUsername: string) {
|
||||
@ -566,7 +577,7 @@ export class ServerManager {
|
||||
return time;
|
||||
}, false);
|
||||
}
|
||||
}, 250)
|
||||
}, this.#updateMode === "normal"? 250: 2000)
|
||||
);
|
||||
|
||||
this.#intervals.push(
|
||||
@ -577,7 +588,7 @@ export class ServerManager {
|
||||
return time;
|
||||
}, false);
|
||||
}
|
||||
}, 250)
|
||||
}, this.#updateMode === "normal"? 250: 2000)
|
||||
);
|
||||
|
||||
this.#intervals.push(
|
||||
@ -590,7 +601,7 @@ export class ServerManager {
|
||||
}, true);
|
||||
}
|
||||
},
|
||||
this.getServerIsPaused() ? 500 : 5000
|
||||
5000
|
||||
)
|
||||
);
|
||||
|
||||
|
||||
@ -26,9 +26,8 @@ export type MapOptions = {
|
||||
cameraPluginEnabled: boolean;
|
||||
cameraPluginMode: string;
|
||||
tabletMode: boolean;
|
||||
showUnitBullseyes: boolean;
|
||||
showUnitBRAA: boolean;
|
||||
AWACSMode: boolean;
|
||||
AWACSCoalition: Coalition;
|
||||
};
|
||||
|
||||
export type MapHiddenTypes = {
|
||||
|
||||
@ -7,43 +7,67 @@ import {
|
||||
BullseyesDataChanged,
|
||||
HotgroupsChangedEvent,
|
||||
MapOptionsChangedEvent,
|
||||
UnitUpdatedEvent,
|
||||
} 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";
|
||||
import { Bullseye } from "../../mission/bullseye";
|
||||
import { coalitionToEnum, computeBearingRangeString, mToFt, rad2deg } from "../../other/utils";
|
||||
import { bearing, coalitionToEnum, computeBearingRangeString, mToFt, rad2deg } from "../../other/utils";
|
||||
|
||||
const trackStrings = ["North", "North-East", "East", "South-East", "South", "South-West", "West", "North-West"]
|
||||
const trackStrings = ["North", "North-East", "East", "South-East", "South", "South-West", "West", "North-West", "North"]
|
||||
const relTrackStrings = ["hot", "flank right", "beam right", "cold", "cold", "cold", "beam left", "flank left", "hot"]
|
||||
|
||||
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);
|
||||
const [bullseyes, setBullseyes] = useState(null as null | { [name: string]: Bullseye });
|
||||
const [refreshTime, setRefreshTime] = useState(0);
|
||||
|
||||
useEffect(() => {
|
||||
MapOptionsChangedEvent.on((mapOptions) => setMapOptions({ ...mapOptions }));
|
||||
HotgroupsChangedEvent.on((hotgroups) => setHotgroups({ ...hotgroups }));
|
||||
AWACSReferenceUnitChangedEvent.on((unit) => setReferenceUnit(unit));
|
||||
AWACSReferenceUnitChangedEvent.on((unit) => setReferenceUnit(unit));
|
||||
BullseyesDataChanged.on((bullseyes) => setBullseyes(bullseyes));
|
||||
UnitUpdatedEvent.on((unit) => setRefreshTime(Date.now()));
|
||||
}, []);
|
||||
|
||||
const activeGroups = Object.values(hotgroups).filter((hotgroup) => {
|
||||
return hotgroup.every((unit) => unit.getCoalition() !== coalition);
|
||||
});
|
||||
const activeGroups = Object.values(getApp()?.getUnitsManager().computeClusters((unit) => unit.getCoalition() !== mapOptions.AWACSCoalition, 6) ?? {});
|
||||
|
||||
/*Object.values(hotgroups).filter((hotgroup) => {
|
||||
return hotgroup.every((unit) => unit.getCoalition() !== mapOptions.AWACSCoalition);
|
||||
});*/
|
||||
|
||||
let readout: string[] = [];
|
||||
|
||||
if (bullseyes) {
|
||||
if (referenceUnit) {
|
||||
readout.push(`$`);
|
||||
readout.push(`${callsign}, ${activeGroups.length} group${activeGroups.length > 1 ? "s": ""}`);
|
||||
readout.push(
|
||||
...activeGroups.map((group, idx) => {
|
||||
let order = "th";
|
||||
if (idx == 0) order = "st";
|
||||
else if (idx == 1) order = "nd";
|
||||
else if (idx == 2) order = "rd";
|
||||
|
||||
let trackDegs = bearing(group[0].getPosition().lat, group[0].getPosition().lng, referenceUnit.getPosition().lat, referenceUnit.getPosition().lng) - rad2deg(group[0].getTrack())
|
||||
if (trackDegs < 0) trackDegs += 360
|
||||
if (trackDegs > 360) trackDegs -= 360
|
||||
let trackIndex = Math.round(trackDegs / 45)
|
||||
|
||||
let groupLine = `${activeGroups.length > 1? (idx + 1 + "" + order + " group"): "Single group"} bullseye ${computeBearingRangeString(bullseyes[coalitionToEnum(mapOptions.AWACSCoalition)].getLatLng(), group[0].getPosition()).replace("/", " ")}, ${ (mToFt(group[0].getPosition().alt ?? 0) / 1000).toFixed()} thousand, ${relTrackStrings[trackIndex]}`;
|
||||
|
||||
if (group.find((unit) => unit.getCoalition() === "neutral")) groupLine += ", bogey"
|
||||
else groupLine += ", hostile"
|
||||
|
||||
return groupLine;
|
||||
})
|
||||
);
|
||||
} else {
|
||||
readout.push(`${callsign}, ${activeGroups.length} group${activeGroups.length > 1 && "s"}`);
|
||||
readout.push(`${callsign}, ${activeGroups.length} group${activeGroups.length > 1 ? "s": ""}`);
|
||||
readout.push(
|
||||
...activeGroups.map((group, idx) => {
|
||||
let order = "th";
|
||||
@ -55,15 +79,17 @@ export function AWACSMenu(props: { open: boolean; onClose: () => void; children?
|
||||
if (trackDegs < 0) trackDegs += 360
|
||||
let trackIndex = Math.round(trackDegs / 45)
|
||||
|
||||
let groupLine = `${idx + 1}${order} group bullseye ${computeBearingRangeString(bullseyes[coalitionToEnum(coalition)].getLatLng(), group[0].getPosition()).replace("/", " ")}, ${ (mToFt(group[0].getPosition().alt ?? 0) / 1000).toFixed()} thousand, track ${trackStrings[trackIndex]}`;
|
||||
let groupLine = `${activeGroups.length > 1? (idx + 1 + "" + order + " group"): "Single group"} bullseye ${computeBearingRangeString(bullseyes[coalitionToEnum(mapOptions.AWACSCoalition)].getLatLng(), group[0].getPosition()).replace("/", " ")}, ${ (mToFt(group[0].getPosition().alt ?? 0) / 1000).toFixed()} thousand, track ${trackStrings[trackIndex]}`;
|
||||
|
||||
if (group.find((unit) => unit.getCoalition() === "neutral")) groupLine += ", bogey"
|
||||
else groupLine += ", hostile"
|
||||
|
||||
return groupLine;
|
||||
})
|
||||
);
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
return (
|
||||
<Menu title={"AWACS Tools"} open={props.open} onClose={props.onClose} showBackButton={false} canBeHidden={true}>
|
||||
<div
|
||||
@ -75,14 +101,11 @@ export function AWACSMenu(props: { open: boolean; onClose: () => void; children?
|
||||
<>
|
||||
<div className="flex content-center gap-4">
|
||||
<FaQuestionCircle
|
||||
className={`
|
||||
my-auto min-h-5 min-w-5 text-sm text-gray-500
|
||||
`}
|
||||
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 enemy unit hotgroups to automatically create picture calls to read on radio for your CAP.</p>
|
||||
<p>3 Optionally, set a friendly unit as reference by right clicking on it and selecting "Set AWACS reference" to create tactical calls.</p>
|
||||
<p>2 Optionally, set a friendly unit as reference by right clicking on it and selecting "Set AWACS reference" to create tactical calls.</p>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex gap-4">
|
||||
@ -101,11 +124,11 @@ export function AWACSMenu(props: { open: boolean; onClose: () => void; children?
|
||||
></input>
|
||||
<OlCoalitionToggle
|
||||
onClick={() => {
|
||||
coalition === "blue" && setCoalition("neutral");
|
||||
coalition === "neutral" && setCoalition("red");
|
||||
coalition === "red" && setCoalition("blue");
|
||||
mapOptions.AWACSCoalition === "blue" && getApp().getMap().setOption("AWACSCoalition", "neutral");
|
||||
mapOptions.AWACSCoalition === "neutral" && getApp().getMap().setOption("AWACSCoalition", "red");
|
||||
mapOptions.AWACSCoalition === "red" && getApp().getMap().setOption("AWACSCoalition","blue");
|
||||
}}
|
||||
coalition={coalition}
|
||||
coalition={mapOptions.AWACSCoalition}
|
||||
/>
|
||||
</div>
|
||||
<div className="flex flex-col gap-2">
|
||||
@ -118,24 +141,6 @@ export function AWACSMenu(props: { open: boolean; onClose: () => void; children?
|
||||
/>{" "}
|
||||
Enable AWACS map mode
|
||||
</div>
|
||||
<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 flex flex-col gap-2">
|
||||
{activeGroups.length == 0 ? (
|
||||
|
||||
@ -42,6 +42,7 @@ import {
|
||||
ContextActions,
|
||||
ContextActionTarget,
|
||||
SHORT_PRESS_MILLISECONDS,
|
||||
TRAIL_LENGTH,
|
||||
} from "../constants/constants";
|
||||
import { DataExtractor } from "../server/dataextractor";
|
||||
import { Weapon } from "../weapon/weapon";
|
||||
@ -62,6 +63,8 @@ import {
|
||||
UnitUpdatedEvent,
|
||||
} from "../events";
|
||||
|
||||
const bearingStrings = ["north", "north-east", "east", "south-east", "south", "south-west", "west", "north-west", "north"];
|
||||
|
||||
var pathIcon = new Icon({
|
||||
iconUrl: "/vite/images/markers/marker-icon.png",
|
||||
shadowUrl: "/vite/images/markers/marker-shadow.png",
|
||||
@ -157,6 +160,8 @@ export abstract class Unit extends CustomMarker {
|
||||
#targetPositionPolyline: Polyline;
|
||||
#hotgroup: number | null = null;
|
||||
#detectionMethods: number[] = [];
|
||||
#trailPositions: LatLng[] = [];
|
||||
#trailPolylines: Polyline[] = [];
|
||||
|
||||
/* Inputs timers */
|
||||
#debounceTimeout: number | null = null;
|
||||
@ -466,6 +471,8 @@ export abstract class Unit extends CustomMarker {
|
||||
this.#hasTask = dataExtractor.extractBool();
|
||||
break;
|
||||
case DataIndexes.position:
|
||||
this.#trailPositions.unshift(this.#position);
|
||||
this.#trailPositions.splice(TRAIL_LENGTH);
|
||||
this.#position = dataExtractor.extractLatLng();
|
||||
updateMarker = true;
|
||||
break;
|
||||
@ -902,7 +909,7 @@ export abstract class Unit extends CustomMarker {
|
||||
if (this.belongsToCommandedCoalition() || this.getDetectionMethods().some((value) => [VISUAL, OPTIC].includes(value)))
|
||||
marker = this.getBlueprint()?.markerFile ?? this.getDefaultMarker();
|
||||
else marker = "aircraft";
|
||||
img.src = `/vite/images/units/map/${getApp().getMap().getOptions().AWACSMode? 'awacs': 'normal'}/${this.getCoalition()}/${marker}.svg`;
|
||||
img.src = `/vite/images/units/map/${getApp().getMap().getOptions().AWACSMode ? "awacs" : "normal"}/${this.getCoalition()}/${marker}.svg`;
|
||||
img.onload = () => SVGInjector(img);
|
||||
unitIcon.appendChild(img);
|
||||
|
||||
@ -968,28 +975,15 @@ export abstract class Unit extends CustomMarker {
|
||||
summary.appendChild(altitude);
|
||||
summary.appendChild(speed);
|
||||
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);
|
||||
}
|
||||
var bullseye = document.createElement("div");
|
||||
bullseye.classList.add("unit-bullseye");
|
||||
summary.appendChild(bullseye);
|
||||
|
||||
if (iconOptions.showBRAA) {
|
||||
var BRAA = document.createElement("div");
|
||||
BRAA.classList.add("unit-braa");
|
||||
tactical.appendChild(BRAA);
|
||||
summary.appendChild(BRAA);
|
||||
}
|
||||
el.appendChild(tactical);
|
||||
|
||||
this.getElement()?.appendChild(el);
|
||||
}
|
||||
@ -1337,7 +1331,10 @@ export abstract class Unit extends CustomMarker {
|
||||
this.#isLeftMouseDown = true;
|
||||
this.#leftMouseDownEpoch = Date.now();
|
||||
} else if (e.originalEvent?.button === 2) {
|
||||
if (getApp().getState() === OlympusState.UNIT_CONTROL && getApp().getMap().getContextAction()?.getTarget() !== ContextActionTarget.POINT) {
|
||||
if (
|
||||
getApp().getState() === OlympusState.IDLE ||
|
||||
(getApp().getState() === OlympusState.UNIT_CONTROL && getApp().getMap().getContextAction()?.getTarget() !== ContextActionTarget.POINT)
|
||||
) {
|
||||
DomEvent.stop(e);
|
||||
DomEvent.preventDefault(e);
|
||||
e.originalEvent.stopImmediatePropagation();
|
||||
@ -1375,6 +1372,19 @@ export abstract class Unit extends CustomMarker {
|
||||
#onRightLongClick(e: any) {
|
||||
console.log(`Right long click on ${this.getUnitName()}`);
|
||||
|
||||
if (getApp().getState() === OlympusState.IDLE) {
|
||||
this.setSelected(!this.getSelected());
|
||||
|
||||
DomEvent.stop(e);
|
||||
DomEvent.preventDefault(e);
|
||||
e.originalEvent.stopImmediatePropagation();
|
||||
|
||||
window.setTimeout(() => {
|
||||
getApp().setState(OlympusState.UNIT_CONTROL, UnitControlSubState.UNIT_CONTEXT_MENU);
|
||||
UnitContextMenuRequestEvent.dispatch(this);
|
||||
}, 200);
|
||||
}
|
||||
|
||||
if (getApp().getState() === OlympusState.UNIT_CONTROL && !getApp().getMap().getContextAction()) {
|
||||
DomEvent.stop(e);
|
||||
DomEvent.preventDefault(e);
|
||||
@ -1402,6 +1412,10 @@ export abstract class Unit extends CustomMarker {
|
||||
}
|
||||
|
||||
#updateMarker() {
|
||||
/* Delete existing trails */
|
||||
this.#trailPolylines.forEach((polyline) => polyline.removeFrom(getApp().getMap()));
|
||||
this.#trailPolylines = [];
|
||||
|
||||
this.updateVisibility();
|
||||
|
||||
/* Draw the minimap marker */
|
||||
@ -1516,11 +1530,8 @@ export abstract class Unit extends CustomMarker {
|
||||
/* Set bullseyes positions */
|
||||
const bullseyes = getApp().getMissionManager().getBullseyes();
|
||||
if (Object.keys(bullseyes).length > 0) {
|
||||
computeBearingRangeString
|
||||
const blueBullseye = `${computeBearingRangeString(bullseyes[2].getLatLng(), this.getPosition())}`;
|
||||
const redBullseye = `${computeBearingRangeString(bullseyes[1].getLatLng(), this.getPosition())}`;
|
||||
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}`;
|
||||
const bullseye = `${computeBearingRangeString(bullseyes[coalitionToEnum(getApp().getMap().getOptions().AWACSCoalition)].getLatLng(), this.getPosition())}`;
|
||||
if (element.querySelector(".unit-bullseye")) (<HTMLElement>element.querySelector(".unit-bullseye")).innerText = `${bullseye}`;
|
||||
}
|
||||
|
||||
/* Set BRAA */
|
||||
@ -1534,6 +1545,35 @@ export abstract class Unit extends CustomMarker {
|
||||
/* Set vertical offset for altitude stacking */
|
||||
var pos = getApp().getMap().latLngToLayerPoint(this.getLatLng()).round();
|
||||
this.setZIndexOffset(1000 + Math.floor(this.#position.alt as number) - pos.y + (this.#highlighted || this.#selected ? 5000 : 0));
|
||||
|
||||
/* Get the cluster this unit is in to position the label correctly */
|
||||
let cluster = Object.values(getApp().getUnitsManager().getClusters()).find((cluster) => cluster.includes(this));
|
||||
if (cluster && cluster.length > 1) {
|
||||
let clusterMean = turf.centroid(turf.featureCollection(cluster.map((unit) => turf.point([unit.getPosition().lng, unit.getPosition().lat]))));
|
||||
let bearingFromCluster = bearing(
|
||||
clusterMean.geometry.coordinates[1],
|
||||
clusterMean.geometry.coordinates[0],
|
||||
this.getPosition().lat,
|
||||
this.getPosition().lng
|
||||
);
|
||||
|
||||
if (bearingFromCluster < 0) bearingFromCluster += 360;
|
||||
let trackIndex = Math.round(bearingFromCluster / 45);
|
||||
|
||||
for (let idx = 0; idx < bearingStrings.length; idx++) element?.querySelector(".unit-summary")?.classList.remove("cluster-" + bearingStrings[idx]);
|
||||
element?.querySelector(".unit-summary")?.classList.add("cluster-" + bearingStrings[trackIndex]);
|
||||
} else {
|
||||
for (let idx = 0; idx < bearingStrings.length; idx++) element?.querySelector(".unit-summary")?.classList.remove("cluster-" + bearingStrings[idx]);
|
||||
element?.querySelector(".unit-summary")?.classList.add("cluster-north-east");
|
||||
}
|
||||
|
||||
/* Draw the contact trail */
|
||||
if (getApp().getMap().getOptions().AWACSMode) {
|
||||
this.#trailPolylines = this.#trailPositions.map(
|
||||
(latlng, idx) => new Polyline([latlng, latlng], { color: "#FFFFFF", opacity: 1 - (idx + 1) / TRAIL_LENGTH })
|
||||
);
|
||||
this.#trailPolylines.forEach((polyline) => polyline.addTo(getApp().getMap()));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -1807,10 +1847,8 @@ export abstract class AirUnit extends Unit {
|
||||
showFuel: belongsToCommandedCoalition,
|
||||
showAmmo: belongsToCommandedCoalition,
|
||||
showSummary: belongsToCommandedCoalition || this.getDetectionMethods().some((value) => [VISUAL, OPTIC, RADAR, IRST, DLINK].includes(value)),
|
||||
showCallsign: belongsToCommandedCoalition,
|
||||
rotateToHeading: false,
|
||||
showBullseyes: true,
|
||||
showBRAA: true,
|
||||
showCallsign: belongsToCommandedCoalition && (!getApp().getMap().getOptions().AWACSMode || this.getHuman()),
|
||||
rotateToHeading: false
|
||||
} as ObjectIconOptions;
|
||||
}
|
||||
|
||||
@ -1898,10 +1936,8 @@ export class GroundUnit extends Unit {
|
||||
showFuel: false,
|
||||
showAmmo: false,
|
||||
showSummary: false,
|
||||
showCallsign: belongsToCommandedCoalition,
|
||||
showCallsign: belongsToCommandedCoalition && (!getApp().getMap().getOptions().AWACSMode || this.getHuman()),
|
||||
rotateToHeading: false,
|
||||
showBullseyes: false,
|
||||
showBRAA: false,
|
||||
} as ObjectIconOptions;
|
||||
}
|
||||
|
||||
@ -1969,10 +2005,8 @@ export class NavyUnit extends Unit {
|
||||
showFuel: false,
|
||||
showAmmo: false,
|
||||
showSummary: false,
|
||||
showCallsign: belongsToCommandedCoalition,
|
||||
showCallsign: belongsToCommandedCoalition && (!getApp().getMap().getOptions().AWACSMode || this.getHuman()),
|
||||
rotateToHeading: false,
|
||||
showBullseyes: false,
|
||||
showBRAA: false,
|
||||
} as ObjectIconOptions;
|
||||
}
|
||||
|
||||
|
||||
@ -1,16 +1,7 @@
|
||||
import { LatLng, LatLngBounds } from "leaflet";
|
||||
import { getApp } from "../olympusapp";
|
||||
import { Unit } from "./unit";
|
||||
import {
|
||||
areaContains,
|
||||
bearingAndDistanceToLatLng,
|
||||
deg2rad,
|
||||
getGroundElevation,
|
||||
latLngToMercator,
|
||||
mToFt,
|
||||
mercatorToLatLng,
|
||||
msToKnots,
|
||||
} from "../other/utils";
|
||||
import { AirUnit, Unit } from "./unit";
|
||||
import { areaContains, bearingAndDistanceToLatLng, deg2rad, getGroundElevation, latLngToMercator, mToFt, mercatorToLatLng, msToKnots } from "../other/utils";
|
||||
import { CoalitionPolygon } from "../map/coalitionarea/coalitionpolygon";
|
||||
import { DELETE_CYCLE_TIME, DELETE_SLOW_THRESHOLD, DataIndexes, GAME_MASTER, IADSDensities, OlympusState, UnitControlSubState } from "../constants/constants";
|
||||
import { DataExtractor } from "../server/dataextractor";
|
||||
@ -33,6 +24,8 @@ import {
|
||||
UnitSelectedEvent,
|
||||
} from "../events";
|
||||
import { UnitDatabase } from "./databases/unitdatabase";
|
||||
import * as turf from "@turf/turf";
|
||||
import * as turfC from "@turf/clusters";
|
||||
|
||||
/** The UnitsManager handles the creation, update, and control of units. Data is strictly updated by the server ONLY. This means that any interaction from the user will always and only
|
||||
* result in a command to the server, executed by means of a REST PUT request. Any subsequent change in data will be reflected only when the new data is sent back by the server. This strategy allows
|
||||
@ -51,6 +44,7 @@ export class UnitsManager {
|
||||
#unitDatabase: UnitDatabase;
|
||||
#protectionCallback: (units: Unit[]) => void = (units) => {};
|
||||
#AWACSReference: Unit | null = null;
|
||||
#clusters: {[key: number]: Unit[]} = {};
|
||||
|
||||
constructor() {
|
||||
this.#unitDatabase = new UnitDatabase();
|
||||
@ -82,7 +76,7 @@ export class UnitsManager {
|
||||
code: "KeyA",
|
||||
ctrlKey: true,
|
||||
shiftKey: false,
|
||||
altKey: false
|
||||
altKey: false,
|
||||
})
|
||||
.addShortcut("copyUnits", {
|
||||
label: "Copy units",
|
||||
@ -90,7 +84,7 @@ export class UnitsManager {
|
||||
code: "KeyC",
|
||||
ctrlKey: true,
|
||||
shiftKey: false,
|
||||
altKey: false
|
||||
altKey: false,
|
||||
})
|
||||
.addShortcut("pasteUnits", {
|
||||
label: "Paste units",
|
||||
@ -98,7 +92,7 @@ export class UnitsManager {
|
||||
code: "KeyV",
|
||||
ctrlKey: true,
|
||||
shiftKey: false,
|
||||
altKey: false
|
||||
altKey: false,
|
||||
});
|
||||
|
||||
const digits = ["Digit1", "Digit2", "Digit3", "Digit4", "Digit5", "Digit6", "Digit7", "Digit8", "Digit9"];
|
||||
@ -113,8 +107,9 @@ export class UnitsManager {
|
||||
code: code,
|
||||
shiftKey: false,
|
||||
altKey: false,
|
||||
ctrlKey: false
|
||||
}).addShortcut(`hotgroup${idx + 1}add`, {
|
||||
ctrlKey: false,
|
||||
})
|
||||
.addShortcut(`hotgroup${idx + 1}add`, {
|
||||
label: `Hotgroup ${idx + 1} (Add to)`,
|
||||
keyUpCallback: (ev: KeyboardEvent) => {
|
||||
this.addToHotgroup(parseInt(ev.code.substring(5)));
|
||||
@ -122,8 +117,9 @@ export class UnitsManager {
|
||||
code: code,
|
||||
shiftKey: true,
|
||||
altKey: false,
|
||||
ctrlKey: false
|
||||
}).addShortcut(`hotgroup${idx + 1}set`, {
|
||||
ctrlKey: false,
|
||||
})
|
||||
.addShortcut(`hotgroup${idx + 1}set`, {
|
||||
label: `Hotgroup ${idx + 1} (Set)`,
|
||||
keyUpCallback: (ev: KeyboardEvent) => {
|
||||
this.setHotgroup(parseInt(ev.code.substring(5)));
|
||||
@ -131,8 +127,9 @@ export class UnitsManager {
|
||||
code: code,
|
||||
ctrlKey: true,
|
||||
altKey: false,
|
||||
shiftKey: false
|
||||
}).addShortcut(`hotgroup${idx + 1}also`, {
|
||||
shiftKey: false,
|
||||
})
|
||||
.addShortcut(`hotgroup${idx + 1}also`, {
|
||||
label: `Hotgroup ${idx + 1} (Select also)`,
|
||||
keyUpCallback: (ev: KeyboardEvent) => {
|
||||
this.selectUnitsByHotgroup(parseInt(ev.code.substring(5)), false);
|
||||
@ -140,7 +137,7 @@ export class UnitsManager {
|
||||
code: code,
|
||||
ctrlKey: true,
|
||||
shiftKey: true,
|
||||
altKey: false
|
||||
altKey: false,
|
||||
});
|
||||
});
|
||||
|
||||
@ -280,6 +277,9 @@ export class UnitsManager {
|
||||
if (this.#units[ID].getSelected()) this.#units[ID].drawLines();
|
||||
}
|
||||
|
||||
/* Compute the base clusters */
|
||||
this.#clusters = this.computeClusters();
|
||||
|
||||
return updateTime;
|
||||
}
|
||||
|
||||
@ -1548,13 +1548,37 @@ export class UnitsManager {
|
||||
|
||||
setAWACSReference(ID) {
|
||||
this.#AWACSReference = this.#units[ID] ?? null;
|
||||
AWACSReferenceChangedEvent.dispatch(this.#AWACSReference)
|
||||
AWACSReferenceChangedEvent.dispatch(this.#AWACSReference);
|
||||
}
|
||||
|
||||
getAWACSReference() {
|
||||
return this.#AWACSReference;
|
||||
}
|
||||
|
||||
computeClusters(filter: (unit: Unit) => boolean = (unit) => true, distance: number = 5 /* km */) {
|
||||
let units = Object.values(this.#units)
|
||||
.filter((unit) => unit.getAlive() && unit instanceof AirUnit)
|
||||
.filter(filter);
|
||||
|
||||
var geojson = turf.featureCollection(units.map((unit) => turf.point([unit.getPosition().lng, unit.getPosition().lat])));
|
||||
|
||||
//@ts-ignore
|
||||
var clustered = turf.clustersDbscan(geojson, distance, { minPoints: 1 });
|
||||
|
||||
let clusters: {[key: number]: Unit[]} = {};
|
||||
clustered.features.forEach((feature, idx) => {
|
||||
if (clusters[feature.properties.cluster] === undefined)
|
||||
clusters[feature.properties.cluster] = [] as Unit[];
|
||||
clusters[feature.properties.cluster].push(units[idx]);
|
||||
})
|
||||
|
||||
return clusters;
|
||||
}
|
||||
|
||||
getClusters() {
|
||||
return this.#clusters;
|
||||
}
|
||||
|
||||
/***********************************************/
|
||||
#onUnitSelection(unit: Unit) {
|
||||
if (this.getSelectedUnits().length > 0) {
|
||||
|
||||
@ -178,7 +178,7 @@ export class Weapon extends CustomMarker {
|
||||
var unitIcon = document.createElement("div");
|
||||
unitIcon.classList.add("unit-icon");
|
||||
var img = document.createElement("img");
|
||||
img.src = `/vite/images/units/${this.getMarkerCategory()}.svg`;
|
||||
img.src = `/vite/images/units/map/${getApp().getMap().getOptions().AWACSMode ? "awacs" : "normal"}/${this.getCoalition()}/${this.getMarkerCategory()}.svg`;
|
||||
img.onload = () => SVGInjector(img);
|
||||
unitIcon.appendChild(img);
|
||||
unitIcon.toggleAttribute("data-rotate-to-heading", this.getIconOptions().rotateToHeading);
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user