Large rework of context menus for units and map

This commit is contained in:
Pax1601 2023-11-16 15:31:07 +01:00
parent 0c50141be6
commit 4a54011aac
13 changed files with 614 additions and 538 deletions

View File

@ -54,7 +54,7 @@ class DemoDataGenerator {
/*
UNCOMMENT TO TEST ALL UNITS
***************** UNCOMMENT TO TEST ALL UNITS ****************
var databases = Object.assign({}, aircraftDatabase, helicopterDatabase, groundUnitDatabase, navyUnitDatabase);
var t = Object.keys(databases).length;
@ -114,6 +114,39 @@ class DemoDataGenerator {
DEMO_UNIT_DATA[idx].position.lat += idx / 100;
DEMO_UNIT_DATA[idx].category = "GroundUnit";
DEMO_UNIT_DATA[idx].isLeader = false;
idx += 1;
DEMO_UNIT_DATA[idx] = JSON.parse(JSON.stringify(baseData));
DEMO_UNIT_DATA[idx].name = "F-14B";
DEMO_UNIT_DATA[idx].groupName = `Group-1`;
DEMO_UNIT_DATA[idx].position.lat += idx / 100;
DEMO_UNIT_DATA[idx].category = "Aircraft";
DEMO_UNIT_DATA[idx].isLeader = false;
idx += 1;
DEMO_UNIT_DATA[idx] = JSON.parse(JSON.stringify(baseData));
DEMO_UNIT_DATA[idx].name = "Infantry AK";
DEMO_UNIT_DATA[idx].groupName = `Group-2`;
DEMO_UNIT_DATA[idx].position.lat += idx / 100;
DEMO_UNIT_DATA[idx].category = "GroundUnit";
DEMO_UNIT_DATA[idx].isLeader = true;
idx += 1;
DEMO_UNIT_DATA[idx] = JSON.parse(JSON.stringify(baseData));
DEMO_UNIT_DATA[idx].name = "Infantry AK";
DEMO_UNIT_DATA[idx].groupName = `Group-3`;
DEMO_UNIT_DATA[idx].position.lat += idx / 100;
DEMO_UNIT_DATA[idx].category = "GroundUnit";
DEMO_UNIT_DATA[idx].isLeader = true;
idx += 1;
DEMO_UNIT_DATA[idx] = JSON.parse(JSON.stringify(baseData));
DEMO_UNIT_DATA[idx].name = "F-14B";
DEMO_UNIT_DATA[idx].groupName = `Group-4`;
DEMO_UNIT_DATA[idx].position.lat += idx / 100;
DEMO_UNIT_DATA[idx].category = "Aircraft";
DEMO_UNIT_DATA[idx].isLeader = true;
this.startTime = Date.now();
}

View File

@ -24,7 +24,7 @@ export class AirbaseContextMenu extends ContextMenu {
document.addEventListener("contextMenuLandAirbase", (e: any) => {
if (this.#airbase)
getApp().getUnitsManager().selectedUnitsLandAt(this.#airbase.getLatLng());
getApp().getUnitsManager().landAt(this.#airbase.getLatLng());
this.hide();
})
}
@ -111,7 +111,7 @@ export class AirbaseContextMenu extends ContextMenu {
#showSpawnMenu() {
if (this.#airbase != null) {
getApp().setActiveCoalition(this.#airbase.getCoalition());
getApp().getMap().showAirbaseSpawnMenu(this.getX(), this.getY(), this.getLatLng(), this.#airbase);
getApp().getMap().showAirbaseSpawnMenu(this.#airbase, this.getX(), this.getY(), this.getLatLng());
}
}

View File

@ -46,7 +46,7 @@ export class AirbaseSpawnContextMenu extends ContextMenu {
* @param x X screen coordinate of the top left corner of the context menu
* @param y Y screen coordinate of the top left corner of the context menu
*/
show(x: number, y: number) {
show(x: number | undefined, y: number | undefined) {
super.show(x, y, new LatLng(0, 0));
this.#aircraftSpawnMenu.setAirbase(undefined);

View File

@ -19,15 +19,15 @@ export class ContextMenu {
/** Show the contextmenu on top of the map, usually at the location where the user has clicked on it.
*
* @param x X screen coordinate of the top left corner of the context menu
* @param y Y screen coordinate of the top left corner of the context menu
* @param latlng Leaflet latlng object of the mouse click
* @param x X screen coordinate of the top left corner of the context menu. If undefined, use the old value
* @param y Y screen coordinate of the top left corner of the context menu. If undefined, use the old value
* @param latlng Leaflet latlng object of the mouse click. If undefined, use the old value
*/
show(x: number, y: number, latlng: LatLng) {
this.#latlng = latlng;
show(x: number | undefined = undefined, y: number | undefined = undefined, latlng: LatLng | undefined = undefined) {
this.#latlng = latlng ?? this.#latlng;
this.#container?.classList.toggle("hide", false);
this.#x = x;
this.#y = y;
this.#x = x ?? this.#x;
this.#y = y ?? this.#y;
this.clip();
this.getContainer()?.dispatchEvent(new Event("show"));
}

View File

@ -1,4 +1,4 @@
import { deg2rad, ftToM } from "../other/utils";
import { ContextActionSet } from "../unit/contextactionset";
import { ContextMenu } from "./contextmenu";
/** The UnitContextMenu is shown when the user rightclicks on a unit. It dynamically presents the user with possible actions to perform on the unit. */
@ -16,15 +16,19 @@ export class UnitContextMenu extends ContextMenu {
* @param options Dictionary element containing the text and tooltip of the options shown in the menu
* @param callback Callback that will be called when the user clicks on one of the options
*/
setOptions(options: { [key: string]: {text: string, tooltip: string }}, callback: CallableFunction) {
this.getContainer()?.replaceChildren(...Object.keys(options).map((key: string, idx: number) => {
const option = options[key];
setContextActions(contextActionSet: ContextActionSet) {
this.getContainer()?.replaceChildren(...Object.keys(contextActionSet.getContextActions()).map((key: string, idx: number) => {
const contextAction = contextActionSet.getContextActions()[key];
var button = document.createElement("button");
var el = document.createElement("div");
el.title = option.tooltip;
el.innerText = option.text;
el.title = contextAction.getDescription();
el.innerText = contextAction.getLabel();
el.id = key;
button.addEventListener("click", () => callback(key));
button.addEventListener("click", () => {
contextAction.executeCallback();
if (contextAction.getHideContextAfterExecution())
this.hide();
});
button.appendChild(el);
return (button);
}));

View File

@ -12,16 +12,16 @@ import { DestinationPreviewMarker } from "./markers/destinationpreviewmarker";
import { TemporaryUnitMarker } from "./markers/temporaryunitmarker";
import { ClickableMiniMap } from "./clickableminimap";
import { SVGInjector } from '@tanem/svg-injector'
import { mapLayers, mapBounds, minimapBoundaries, IDLE, COALITIONAREA_DRAW_POLYGON, visibilityControls, visibilityControlsTooltips, MOVE_UNIT, SHOW_UNIT_CONTACTS, HIDE_GROUP_MEMBERS, SHOW_UNIT_PATHS, SHOW_UNIT_TARGETS, visibilityControlsTypes, SHOW_UNIT_LABELS, SHOW_UNITS_ENGAGEMENT_RINGS, SHOW_UNITS_ACQUISITION_RINGS, HIDE_UNITS_SHORT_RANGE_RINGS, FILL_SELECTED_RING, MAP_MARKER_CONTROLS } from "../constants/constants";
import { mapLayers, mapBounds, minimapBoundaries, IDLE, COALITIONAREA_DRAW_POLYGON, MOVE_UNIT, SHOW_UNIT_CONTACTS, HIDE_GROUP_MEMBERS, SHOW_UNIT_PATHS, SHOW_UNIT_TARGETS, SHOW_UNIT_LABELS, SHOW_UNITS_ENGAGEMENT_RINGS, SHOW_UNITS_ACQUISITION_RINGS, HIDE_UNITS_SHORT_RANGE_RINGS, FILL_SELECTED_RING, MAP_MARKER_CONTROLS } from "../constants/constants";
import { TargetMarker } from "./markers/targetmarker";
import { CoalitionArea } from "./coalitionarea/coalitionarea";
import { CoalitionAreaContextMenu } from "../contextmenus/coalitionareacontextmenu";
import { DrawingCursor } from "./coalitionarea/drawingcursor";
import { AirbaseSpawnContextMenu } from "../contextmenus/airbasespawnmenu";
import { Popup } from "../popups/popup";
import { GestureHandling } from "leaflet-gesture-handling";
import { TouchBoxSelect } from "./touchboxselect";
import { DestinationPreviewHandle } from "./markers/destinationpreviewHandle";
import { ContextActionSet } from "../unit/contextactionset";
var hasTouchScreen = false;
//if ("maxTouchPoints" in navigator)
@ -322,7 +322,7 @@ export class Map extends L.Map {
return this.#mapContextMenu;
}
showUnitContextMenu(x: number, y: number, latlng: L.LatLng) {
showUnitContextMenu(x: number | undefined = undefined, y: number | undefined = undefined, latlng: L.LatLng | undefined = undefined) {
this.hideAllContextMenus();
this.#unitContextMenu.show(x, y, latlng);
}
@ -335,7 +335,7 @@ export class Map extends L.Map {
this.#unitContextMenu.hide();
}
showAirbaseContextMenu(x: number, y: number, latlng: L.LatLng, airbase: Airbase) {
showAirbaseContextMenu(airbase: Airbase, x: number | undefined = undefined, y: number | undefined = undefined, latlng: L.LatLng | undefined = undefined) {
this.hideAllContextMenus();
this.#airbaseContextMenu.show(x, y, latlng);
this.#airbaseContextMenu.setAirbase(airbase);
@ -349,7 +349,7 @@ export class Map extends L.Map {
this.#airbaseContextMenu.hide();
}
showAirbaseSpawnMenu(x: number, y: number, latlng: L.LatLng, airbase: Airbase) {
showAirbaseSpawnMenu(airbase: Airbase, x: number | undefined = undefined, y: number | undefined = undefined, latlng: L.LatLng | undefined = undefined) {
this.hideAllContextMenus();
this.#airbaseSpawnMenu.show(x, y);
this.#airbaseSpawnMenu.setAirbase(airbase);
@ -561,9 +561,9 @@ export class Map extends L.Map {
}
else if (this.#state === MOVE_UNIT) {
if (!e.originalEvent.ctrlKey) {
getApp().getUnitsManager().selectedUnitsClearDestinations();
getApp().getUnitsManager().clearDestinations();
}
getApp().getUnitsManager().selectedUnitsAddDestination(this.#computeDestinationRotation && this.#destinationRotationCenter != null ? this.#destinationRotationCenter : e.latlng, this.#shiftKey, this.#destinationGroupRotation)
getApp().getUnitsManager().addDestination(this.#computeDestinationRotation && this.#destinationRotationCenter != null ? this.#destinationRotationCenter : e.latlng, this.#shiftKey, this.#destinationGroupRotation)
this.#destinationGroupRotation = 0;
this.#destinationRotationCenter = null;
@ -611,59 +611,15 @@ export class Map extends L.Map {
if (e.originalEvent.button != 2 || e.originalEvent.ctrlKey || e.originalEvent.shiftKey)
return;
var options: { [key: string]: { text: string, tooltip: string } } = {};
const selectedUnits = getApp().getUnitsManager().getSelectedUnits();
const selectedUnitTypes = getApp().getUnitsManager().getSelectedUnitsCategories();
if (selectedUnitTypes.length === 1 && ["Aircraft", "Helicopter"].includes(selectedUnitTypes[0])) {
if (selectedUnits.every((unit: Unit) => { return unit.canLandAtPoint()}))
options["land-at-point"] = { text: "Land here", tooltip: "Land at this precise location" };
if (selectedUnits.every((unit: Unit) => { return unit.canTargetPoint()})) {
options["bomb"] = { text: "Precision bombing", tooltip: "Precision bombing of a specific point" };
options["carpet-bomb"] = { text: "Carpet bombing", tooltip: "Carpet bombing close to a point" };
}
if (Object.keys(options).length === 0)
(getApp().getPopupsManager().get("infoPopup") as Popup).setText(`Selected units can not perform point actions.`);
}
else if (selectedUnitTypes.length === 1 && ["GroundUnit", "NavyUnit"].includes(selectedUnitTypes[0])) {
if (selectedUnits.every((unit: Unit) => { return unit.canTargetPoint() })) {
options["fire-at-area"] = { text: "Fire at area", tooltip: "Fire at a large area" };
options["simulate-fire-fight"] = { text: "Simulate fire fight", tooltip: "Simulate a fire fight by shooting randomly in a certain large area" };
}
else
(getApp().getPopupsManager().get("infoPopup") as Popup).setText(`Selected units can not perform point actions.`);
}
else if(selectedUnitTypes.length > 1) {
(getApp().getPopupsManager().get("infoPopup") as Popup).setText(`Multiple unit types selected, no common actions available.`);
}
if (Object.keys(options).length > 0) {
this.showUnitContextMenu(e.originalEvent.x, e.originalEvent.y, e.latlng);
this.getUnitContextMenu().setOptions(options, (option: string) => {
this.hideUnitContextMenu();
if (option === "bomb") {
getApp().getUnitsManager().getSelectedUnits().length > 0 ? this.setState(MOVE_UNIT) : this.setState(IDLE);
getApp().getUnitsManager().selectedUnitsBombPoint(this.getMouseCoordinates());
}
else if (option === "carpet-bomb") {
getApp().getUnitsManager().getSelectedUnits().length > 0 ? this.setState(MOVE_UNIT) : this.setState(IDLE);
getApp().getUnitsManager().selectedUnitsCarpetBomb(this.getMouseCoordinates());
}
else if (option === "fire-at-area") {
getApp().getUnitsManager().getSelectedUnits().length > 0 ? this.setState(MOVE_UNIT) : this.setState(IDLE);
getApp().getUnitsManager().selectedUnitsFireAtArea(this.getMouseCoordinates());
}
else if (option === "simulate-fire-fight") {
getApp().getUnitsManager().getSelectedUnits().length > 0 ? this.setState(MOVE_UNIT) : this.setState(IDLE);
getApp().getUnitsManager().selectedUnitsSimulateFireFight(this.getMouseCoordinates());
}
else if (option === "land-at-point") {
getApp().getUnitsManager().getSelectedUnits().length > 0 ? this.setState(MOVE_UNIT) : this.setState(IDLE);
getApp().getUnitsManager().selectedUnitsLandAtPoint(this.getMouseCoordinates());
}
});
var contextActionSet = new ContextActionSet();
var units = getApp().getUnitsManager().getSelectedUnits();
units.forEach((unit: Unit) => {
unit.appendContextActions(contextActionSet, null, e.latlng);
})
if (Object.keys(contextActionSet.getContextActions()).length > 0) {
getApp().getMap().showUnitContextMenu(e.originalEvent.x, e.originalEvent.y, e.latlng);
getApp().getMap().getUnitContextMenu().setContextActions(contextActionSet);
}
}, 150);
this.#longPressHandled = false;
@ -742,6 +698,8 @@ export class Map extends L.Map {
const toggles = `["${control.toggles.join('","')}"]`;
const div = document.createElement("div");
div.className = control.protectable === true ? "protectable" : "";
// TODO: for consistency let's avoid using innerHTML. Let's create elements.
div.innerHTML = `
<button data-on-click="toggleMarkerVisibility" title="${control.tooltip}" data-on-click-params='{"types":${toggles}}'>
<img src="/resources/theme/images/buttons/${control.image}" />
@ -842,7 +800,7 @@ export class Map extends L.Map {
if (this.#destinationPreviewCursors.length == 1)
this.#destinationPreviewCursors[0].setLatLng(this.getMouseCoordinates());
else {
Object.values(getApp().getUnitsManager().selectedUnitsComputeGroupDestination(groupLatLng, this.#destinationGroupRotation)).forEach((latlng: L.LatLng, idx: number) => {
Object.values(getApp().getUnitsManager().computeGroupDestination(groupLatLng, this.#destinationGroupRotation)).forEach((latlng: L.LatLng, idx: number) => {
if (idx < this.#destinationPreviewCursors.length)
this.#destinationPreviewCursors[idx].setLatLng(this.#shiftKey ? latlng : this.getMouseCoordinates());
})

View File

@ -242,7 +242,7 @@ export class MissionManager {
}
#onAirbaseClick(e: any) {
getApp().getMap().showAirbaseContextMenu(e.originalEvent.x, e.originalEvent.y, e.latlng, e.sourceTarget);
getApp().getMap().showAirbaseContextMenu(e.sourceTarget, e.originalEvent.x, e.originalEvent.y, e.latlng);
}
#loadAirbaseChartData(callsign: string) {

View File

@ -380,9 +380,9 @@ export class OlympusApp {
if (ev.ctrlKey && ev.shiftKey)
this.getUnitsManager().selectUnitsByHotgroup(parseInt(ev.code.substring(5)), false); // "Select hotgroup X in addition to any units already selected"
else if (ev.ctrlKey && !ev.shiftKey)
this.getUnitsManager().selectedUnitsSetHotgroup(parseInt(ev.code.substring(5))); // "These selected units are hotgroup X (forget any previous membership)"
this.getUnitsManager().setHotgroup(parseInt(ev.code.substring(5))); // "These selected units are hotgroup X (forget any previous membership)"
else if (!ev.ctrlKey && ev.shiftKey)
this.getUnitsManager().selectedUnitsAddToHotgroup(parseInt(ev.code.substring(5))); // "Add (append) these units to hotgroup X (in addition to any existing members)"
this.getUnitsManager().addToHotgroup(parseInt(ev.code.substring(5))); // "Add (append) these units to hotgroup X (in addition to any existing members)"
else
this.getUnitsManager().selectUnitsByHotgroup(parseInt(ev.code.substring(5))); // "Select hotgroup X, deselect any units not in it."
},

View File

@ -10,6 +10,8 @@ import { ROEDescriptions, ROEs, altitudeIncrements, emissionsCountermeasures, em
import { ftToM, knotsToMs, mToFt, msToKnots } from "../other/utils";
import { GeneralSettings, Radio, TACAN } from "../interfaces";
import { PrimaryToolbar } from "../toolbars/primarytoolbar";
import { ContextActionSet } from "../unit/contextactionset";
import { ContextAction } from "../unit/contextaction";
export class UnitControlPanel extends Panel {
#altitudeSlider: Slider;
@ -38,32 +40,32 @@ export class UnitControlPanel extends Panel {
super(ID);
/* Unit control sliders */
this.#altitudeSlider = new Slider("altitude-slider", 0, 100, "ft", (value: number) => { getApp().getUnitsManager().selectedUnitsSetAltitude(ftToM(value)); });
this.#altitudeTypeSwitch = new Switch("altitude-type-switch", (value: boolean) => { getApp().getUnitsManager().selectedUnitsSetAltitudeType(value? "ASL": "AGL"); });
this.#altitudeSlider = new Slider("altitude-slider", 0, 100, "ft", (value: number) => { getApp().getUnitsManager().setAltitude(ftToM(value)); });
this.#altitudeTypeSwitch = new Switch("altitude-type-switch", (value: boolean) => { getApp().getUnitsManager().setAltitudeType(value? "ASL": "AGL"); });
this.#speedSlider = new Slider("speed-slider", 0, 100, "kts", (value: number) => { getApp().getUnitsManager().selectedUnitsSetSpeed(knotsToMs(value)); });
this.#speedTypeSwitch = new Switch("speed-type-switch", (value: boolean) => { getApp().getUnitsManager().selectedUnitsSetSpeedType(value? "CAS": "GS"); });
this.#speedSlider = new Slider("speed-slider", 0, 100, "kts", (value: number) => { getApp().getUnitsManager().setSpeed(knotsToMs(value)); });
this.#speedTypeSwitch = new Switch("speed-type-switch", (value: boolean) => { getApp().getUnitsManager().setSpeedType(value? "CAS": "GS"); });
/* Option buttons */
// Reversing the ROEs so that the least "aggressive" option is always on the left
this.#optionButtons["ROE"] = ROEs.slice(0).reverse().map((option: string, index: number) => {
return this.#createOptionButton(option, `roe/${option.toLowerCase()}.svg`, ROEDescriptions.slice(0).reverse()[index], () => { getApp().getUnitsManager().selectedUnitsSetROE(option); });
return this.#createOptionButton(option, `roe/${option.toLowerCase()}.svg`, ROEDescriptions.slice(0).reverse()[index], () => { getApp().getUnitsManager().setROE(option); });
}).filter((button: HTMLButtonElement, index: number) => {return ROEs[index] !== "";});
this.#optionButtons["reactionToThreat"] = reactionsToThreat.map((option: string, index: number) => {
return this.#createOptionButton(option, `threat/${option.toLowerCase()}.svg`, reactionsToThreatDescriptions[index],() => { getApp().getUnitsManager().selectedUnitsSetReactionToThreat(option); });
return this.#createOptionButton(option, `threat/${option.toLowerCase()}.svg`, reactionsToThreatDescriptions[index],() => { getApp().getUnitsManager().setReactionToThreat(option); });
});
this.#optionButtons["emissionsCountermeasures"] = emissionsCountermeasures.map((option: string, index: number) => {
return this.#createOptionButton(option, `emissions/${option.toLowerCase()}.svg`, emissionsCountermeasuresDescriptions[index],() => { getApp().getUnitsManager().selectedUnitsSetEmissionsCountermeasures(option); });
return this.#createOptionButton(option, `emissions/${option.toLowerCase()}.svg`, emissionsCountermeasuresDescriptions[index],() => { getApp().getUnitsManager().setEmissionsCountermeasures(option); });
});
this.#optionButtons["shotsScatter"] = [1, 2, 3].map((option: number, index: number) => {
return this.#createOptionButton(option.toString(), `scatter/${option.toString().toLowerCase()}.svg`, shotsScatterDescriptions[index],() => { getApp().getUnitsManager().selectedUnitsSetShotsScatter(option); });
return this.#createOptionButton(option.toString(), `scatter/${option.toString().toLowerCase()}.svg`, shotsScatterDescriptions[index],() => { getApp().getUnitsManager().setShotsScatter(option); });
});
this.#optionButtons["shotsIntensity"] = [1, 2, 3].map((option: number, index: number) => {
return this.#createOptionButton(option.toString(), `intensity/${option.toString().toLowerCase()}.svg`, shotsIntensityDescriptions[index],() => { getApp().getUnitsManager().selectedUnitsSetShotsIntensity(option); });
return this.#createOptionButton(option.toString(), `intensity/${option.toString().toLowerCase()}.svg`, shotsIntensityDescriptions[index],() => { getApp().getUnitsManager().setShotsIntensity(option); });
});
this.getElement().querySelector("#roe-buttons-container")?.append(...this.#optionButtons["ROE"]);
@ -92,17 +94,17 @@ export class UnitControlPanel extends Panel {
/* On off switch */
this.#onOffSwitch = new Switch("on-off-switch", (value: boolean) => {
getApp().getUnitsManager().selectedUnitsSetOnOff(value);
getApp().getUnitsManager().setOnOff(value);
});
/* Follow roads switch */
this.#followRoadsSwitch = new Switch("follow-roads-switch", (value: boolean) => {
getApp().getUnitsManager().selectedUnitsSetFollowRoads(value);
getApp().getUnitsManager().setFollowRoads(value);
});
/* Operate as */
this.#operateAsSwitch = new Switch("operate-as-switch", (value: boolean) => {
getApp().getUnitsManager().selectedUnitsSetOperateAs(value);
getApp().getUnitsManager().setOperateAs(value);
});
/* Advanced settings dialog */
@ -300,54 +302,32 @@ export class UnitControlPanel extends Panel {
}
#updateRapidControls() {
var options: { [key: string]: { text: string, tooltip: string, type: string } } | null = null;
var contextActionSet = new ContextActionSet();
var units = getApp().getUnitsManager().getSelectedUnits();
var selectedUnits = getApp().getUnitsManager().getSelectedUnits();
var showAltitudeChange = selectedUnits.some((unit: Unit) => {return ["Aircraft", "Helicopter"].includes(unit.getCategory());});
var showAltitudeChange = units.some((unit: Unit) => {return ["Aircraft", "Helicopter"].includes(unit.getCategory());});
this.getElement().querySelector("#climb")?.classList.toggle("hide", !showAltitudeChange);
this.getElement().querySelector("#descend")?.classList.toggle("hide", !showAltitudeChange);
/* Keep only the common "and" options, unless a single unit is selected */
selectedUnits.forEach((unit: Unit) => {
var unitOptions = unit.getActions();
if (options === null) {
options = unitOptions;
} else {
/* Delete all the "or" type options */
for (let optionKey in options) {
if (options[optionKey].type == "or") {
delete options[optionKey];
}
}
/* Options of "and" type get shown if ALL units have it */
for (let optionKey in options) {
if (!(optionKey in unitOptions)) {
delete options[optionKey];
}
}
}
});
options = options ?? {};
units.forEach((unit: Unit) => {
unit.appendContextActions(contextActionSet, null, null);
})
const rapidControlsContainer = this.getElement().querySelector("#rapid-controls") as HTMLElement;
const unitActionButtons = rapidControlsContainer.querySelectorAll(".unit-action-button");
for (let button of unitActionButtons) {
rapidControlsContainer.removeChild(button);
}
for (let option in options) {
for (let key in contextActionSet.getContextActions()) {
const contextAction = contextActionSet.getContextActions()[key];
let button = document.createElement("button");
button.title = options[option].tooltip;
button.title = contextAction.getDescription();
button.classList.add("ol-button", "unit-action-button");
button.id = option;
button.id = key;
rapidControlsContainer.appendChild(button);
button.onclick = () => {
/* Since only common actions are shown in the rapid controls, we execute it only on the first unit */
if (selectedUnits.length > 0)
selectedUnits[0].executeAction(null, option);
contextAction.executeCallback();
}
}
}

View File

@ -0,0 +1,47 @@
import { Unit } from "./unit";
export class ContextAction {
#id: string = "";
#label: string = "";
#description: string = "";
#callback: CallableFunction | null = null;
#units: Unit[] = [];
#hideContextAfterExecution: boolean = true
constructor(id: string, label: string, description: string, callback: CallableFunction, hideContextAfterExecution: boolean = true) {
this.#id = id;
this.#label = label;
this.#description = description;
this.#callback = callback;
this.#hideContextAfterExecution = hideContextAfterExecution;
}
addUnit(unit: Unit) {
this.#units.push(unit);
}
getId() {
return this.#id;
}
getLabel() {
return this.#label;
}
getDescription() {
return this.#description;
}
getCallback() {
return this.#callback;
}
executeCallback() {
if (this.#callback)
this.#callback(this.#units);
}
getHideContextAfterExecution() {
return this.#hideContextAfterExecution;
}
}

View File

@ -0,0 +1,23 @@
import { ContextAction } from "./contextaction";
import { Unit } from "./unit";
export class ContextActionSet {
#contextActions: {[key: string]: ContextAction} = {};
constructor() {
}
addContextAction(unit: Unit, id: string, label: string, description: string, callback: CallableFunction, hideContextAfterExecution: boolean = true) {
if (!(id in this.#contextActions)) {
this.#contextActions[id] = new ContextAction(id, label, description, callback, hideContextAfterExecution);
}
this.#contextActions[id].addUnit(unit);
}
getContextActions() {
return this.#contextActions;
}
}

View File

@ -13,6 +13,8 @@ import { Weapon } from '../weapon/weapon';
import { Ammo, Contact, GeneralSettings, LoadoutBlueprint, ObjectIconOptions, Offset, Radio, TACAN, UnitData } from '../interfaces';
import { RangeCircle } from "../map/rangecircle";
import { Group } from './group';
import { ContextActionSet } from './contextactionset';
import { ContextAction } from './contextaction';
var pathIcon = new Icon({
iconUrl: '/resources/theme/images/markers/marker-icon.png',
@ -212,7 +214,7 @@ export abstract class Unit extends CustomMarker {
});
}
/********************** Abstract methods *************************/
/********************** Abstract methods *************************/
/** Get the unit category string
*
* @returns string The unit category
@ -228,9 +230,8 @@ export abstract class Unit extends CustomMarker {
/** Get the actions that this unit can perform
*
* @returns Object containing the available actions
*/
abstract getActions(): {[key: string]: { text: string, tooltip: string, type: string}};
abstract appendContextActions(contextActionSet: ContextActionSet, targetUnit: Unit | null, targetPosition: LatLng | null): void;
/********************** Unit data *************************/
/** This function is called by the units manager to update all the data coming from the backend. It reads the binary raw data using a DataExtractor
@ -240,7 +241,7 @@ export abstract class Unit extends CustomMarker {
setData(dataExtractor: DataExtractor) {
/* This variable controls if the marker must be updated. This is not always true since not all variables have an effect on the marker */
var updateMarker = !getApp().getMap().hasLayer(this);
var oldIsLeader = this.#isLeader;
var datumIndex = 0;
while (datumIndex != DataIndexes.endOfData) {
@ -489,7 +490,7 @@ export abstract class Unit extends CustomMarker {
* @returns Unit[]
*/
getGroupMembers() {
if (this.#group !== null)
if (this.#group !== null)
return this.#group.getMembers().filter((unit: Unit) => { return unit != this; })
return [];
}
@ -499,7 +500,7 @@ export abstract class Unit extends CustomMarker {
* @returns Unit The leader of the group
*/
getGroupLeader() {
if (this.#group !== null)
if (this.#group !== null)
return this.#group.getLeader();
return null;
}
@ -563,7 +564,7 @@ export abstract class Unit extends CustomMarker {
var iconOptions = this.getIconOptions();
/* Generate and append elements depending on active options */
/* Generate and append elements depending on active options */
/* Velocity vector */
if (iconOptions.showVvi) {
var vvi = document.createElement("div");
@ -681,8 +682,8 @@ export abstract class Unit extends CustomMarker {
/* Hide the unit if it does not belong to the commanded coalition and it is not detected by a method that can pinpoint its location (RWR does not count) */
(!this.belongsToCommandedCoalition() && (this.#detectionMethods.length == 0 || (this.#detectionMethods.length == 1 && this.#detectionMethods[0] === RWR))) ||
/* Hide the unit if grouping is activated, the unit is not the group leader, it is not selected, and the zoom is higher than the grouping threshold */
(getApp().getMap().getVisibilityOptions()[HIDE_GROUP_MEMBERS] && !this.#isLeader && this.getCategory() == "GroundUnit" && getApp().getMap().getZoom() < GROUPING_ZOOM_TRANSITION &&
(this.belongsToCommandedCoalition() || (!this.belongsToCommandedCoalition() && this.#detectionMethods.length == 0)))) &&
(getApp().getMap().getVisibilityOptions()[HIDE_GROUP_MEMBERS] && !this.#isLeader && this.getCategory() == "GroundUnit" && getApp().getMap().getZoom() < GROUPING_ZOOM_TRANSITION &&
(this.belongsToCommandedCoalition() || (!this.belongsToCommandedCoalition() && this.#detectionMethods.length == 0)))) &&
!(this.getSelected()
);
@ -767,10 +768,6 @@ export abstract class Unit extends CustomMarker {
return this.getDatabaseEntry()?.canRearm === true;
}
canLandAtPoint() {
return this.getCategory() === "Helicopter";
}
canAAA() {
return this.getDatabaseEntry()?.canAAA === true;
}
@ -964,24 +961,6 @@ export abstract class Unit extends CustomMarker {
getApp().getServerManager().setShotsIntensity(this.ID, shotsIntensity);
}
/***********************************************/
executeAction(e: any, action: string) {
if (action === "center-map")
getApp().getMap().centerOnUnit(this.ID);
if (action === "attack")
getApp().getUnitsManager().selectedUnitsAttackUnit(this.ID);
else if (action === "refuel")
getApp().getUnitsManager().selectedUnitsRefuel();
else if (action === "group-ground" || action === "group-navy")
getApp().getUnitsManager().selectedUnitsCreateGroup();
else if (action === "scenic-aaa")
getApp().getUnitsManager().selectedUnitsScenicAAA();
else if (action === "miss-aaa")
getApp().getUnitsManager().selectedUnitsMissOnPurpose();
else if (action === "follow")
this.#showFollowOptions(e);
}
/***********************************************/
onAdd(map: Map): this {
super.onAdd(map);
@ -992,6 +971,56 @@ export abstract class Unit extends CustomMarker {
this.#redrawMarker();
}
showFollowOptions(units: Unit[]) {
var contextActionSet = new ContextActionSet();
contextActionSet.addContextAction(this, 'trail', "Trail", "Follow unit in trail formation", () => this.applyFollowOptions('trail', units));
contextActionSet.addContextAction(this, 'echelon-lh', "Echelon (LH)", "Follow unit in echelon left formation", () => this.applyFollowOptions('echelon-lh', units));
contextActionSet.addContextAction(this, 'echelon-rh', "Echelon (RH)", "Follow unit in echelon right formation", () => this.applyFollowOptions('echelon-rh', units));
contextActionSet.addContextAction(this, 'line-abreast-lh', "Line abreast (LH)", "Follow unit in line abreast left formation", () => this.applyFollowOptions('line-abreast-lh', units));
contextActionSet.addContextAction(this, 'line-abreast-rh', "Line abreast (RH)", "Follow unit in line abreast right formation", () => this.applyFollowOptions('line-abreast-rh', units));
contextActionSet.addContextAction(this, 'front', "Front", "Fly in front of unit", () => this.applyFollowOptions('front', units));
contextActionSet.addContextAction(this, 'diamond', "Diamond", "Follow unit in diamond formation", () => this.applyFollowOptions('diamond', units));
contextActionSet.addContextAction(this, 'custom', "Custom", "Set a custom formation position", () => this.applyFollowOptions('custom', units));
getApp().getMap().getUnitContextMenu().setContextActions(contextActionSet);
getApp().getMap().showUnitContextMenu();
}
applyFollowOptions(formation: string, units: Unit[]) {
if (formation === "custom") {
document.getElementById("custom-formation-dialog")?.classList.remove("hide");
document.addEventListener("applyCustomFormation", () => {
var dialog = document.getElementById("custom-formation-dialog");
if (dialog) {
dialog.classList.add("hide");
var clock = 1;
while (clock < 8) {
if ((<HTMLInputElement>dialog.querySelector(`#formation-${clock}`)).checked)
break
clock++;
}
var angleDeg = 360 - (clock - 1) * 45;
var angleRad = deg2rad(angleDeg);
var distance = ftToM(parseInt((<HTMLInputElement>dialog.querySelector(`#distance`)?.querySelector("input")).value));
var upDown = ftToM(parseInt((<HTMLInputElement>dialog.querySelector(`#up-down`)?.querySelector("input")).value));
// X: front-rear, positive front
// Y: top-bottom, positive top
// Z: left-right, positive right
var x = distance * Math.cos(angleRad);
var y = upDown;
var z = distance * Math.sin(angleRad);
getApp().getUnitsManager().followUnit(this.ID, { "x": x, "y": y, "z": z }, undefined, units);
}
});
}
else {
getApp().getUnitsManager().followUnit(this.ID, undefined, formation, units);
}
}
/***********************************************/
#onClick(e: any) {
/* Exit if we were waiting for a doubleclick */
@ -1030,102 +1059,20 @@ export abstract class Unit extends CustomMarker {
});
}
getActionOptions() {
var options: { [key: string]: { text: string, tooltip: string, type: string } } | null = null;
#onContextMenu(e: any) {
var contextActionSet = new ContextActionSet();
var units = getApp().getUnitsManager().getSelectedUnits();
units.push(this);
if (!units.includes(this))
units.push(this);
/* Keep only the common "or" options or any "and" option */
units.forEach((unit: Unit) => {
var unitOptions = unit.getActions();
if (options === null) {
options = unitOptions;
} else {
/* Options of "or" type get shown if any one unit has it*/
for (let optionKey in unitOptions) {
if (unitOptions[optionKey].type == "or") {
options[optionKey] = unitOptions[optionKey];
}
}
unit.appendContextActions(contextActionSet, this, null);
})
/* Options of "and" type get shown if ALL units have it */
for (let optionKey in options) {
if (!(optionKey in unitOptions)) {
delete options[optionKey];
}
}
}
});
return options ?? {};
}
#onContextMenu(e: any) {
var options = this.getActionOptions();
if (Object.keys(options).length > 0) {
if (Object.keys(contextActionSet.getContextActions()).length > 0) {
getApp().getMap().showUnitContextMenu(e.originalEvent.x, e.originalEvent.y, e.latlng);
getApp().getMap().getUnitContextMenu().setOptions(options, (option: string) => {
getApp().getMap().hideUnitContextMenu();
this.executeAction(e, option);
});
}
}
#showFollowOptions(e: any) {
var options: { [key: string]: { text: string, tooltip: string } } = {};
options = {
'trail': { text: "Trail", tooltip: "Follow unit in trail formation" },
'echelon-lh': { text: "Echelon (LH)", tooltip: "Follow unit in echelon left formation" },
'echelon-rh': { text: "Echelon (RH)", tooltip: "Follow unit in echelon right formation" },
'line-abreast-lh': { text: "Line abreast (LH)", tooltip: "Follow unit in line abreast left formation" },
'line-abreast-rh': { text: "Line abreast (RH)", tooltip: "Follow unit in line abreast right formation" },
'front': { text: "Front", tooltip: "Fly in front of unit" },
'diamond': { text: "Diamond", tooltip: "Follow unit in diamond formation" },
'custom': { text: "Custom", tooltip: "Set a custom formation position" },
}
getApp().getMap().getUnitContextMenu().setOptions(options, (option: string) => {
getApp().getMap().hideUnitContextMenu();
this.#applyFollowOptions(option);
});
getApp().getMap().showUnitContextMenu(e.originalEvent.x, e.originalEvent.y, e.latlng);
}
#applyFollowOptions(action: string) {
if (action === "custom") {
document.getElementById("custom-formation-dialog")?.classList.remove("hide");
document.addEventListener("applyCustomFormation", () => {
var dialog = document.getElementById("custom-formation-dialog");
if (dialog) {
dialog.classList.add("hide");
var clock = 1;
while (clock < 8) {
if ((<HTMLInputElement>dialog.querySelector(`#formation-${clock}`)).checked)
break
clock++;
}
var angleDeg = 360 - (clock - 1) * 45;
var angleRad = deg2rad(angleDeg);
var distance = ftToM(parseInt((<HTMLInputElement>dialog.querySelector(`#distance`)?.querySelector("input")).value));
var upDown = ftToM(parseInt((<HTMLInputElement>dialog.querySelector(`#up-down`)?.querySelector("input")).value));
// X: front-rear, positive front
// Y: top-bottom, positive top
// Z: left-right, positive right
var x = distance * Math.cos(angleRad);
var y = upDown;
var z = distance * Math.sin(angleRad);
getApp().getUnitsManager().selectedUnitsFollowUnit(this.ID, { "x": x, "y": y, "z": z });
}
});
}
else {
getApp().getUnitsManager().selectedUnitsFollowUnit(this.ID, undefined, action);
getApp().getMap().getUnitContextMenu().setContextActions(contextActionSet);
}
}
@ -1483,7 +1430,7 @@ export abstract class Unit extends CustomMarker {
}
#onZoom(e: any) {
if (this.checkZoomRedraw())
if (this.checkZoomRedraw())
this.#redrawMarker();
this.#updateMarker();
}
@ -1507,35 +1454,27 @@ export abstract class AirUnit extends Unit {
};
}
getActions() {
var options: { [key: string]: { text: string, tooltip: string, type: string } } = {};
/* Options if this unit is not selected */
if (!this.getSelected()) {
/* Someone else is selected */
if (getApp().getUnitsManager().getSelectedUnits().length > 0) {
options["attack"] = { text: "Attack", tooltip: "Attack the unit using A/A or A/G weapons", type: "or" };
options["follow"] = { text: "Follow", tooltip: "Follow the unit at a user defined distance and position", type: "or" };
} else {
options["center-map"] = { text: "Center map", tooltip: "Center the map on the unit and follow it", type: "and" };
appendContextActions(contextActionSet: ContextActionSet, targetUnit: Unit | null, targetPosition: LatLng | null) {
if (targetUnit !== null) {
if (targetUnit != this) {
if (this.canFulfillRole(["CAP", "CAS", "Strike"]))
contextActionSet.addContextAction(this, "attack", "Attack unit", "Attack the unit using A/A or A/G weapons", (units: Unit[]) => { getApp().getUnitsManager().attackUnit(targetUnit.ID, units) });
}
else {
contextActionSet.addContextAction(this, "follow", "Follow unit", "Follow this unit in formation", (units: Unit[]) => { this.showFollowOptions(units); }, false); // Don't hide the context menu after the execution (to show the follow options)
}
if (targetUnit.getSelected()) {
contextActionSet.addContextAction(this, "refuel", "Refuel", "Refuel units at the nearest AAR Tanker. If no tanker is available the unit will RTB", (units: Unit[]) => { getApp().getUnitsManager().refuel(units) });
}
if (getApp().getUnitsManager().getSelectedUnits().length == 1 && targetUnit === this) {
contextActionSet.addContextAction(this, "center-map", "Center map", "Center the map on the unit and follow it", () => { getApp().getMap().centerOnUnit(this.ID); });
}
}
/* Options if this unit is selected*/
else if (this.getSelected()) {
/* This is the only selected unit */
if (getApp().getUnitsManager().getSelectedUnits().length == 1) {
options["center-map"] = { text: "Center map", tooltip: "Center the map on the unit and follow it", type: "and" };
} else {
options["follow"] = { text: "Follow", tooltip: "Follow the unit at a user defined distance and position", type: "or" };
}
options["refuel"] = { text: "Air to air refuel", tooltip: "Refuel units at the nearest AAR Tanker. If no tanker is available the unit will RTB.", type: "and" }; // TODO Add some way of knowing which aircraft can AAR
if (targetPosition !== null) {
contextActionSet.addContextAction(this, "bomb", "Precision bombing", "Precision bombing of a specific point", (units: Unit[]) => { getApp().getUnitsManager().bombPoint(targetPosition, units) });
contextActionSet.addContextAction(this, "carpet-bomb", "Carpet bombing", "Carpet bombing close to a point", (units: Unit[]) => { getApp().getUnitsManager().carpetBomb(targetPosition, units) });
}
/* All other options */
else {
/* Provision */
}
return options;
}
}
@ -1547,6 +1486,14 @@ export class Aircraft extends AirUnit {
getCategory() {
return "Aircraft";
}
appendContextActions(contextActionSet: ContextActionSet, targetUnit: Unit | null, targetPosition: LatLng | null) {
super.appendContextActions(contextActionSet, targetUnit, targetPosition);
if (targetUnit !== null && targetUnit.getSelected()) {
contextActionSet.addContextAction(this, "refuel", "Refuel", "Refuel units at the nearest AAR Tanker. If no tanker is available the unit will RTB", (units: Unit[]) => { getApp().getUnitsManager().refuel(units) });
}
}
}
export class Helicopter extends AirUnit {
@ -1557,6 +1504,13 @@ export class Helicopter extends AirUnit {
getCategory() {
return "Helicopter";
}
appendContextActions(contextActionSet: ContextActionSet, targetUnit: Unit | null, targetPosition: LatLng | null) {
super.appendContextActions(contextActionSet, targetUnit, targetPosition);
if (targetPosition !== null)
contextActionSet.addContextAction(this, "land-at-point", "Land here", "land at this precise location", (units: Unit[]) => { getApp().getUnitsManager().landAtPoint(targetPosition, units) });
}
}
export class GroundUnit extends Unit {
@ -1581,37 +1535,31 @@ export class GroundUnit extends Unit {
};
}
getActions() {
var options: { [key: string]: { text: string, tooltip: string, type: string } } = {};
appendContextActions(contextActionSet: ContextActionSet, targetUnit: Unit | null, targetPosition: LatLng | null) {
contextActionSet.addContextAction(this, "group-ground", "Group ground units", "Create a group of ground units", (units: Unit[]) => { getApp().getUnitsManager().createGroup(units) });
/* Options if this unit is not selected */
if (!this.getSelected()) {
/* Someone else is selected */
if (getApp().getUnitsManager().getSelectedUnits().length > 0) {
options["attack"] = { text: "Attack", tooltip: "Attack the unit using A/A or A/G weapons", type: "or" };
} else {
options["center-map"] = { text: "Center map", tooltip: "Center the map on the unit and follow it", type: "and" };
}
}
/* Options if this unit is selected*/
else if (this.getSelected()) {
/* This is the only selected unit */
if (getApp().getUnitsManager().getSelectedUnits().length == 1) {
options["center-map"] = { text: "Center map", tooltip: "Center the map on the unit and follow it", type: "and" };
} else {
options["group-ground"] = { text: "Create group", tooltip: "Create a group from the selected units", type: "and" };
if (targetUnit !== null) {
if (targetUnit != this) {
contextActionSet.addContextAction(this, "attack", "Attack unit", "Attack the unit using A/A or A/G weapons", (units: Unit[]) => { getApp().getUnitsManager().attackUnit(targetUnit.ID, units) });
}
if (this.canAAA()) {
options["scenic-aaa"] = { text: "Scenic AAA", tooltip: "Shoot AAA in the air without aiming at any target, when a enemy unit gets close enough. WARNING: works correctly only on neutral units, blue or red units will aim", type: "and" };
options["miss-aaa"] = { text: "Miss on purpose AAA", tooltip: "Shoot AAA towards the closest enemy unit, but don't aim precisely. WARNING: works correctly only on neutral units, blue or red units will aim", type: "and" };
if (getApp().getUnitsManager().getSelectedUnits().length == 1 && targetUnit === this) {
contextActionSet.addContextAction(this, "center-map", "Center map", "Center the map on the unit and follow it", () => { getApp().getMap().centerOnUnit(this.ID); });
}
}
if (targetPosition !== null) {
if (this.canTargetPoint()) {
contextActionSet.addContextAction(this, "fire-at-area", "Fire at area", "Fire at a specific area on the ground", (units: Unit[]) => { getApp().getUnitsManager().fireAtArea(targetPosition, units) });
contextActionSet.addContextAction(this, "simulate-fire-fight", "Simulate fire fight", "Simulate a fire fight by shooting randomly in a certain large area. WARNING: works correctly only on neutral units, blue or red units will aim", (units: Unit[]) => { getApp().getUnitsManager().fireAtArea(targetPosition, units) });
}
}
/* All other options */
else {
/* Provision */
if (this.canAAA()) {
contextActionSet.addContextAction(this, "scenic-aaa", "Scenic AAA", "Shoot AAA in the air without aiming at any target, when a enemy unit gets close enough. WARNING: works correctly only on neutral units, blue or red units will aim", (units: Unit[]) => { getApp().getUnitsManager().scenicAAA(units) });
contextActionSet.addContextAction(this, "miss-aaa", "Misson on purpose", "Shoot AAA towards the closest enemy unit, but don't aim precisely. WARNING: works correctly only on neutral units, blue or red units will aim", (units: Unit[]) => { getApp().getUnitsManager().missOnPurpose(units) });
}
}
return options;
}
getCategory() {
@ -1671,32 +1619,21 @@ export class NavyUnit extends Unit {
};
}
getActions() {
var options: { [key: string]: { text: string, tooltip: string, type: string } } = {};
appendContextActions(contextActionSet: ContextActionSet, targetUnit: Unit | null, targetPosition: LatLng | null) {
contextActionSet.addContextAction(this, "group-navy", "Group navy units", "Create a group of navy units", (units: Unit[]) => { getApp().getUnitsManager().createGroup(units) });
/* Options if this unit is not selected */
if (!this.getSelected()) {
/* Someone else is selected */
if (getApp().getUnitsManager().getSelectedUnits().length > 0) {
options["attack"] = { text: "Attack", tooltip: "Attack the unit using A/A or A/G weapons", type: "or" };
} else {
options["center-map"] = { text: "Center map", tooltip: "Center the map on the unit and follow it", type: "and" };
if (targetUnit !== null) {
if (targetUnit != this) {
contextActionSet.addContextAction(this, "attack", "Attack unit", "Attack the unit using A/A or A/G weapons", (units: Unit[]) => { getApp().getUnitsManager().attackUnit(targetUnit.ID, units) });
}
if (getApp().getUnitsManager().getSelectedUnits().length == 1 && targetUnit === this) {
contextActionSet.addContextAction(this, "center-map", "Center map", "Center the map on the unit and follow it", () => { getApp().getMap().centerOnUnit(this.ID); });
}
}
/* Options if this unit is selected */
else if (this.getSelected()) {
/* This is the only selected unit */
if (getApp().getUnitsManager().getSelectedUnits().length == 1) {
options["center-map"] = { text: "Center map", tooltip: "Center the map on the unit and follow it", type: "and" };
} else {
options["group-navy"] = { text: "Create group", tooltip: "Create a group from the selected units", type: "and" };
}
if (targetPosition !== null) {
contextActionSet.addContextAction(this, "fire-at-area", "Fire at area", "Fire at a specific area on the ground", (units: Unit[]) => { getApp().getUnitsManager().fireAtArea(targetPosition, units) });
}
/* All other options */
else {
/* Provision */
}
return options;
}
getMarkerCategory() {

View File

@ -36,15 +36,15 @@ export class UnitsManager {
document.addEventListener('commandModeOptionsChanged', () => { Object.values(this.#units).forEach((unit: Unit) => unit.updateVisibility()) });
document.addEventListener('contactsUpdated', (e: CustomEvent) => { this.#requestDetectionUpdate = true });
document.addEventListener('copy', () => this.selectedUnitsCopy());
document.addEventListener('deleteSelectedUnits', () => this.selectedUnitsDelete());
document.addEventListener('explodeSelectedUnits', (e: any) => this.selectedUnitsDelete(true, e.detail.type));
document.addEventListener('copy', () => this.copy());
document.addEventListener('deleteSelectedUnits', () => this.delete());
document.addEventListener('explodeSelectedUnits', (e: any) => this.delete(true, e.detail.type));
document.addEventListener('exportToFile', () => this.exportToFile());
document.addEventListener('importFromFile', () => this.importFromFile());
document.addEventListener('keyup', (event) => this.#onKeyUp(event));
document.addEventListener('paste', () => this.pasteUnits());
document.addEventListener('selectedUnitsChangeAltitude', (e: any) => { this.selectedUnitsChangeAltitude(e.detail.type) });
document.addEventListener('selectedUnitsChangeSpeed', (e: any) => { this.selectedUnitsChangeSpeed(e.detail.type) });
document.addEventListener('paste', () => this.paste());
document.addEventListener('selectedUnitsChangeAltitude', (e: any) => { this.changeAltitude(e.detail.type) });
document.addEventListener('selectedUnitsChangeSpeed', (e: any) => { this.changeSpeed(e.detail.type) });
document.addEventListener('unitDeselection', (e: CustomEvent) => this.#onUnitDeselection(e.detail));
document.addEventListener('unitSelection', (e: CustomEvent) => this.#onUnitSelection(e.detail));
document.addEventListener("toggleMarkerProtection", (ev: CustomEventInit) => { this.#showNumberOfSelectedProtectedUnits() });
@ -130,8 +130,8 @@ export class UnitsManager {
this.#units[ID]?.setData(dataExtractor);
}
/* Update the unit groups */
for (let ID in this.#units) {
/* Update the unit groups */
for (let ID in this.#units) {
const unit = this.#units[ID];
const groupName = unit.getGroupName();
@ -141,7 +141,7 @@ export class UnitsManager {
this.#groups[groupName] = new Group(groupName);
/* If the unit was not assigned to a group yet, assign it */
if (unit.getGroup() === null)
if (unit.getGroup() === null)
this.#groups[groupName].addMember(unit);
}
}
@ -350,22 +350,21 @@ export class UnitsManager {
* @param mantainRelativePosition If true, the selected units will mantain their relative positions when reaching the target. This is useful to maintain a formation for groun/navy units
* @param rotation Rotation in radians by which the formation will be rigidly rotated. E.g. a ( V ) formation will look like this ( < ) if rotated pi/4 radians (90 degrees)
*/
selectedUnitsAddDestination(latlng: L.LatLng, mantainRelativePosition: boolean, rotation: number) {
var selectedUnits = this.getSelectedUnits({ excludeHumans: true, excludeProtected: true, onlyOnePerGroup: true, showProtectionReminder: true });
addDestination(latlng: L.LatLng, mantainRelativePosition: boolean, rotation: number, units: Unit[] | null = null) {
if (units === null)
units = this.getSelectedUnits({ excludeHumans: true, excludeProtected: true, onlyOnePerGroup: true, showProtectionReminder: true });
if (selectedUnits.length === 0)
if (units.length === 0)
return;
/* Compute the destination for each unit. If mantainRelativePosition is true, compute the destination so to hold the relative positions */
var unitDestinations: { [key: number]: LatLng } = {};
if (mantainRelativePosition)
unitDestinations = this.selectedUnitsComputeGroupDestination(latlng, rotation);
unitDestinations = this.computeGroupDestination(latlng, rotation);
else
selectedUnits.forEach((unit: Unit) => { unitDestinations[unit.ID] = latlng; });
for (let idx in selectedUnits) {
const unit = selectedUnits[idx];
units.forEach((unit: Unit) => { unitDestinations[unit.ID] = latlng; });
units.forEach((unit: Unit) => {
/* If a unit is following another unit, and that unit is also selected, send the command to the followed ("leader") unit */
if (unit.getState() === "follow") {
const leader = this.getUnitByID(unit.getLeaderID())
@ -378,18 +377,22 @@ export class UnitsManager {
if (unit.ID in unitDestinations)
unit.addDestination(unitDestinations[unit.ID]);
}
}
this.#showActionMessage(selectedUnits, " new destination added");
});
this.#showActionMessage(units, " new destination added");
}
/** Clear the destinations of all the selected units
*
*/
selectedUnitsClearDestinations() {
var selectedUnits = this.getSelectedUnits({ excludeHumans: true, excludeProtected: true, onlyOnePerGroup: true, showProtectionReminder: false });
for (let idx in selectedUnits) {
const unit = selectedUnits[idx];
clearDestinations(units: Unit[] | null = null) {
if (units === null)
units = this.getSelectedUnits({ excludeHumans: true, excludeProtected: true, onlyOnePerGroup: true, showProtectionReminder: false });
if (units.length === 0)
return;
for (let idx in units) {
const unit = units[idx];
if (unit.getState() === "follow") {
const leader = this.getUnitByID(unit.getLeaderID())
if (leader && leader.getSelected())
@ -406,178 +409,224 @@ export class UnitsManager {
*
* @param latlng Location where to land at
*/
selectedUnitsLandAt(latlng: LatLng) {
var selectedUnits = this.getSelectedUnits({ excludeHumans: true, excludeProtected: true, onlyOnePerGroup: true, showProtectionReminder: true });
for (let idx in selectedUnits) {
selectedUnits[idx].landAt(latlng);
}
this.#showActionMessage(selectedUnits, " landing");
landAt(latlng: LatLng, units: Unit[] | null = null) {
if (units === null)
units = this.getSelectedUnits({ excludeHumans: true, excludeProtected: true, onlyOnePerGroup: true, showProtectionReminder: true });
if (units.length === 0)
return;
units.forEach((unit: Unit) => unit.landAt(latlng));
this.#showActionMessage(units, " landing");
}
/** Instruct all the selected units to change their speed
*
* @param speedChange Speed change, either "stop", "slow", or "fast". The specific value depends on the unit category
*/
selectedUnitsChangeSpeed(speedChange: string) {
var selectedUnits = this.getSelectedUnits({ excludeHumans: true, excludeProtected: true, onlyOnePerGroup: true, showProtectionReminder: true });
for (let idx in selectedUnits) {
selectedUnits[idx].changeSpeed(speedChange);
}
changeSpeed(speedChange: string, units: Unit[] | null = null) {
if (units === null)
units = this.getSelectedUnits({ excludeHumans: true, excludeProtected: true, onlyOnePerGroup: true, showProtectionReminder: true });
if (units.length === 0)
return;
units.forEach((unit: Unit) => unit.changeSpeed(speedChange));
}
/** Instruct all the selected units to change their altitude
*
* @param altitudeChange Altitude change, either "climb" or "descend". The specific value depends on the unit category
*/
selectedUnitsChangeAltitude(altitudeChange: string) {
var selectedUnits = this.getSelectedUnits({ excludeHumans: true, excludeProtected: true, onlyOnePerGroup: true, showProtectionReminder: true });
for (let idx in selectedUnits) {
selectedUnits[idx].changeAltitude(altitudeChange);
}
changeAltitude(altitudeChange: string, units: Unit[] | null = null) {
if (units === null)
units = this.getSelectedUnits({ excludeHumans: true, excludeProtected: true, onlyOnePerGroup: true, showProtectionReminder: true });
if (units.length === 0)
return;
units.forEach((unit: Unit) => unit.changeAltitude(altitudeChange));
}
/** Set a specific speed to all the selected units
*
* @param speed Value to set, in m/s
*/
selectedUnitsSetSpeed(speed: number) {
var selectedUnits = this.getSelectedUnits({ excludeHumans: true, excludeProtected: true, onlyOnePerGroup: true, showProtectionReminder: true });
for (let idx in selectedUnits) {
selectedUnits[idx].setSpeed(speed);
}
this.#showActionMessage(selectedUnits, `setting speed to ${msToKnots(speed)} kts`);
setSpeed(speed: number, units: Unit[] | null = null) {
if (units === null)
units = this.getSelectedUnits({ excludeHumans: true, excludeProtected: true, onlyOnePerGroup: true, showProtectionReminder: true });
if (units.length === 0)
return;
units.forEach((unit: Unit) => unit.setSpeed(speed));
this.#showActionMessage(units, `setting speed to ${msToKnots(speed)} kts`);
}
/** Set a specific speed type to all the selected units
*
* @param speedType Value to set, either "CAS" or "GS". If "CAS" is selected, the unit will try to maintain the selected Calibrated Air Speed, but DCS will still only maintain a Ground Speed value so errors may arise depending on wind.
*/
selectedUnitsSetSpeedType(speedType: string) {
var selectedUnits = this.getSelectedUnits({ excludeHumans: true, excludeProtected: true, onlyOnePerGroup: true, showProtectionReminder: true });
for (let idx in selectedUnits) {
selectedUnits[idx].setSpeedType(speedType);
}
this.#showActionMessage(selectedUnits, `setting speed type to ${speedType}`);
setSpeedType(speedType: string, units: Unit[] | null = null) {
if (units === null)
units = this.getSelectedUnits({ excludeHumans: true, excludeProtected: true, onlyOnePerGroup: true, showProtectionReminder: true });
if (units.length === 0)
return;
units.forEach((unit: Unit) => unit.setSpeedType(speedType));
this.#showActionMessage(units, `setting speed type to ${speedType}`);
}
/** Set a specific altitude to all the selected units
*
* @param altitude Value to set, in m
*/
selectedUnitsSetAltitude(altitude: number) {
var selectedUnits = this.getSelectedUnits({ excludeHumans: true, excludeProtected: true, onlyOnePerGroup: true, showProtectionReminder: true });
for (let idx in selectedUnits) {
selectedUnits[idx].setAltitude(altitude);
}
this.#showActionMessage(selectedUnits, `setting altitude to ${mToFt(altitude)} ft`);
setAltitude(altitude: number, units: Unit[] | null = null) {
if (units === null)
units = this.getSelectedUnits({ excludeHumans: true, excludeProtected: true, onlyOnePerGroup: true, showProtectionReminder: true });
if (units.length === 0)
return;
units.forEach((unit: Unit) => unit.setAltitude(altitude));
this.#showActionMessage(units, `setting altitude to ${mToFt(altitude)} ft`);
}
/** Set a specific altitude type to all the selected units
*
* @param altitudeType Value to set, either "ASL" or "AGL". If "AGL" is selected, the unit will try to maintain the selected Above Ground Level altitude. Due to a DCS bug, this will only be true at the final position.
*/
selectedUnitsSetAltitudeType(altitudeType: string) {
var selectedUnits = this.getSelectedUnits({ excludeHumans: true, excludeProtected: true, onlyOnePerGroup: true, showProtectionReminder: true });
for (let idx in selectedUnits) {
selectedUnits[idx].setAltitudeType(altitudeType);
}
this.#showActionMessage(selectedUnits, `setting altitude type to ${altitudeType}`);
setAltitudeType(altitudeType: string, units: Unit[] | null = null) {
if (units === null)
units = this.getSelectedUnits({ excludeHumans: true, excludeProtected: true, onlyOnePerGroup: true, showProtectionReminder: true });
if (units.length === 0)
return;
units.forEach((unit: Unit) => unit.setAltitudeType(altitudeType));
this.#showActionMessage(units, `setting altitude type to ${altitudeType}`);
}
/** Set a specific ROE to all the selected units
*
* @param ROE Value to set, see constants for acceptable values
*/
selectedUnitsSetROE(ROE: string) {
var selectedUnits = this.getSelectedUnits({ excludeHumans: true, excludeProtected: true, onlyOnePerGroup: true, showProtectionReminder: true });
for (let idx in selectedUnits) {
selectedUnits[idx].setROE(ROE);
}
this.#showActionMessage(selectedUnits, `ROE set to ${ROE}`);
setROE(ROE: string, units: Unit[] | null = null) {
if (units === null)
units = this.getSelectedUnits({ excludeHumans: true, excludeProtected: true, onlyOnePerGroup: true, showProtectionReminder: true });
if (units.length === 0)
return;
units.forEach((unit: Unit) => unit.setROE(ROE));
this.#showActionMessage(units, `ROE set to ${ROE}`);
}
/** Set a specific reaction to threat to all the selected units
*
* @param reactionToThreat Value to set, see constants for acceptable values
*/
selectedUnitsSetReactionToThreat(reactionToThreat: string) {
var selectedUnits = this.getSelectedUnits({ excludeHumans: true, excludeProtected: true, onlyOnePerGroup: true, showProtectionReminder: true });
for (let idx in selectedUnits) {
selectedUnits[idx].setReactionToThreat(reactionToThreat);
}
this.#showActionMessage(selectedUnits, `reaction to threat set to ${reactionToThreat}`);
setReactionToThreat(reactionToThreat: string, units: Unit[] | null = null) {
if (units === null)
units = this.getSelectedUnits({ excludeHumans: true, excludeProtected: true, onlyOnePerGroup: true, showProtectionReminder: true });
if (units.length === 0)
return;
units.forEach((unit: Unit) => unit.setReactionToThreat(reactionToThreat));
this.#showActionMessage(units, `reaction to threat set to ${reactionToThreat}`);
}
/** Set a specific emissions & countermeasures to all the selected units
*
* @param emissionCountermeasure Value to set, see constants for acceptable values
*/
selectedUnitsSetEmissionsCountermeasures(emissionCountermeasure: string) {
var selectedUnits = this.getSelectedUnits({ excludeHumans: true, excludeProtected: true, onlyOnePerGroup: true, showProtectionReminder: true });
for (let idx in selectedUnits) {
selectedUnits[idx].setEmissionsCountermeasures(emissionCountermeasure);
}
this.#showActionMessage(selectedUnits, `emissions & countermeasures set to ${emissionCountermeasure}`);
setEmissionsCountermeasures(emissionCountermeasure: string, units: Unit[] | null = null) {
if (units === null)
units = this.getSelectedUnits({ excludeHumans: true, excludeProtected: true, onlyOnePerGroup: true, showProtectionReminder: true });
if (units.length === 0)
return;
units.forEach((unit: Unit) => unit.setEmissionsCountermeasures(emissionCountermeasure));
this.#showActionMessage(units, `emissions & countermeasures set to ${emissionCountermeasure}`);
}
/** Turn selected units on or off, only works on ground and navy units
*
* @param onOff If true, the unit will be turned on
*/
selectedUnitsSetOnOff(onOff: boolean) {
var selectedUnits = this.getSelectedUnits({ excludeHumans: true, excludeProtected: true, onlyOnePerGroup: true, showProtectionReminder: true });
for (let idx in selectedUnits) {
selectedUnits[idx].setOnOff(onOff);
}
this.#showActionMessage(selectedUnits, `unit active set to ${onOff}`);
setOnOff(onOff: boolean, units: Unit[] | null = null) {
if (units === null)
units = this.getSelectedUnits({ excludeHumans: true, excludeProtected: true, onlyOnePerGroup: true, showProtectionReminder: true });
if (units.length === 0)
return;
units.forEach((unit: Unit) => unit.setOnOff(onOff));
this.#showActionMessage(units, `unit active set to ${onOff}`);
}
/** Instruct the selected units to follow roads, only works on ground units
*
* @param followRoads If true, units will follow roads
*/
selectedUnitsSetFollowRoads(followRoads: boolean) {
var selectedUnits = this.getSelectedUnits({ excludeHumans: true, excludeProtected: true, onlyOnePerGroup: true, showProtectionReminder: true });
for (let idx in selectedUnits) {
selectedUnits[idx].setFollowRoads(followRoads);
}
this.#showActionMessage(selectedUnits, `follow roads set to ${followRoads}`);
setFollowRoads(followRoads: boolean, units: Unit[] | null = null) {
if (units === null)
units = this.getSelectedUnits({ excludeHumans: true, excludeProtected: true, onlyOnePerGroup: true, showProtectionReminder: true });
if (units.length === 0)
return;
units.forEach((unit: Unit) => unit.setFollowRoads(followRoads));
this.#showActionMessage(units, `follow roads set to ${followRoads}`);
}
/** Instruct selected units to operate as a certain coalition
*
* @param operateAsBool If true, units will operate as blue
*/
selectedUnitsSetOperateAs(operateAsBool: boolean) {
setOperateAs(operateAsBool: boolean, units: Unit[] | null = null) {
var operateAs = operateAsBool ? "blue" : "red";
var selectedUnits = this.getSelectedUnits({ excludeHumans: true, excludeProtected: true, onlyOnePerGroup: true, showProtectionReminder: true });
for (let idx in selectedUnits) {
selectedUnits[idx].setOperateAs(operateAs);
}
this.#showActionMessage(selectedUnits, `operate as set to ${operateAs}`);
if (units === null)
units = this.getSelectedUnits({ excludeHumans: true, excludeProtected: true, onlyOnePerGroup: true, showProtectionReminder: true });
if (units.length === 0)
return;
units.forEach((unit: Unit) => unit.setOperateAs(operateAs));
this.#showActionMessage(units, `operate as set to ${operateAs}`);
}
/** Instruct units to attack a specific unit
*
* @param ID ID of the unit to attack
*/
selectedUnitsAttackUnit(ID: number) {
var selectedUnits = this.getSelectedUnits({ excludeHumans: true, excludeProtected: true, onlyOnePerGroup: true, showProtectionReminder: true });
for (let idx in selectedUnits) {
selectedUnits[idx].attackUnit(ID);
}
this.#showActionMessage(selectedUnits, `attacking unit ${this.getUnitByID(ID)?.getUnitName()}`);
attackUnit(ID: number, units: Unit[] | null = null) {
if (units === null)
units = this.getSelectedUnits({ excludeHumans: true, excludeProtected: true, onlyOnePerGroup: true, showProtectionReminder: true });
if (units.length === 0)
return;
units.forEach((unit: Unit) => unit.attackUnit(ID));
this.#showActionMessage(units, `attacking unit ${this.getUnitByID(ID)?.getUnitName()}`);
}
/** Instruct units to refuel at the nearest tanker, if possible. Else units will RTB
*
*/
selectedUnitsRefuel() {
var selectedUnits = this.getSelectedUnits({ excludeHumans: true, excludeProtected: true, onlyOnePerGroup: true, showProtectionReminder: true });
for (let idx in selectedUnits) {
selectedUnits[idx].refuel();
}
this.#showActionMessage(selectedUnits, `sent to nearest tanker`);
refuel(units: Unit[] | null = null) {
if (units === null)
units = this.getSelectedUnits({ excludeHumans: true, excludeProtected: true, onlyOnePerGroup: true, showProtectionReminder: true });
if (units.length === 0)
return;
units.forEach((unit: Unit) => unit.refuel());
this.#showActionMessage(units, `sent to nearest tanker`);
}
/** Instruct the selected units to follow another unit in a formation. Only works for aircrafts and helicopters.
@ -586,7 +635,13 @@ export class UnitsManager {
* @param offset Optional parameter, defines a static offset. X: front-rear, positive front, Y: top-bottom, positive top, Z: left-right, positive right
* @param formation Optional parameter, defines a predefined formation type. Values are: "trail", "echelon-lh", "echelon-rh", "line-abreast-lh", "line-abreast-rh", "front", "diamond"
*/
selectedUnitsFollowUnit(ID: number, offset?: { "x": number, "y": number, "z": number }, formation?: string) {
followUnit(ID: number, offset?: { "x": number, "y": number, "z": number }, formation?: string, units: Unit[] | null = null) {
if (units === null)
units = this.getSelectedUnits({ excludeHumans: true, excludeProtected: true, onlyOnePerGroup: true, showProtectionReminder: true });
if (units.length === 0)
return;
if (offset == undefined) {
/* Simple formations with fixed offsets */
offset = { "x": 0, "y": 0, "z": 0 };
@ -599,16 +654,11 @@ export class UnitsManager {
else offset = undefined;
}
var selectedUnits = this.getSelectedUnits({ excludeHumans: true, excludeProtected: true, onlyOnePerGroup: true, showProtectionReminder: true });
if (selectedUnits.length === 0)
return;
var count = 1;
var xr = 0; var yr = 1; var zr = -1;
var layer = 1;
for (let idx in selectedUnits) {
var unit = selectedUnits[idx];
for (let idx in units) {
var unit = units[idx];
if (unit.ID !== ID) {
if (offset != undefined)
/* Offset is set, apply it */
@ -630,51 +680,65 @@ export class UnitsManager {
count++;
}
}
this.#showActionMessage(selectedUnits, `following unit ${this.getUnitByID(ID)?.getUnitName()}`);
this.#showActionMessage(units, `following unit ${this.getUnitByID(ID)?.getUnitName()}`);
}
/** Instruct the selected units to perform precision bombing of specific coordinates
*
* @param latlng Location to bomb
*/
selectedUnitsBombPoint(latlng: LatLng) {
var selectedUnits = this.getSelectedUnits({ excludeHumans: true, excludeProtected: true, onlyOnePerGroup: true, showProtectionReminder: true });
for (let idx in selectedUnits) {
selectedUnits[idx].bombPoint(latlng);
}
this.#showActionMessage(selectedUnits, `unit bombing point`);
bombPoint(latlng: LatLng, units: Unit[] | null = null) {
if (units === null)
units = this.getSelectedUnits({ excludeHumans: true, excludeProtected: true, onlyOnePerGroup: true, showProtectionReminder: true });
if (units.length === 0)
return;
units.forEach((unit: Unit) => unit.bombPoint(latlng));
this.#showActionMessage(units, `unit bombing point`);
}
/** Instruct the selected units to perform carpet bombing of specific coordinates
*
* @param latlng Location to bomb
*/
selectedUnitsCarpetBomb(latlng: LatLng) {
var selectedUnits = this.getSelectedUnits({ excludeHumans: true, excludeProtected: true, onlyOnePerGroup: true, showProtectionReminder: true });
for (let idx in selectedUnits) {
selectedUnits[idx].carpetBomb(latlng);
}
this.#showActionMessage(selectedUnits, `unit carpet bombing point`);
carpetBomb(latlng: LatLng, units: Unit[] | null = null) {
if (units === null)
units = this.getSelectedUnits({ excludeHumans: true, excludeProtected: true, onlyOnePerGroup: true, showProtectionReminder: true });
if (units.length === 0)
return;
units.forEach((unit: Unit) => unit.carpetBomb(latlng));
this.#showActionMessage(units, `unit carpet bombing point`);
}
/** Instruct the selected units to fire at specific coordinates
*
* @param latlng Location to fire at
*/
selectedUnitsFireAtArea(latlng: LatLng) {
var selectedUnits = this.getSelectedUnits({ excludeHumans: true, excludeProtected: true, onlyOnePerGroup: true, showProtectionReminder: true });
for (let idx in selectedUnits) {
selectedUnits[idx].fireAtArea(latlng);
}
this.#showActionMessage(selectedUnits, `unit firing at area`);
fireAtArea(latlng: LatLng, units: Unit[] | null = null) {
if (units === null)
units = this.getSelectedUnits({ excludeHumans: true, excludeProtected: true, onlyOnePerGroup: true, showProtectionReminder: true });
if (units.length === 0)
return;
units.forEach((unit: Unit) => unit.fireAtArea(latlng));
this.#showActionMessage(units, `unit firing at area`);
}
/** Instruct the selected units to simulate a fire fight at specific coordinates
*
* @param latlng Location to fire at
*/
selectedUnitsSimulateFireFight(latlng: LatLng) {
var selectedUnits = this.getSelectedUnits({ excludeHumans: true, excludeProtected: true, onlyOnePerGroup: true, showProtectionReminder: true });
simulateFireFight(latlng: LatLng, units: Unit[] | null = null) {
if (units === null)
units = this.getSelectedUnits({ excludeHumans: true, excludeProtected: true, onlyOnePerGroup: true, showProtectionReminder: true });
if (units.length === 0)
return;
getGroundElevation(latlng, (response: string) => {
var groundElevation: number | null = null;
try {
@ -682,70 +746,83 @@ export class UnitsManager {
} catch {
console.warn("Simulate fire fight: could not retrieve ground elevation")
}
for (let idx in selectedUnits) {
selectedUnits[idx].simulateFireFight(latlng, groundElevation);
}
units?.forEach((unit: Unit) => unit.simulateFireFight(latlng, groundElevation));
});
this.#showActionMessage(selectedUnits, `unit simulating fire fight`);
this.#showActionMessage(units, `unit simulating fire fight`);
}
/** Instruct units to enter into scenic AAA mode. Units will shoot in the air without aiming
*
*/
selectedUnitsScenicAAA() {
var selectedUnits = this.getSelectedUnits({ excludeHumans: true, excludeProtected: true, onlyOnePerGroup: true, showProtectionReminder: true });
for (let idx in selectedUnits) {
selectedUnits[idx].scenicAAA();
}
this.#showActionMessage(selectedUnits, `unit set to perform scenic AAA`);
scenicAAA(units: Unit[] | null = null) {
if (units === null)
units = this.getSelectedUnits({ excludeHumans: true, excludeProtected: true, onlyOnePerGroup: true, showProtectionReminder: true });
if (units.length === 0)
return;
units.forEach((unit: Unit) => unit.scenicAAA());
this.#showActionMessage(units, `unit set to perform scenic AAA`);
}
/** Instruct units to enter into miss on purpose mode. Units will aim to the nearest enemy unit but not precisely.
*
*/
selectedUnitsMissOnPurpose() {
var selectedUnits = this.getSelectedUnits({ excludeHumans: true, excludeProtected: true, onlyOnePerGroup: true, showProtectionReminder: true });
for (let idx in selectedUnits) {
selectedUnits[idx].missOnPurpose();
}
this.#showActionMessage(selectedUnits, `unit set to perform miss-on-purpose AAA`);
missOnPurpose(units: Unit[] | null = null) {
if (units === null)
units = this.getSelectedUnits({ excludeHumans: true, excludeProtected: true, onlyOnePerGroup: true, showProtectionReminder: true });
if (units.length === 0)
return;
units.forEach((unit: Unit) => unit.missOnPurpose());
this.#showActionMessage(units, `unit set to perform miss-on-purpose AAA`);
}
/** Instruct units to land at specific point
*
* @param latlng Point where to land
*/
selectedUnitsLandAtPoint(latlng: LatLng) {
var selectedUnits = this.getSelectedUnits({ excludeHumans: true, excludeProtected: true, onlyOnePerGroup: true, showProtectionReminder: true });
landAtPoint(latlng: LatLng, units: Unit[] | null = null) {
if (units === null)
units = this.getSelectedUnits({ excludeHumans: true, excludeProtected: true, onlyOnePerGroup: true, showProtectionReminder: true });
for (let idx in selectedUnits) {
selectedUnits[idx].landAtPoint(latlng);
}
this.#showActionMessage(selectedUnits, `unit landing at point`);
if (units.length === 0)
return;
units.forEach((unit: Unit) => unit.landAtPoint(latlng));
this.#showActionMessage(units, `unit landing at point`);
}
/** Set a specific shots scatter to all the selected units
*
* @param shotsScatter Value to set
*/
selectedUnitsSetShotsScatter(shotsScatter: number) {
var selectedUnits = this.getSelectedUnits({ excludeHumans: true, onlyOnePerGroup: true });
for (let idx in selectedUnits) {
selectedUnits[idx].setShotsScatter(shotsScatter);
}
this.#showActionMessage(selectedUnits, `shots scatter set to ${shotsScatter}`);
setShotsScatter(shotsScatter: number, units: Unit[] | null = null) {
if (units === null)
units = this.getSelectedUnits({ excludeHumans: true, onlyOnePerGroup: true });
if (units.length === 0)
return;
units.forEach((unit: Unit) => unit.setShotsScatter(shotsScatter));
this.#showActionMessage(units, `shots scatter set to ${shotsScatter}`);
}
/** Set a specific shots intensity to all the selected units
*
* @param shotsScatter Value to set
*/
selectedUnitsSetShotsIntensity(shotsIntensity: number) {
var selectedUnits = this.getSelectedUnits({ excludeHumans: true, onlyOnePerGroup: true });
for (let idx in selectedUnits) {
selectedUnits[idx].setShotsIntensity(shotsIntensity);
}
this.#showActionMessage(selectedUnits, `shots intensity set to ${shotsIntensity}`);
setShotsIntensity(shotsIntensity: number, units: Unit[] | null = null) {
if (units === null)
units = this.getSelectedUnits({ excludeHumans: true, onlyOnePerGroup: true });
if (units.length === 0)
return;
units.forEach((unit: Unit) => unit.setShotsIntensity(shotsIntensity));
this.#showActionMessage(units, `shots intensity set to ${shotsIntensity}`);
}
/*********************** Control operations on selected units ************************/
@ -769,15 +846,18 @@ export class UnitsManager {
/** Groups the selected units in a single (DCS) group, if all the units have the same category
*
*/
selectedUnitsCreateGroup() {
var selectedUnits = this.getSelectedUnits({ excludeHumans: true, excludeProtected: true, onlyOnePerGroup: false, showProtectionReminder: true });
if (this.getUnitsCategories(selectedUnits).length == 1) {
var units: { ID: number, location: LatLng }[] = [];
for (let idx in selectedUnits) {
var unit = selectedUnits[idx];
units.push({ ID: unit.ID, location: unit.getPosition() });
}
getApp().getServerManager().cloneUnits(units, true, 0 /* No spawn points, we delete the original units */);
createGroup(units: Unit[] | null = null) {
if (units === null)
units = this.getSelectedUnits({ excludeHumans: true, excludeProtected: true, onlyOnePerGroup: false, showProtectionReminder: true });
if (units.length === 0)
return;
if (this.getUnitsCategories(units).length == 1) {
var unitsData: { ID: number, location: LatLng }[] = [];
units.forEach((unit: Unit) => unitsData.push({ ID: unit.ID, location: unit.getPosition() }));
getApp().getServerManager().cloneUnits(unitsData, true, 0 /* No spawn points, we delete the original units */);
this.#showActionMessage(units, `created a group`);
} else {
(getApp().getPopupsManager().get("infoPopup") as Popup).setText(`Groups can only be created from units of the same category`);
}
@ -787,21 +867,20 @@ export class UnitsManager {
*
* @param hotgroup Hotgroup number
*/
selectedUnitsSetHotgroup(hotgroup: number) {
setHotgroup(hotgroup: number, units: Unit[] | null = null) {
this.getUnitsByHotgroup(hotgroup).forEach((unit: Unit) => unit.setHotgroup(null));
this.selectedUnitsAddToHotgroup(hotgroup);
this.addToHotgroup(hotgroup);
}
/** Add the selected units to a hotgroup. Units can be in multiple hotgroups at the same type
*
* @param hotgroup Hotgroup number
*/
selectedUnitsAddToHotgroup(hotgroup: number) {
var selectedUnits = this.getSelectedUnits();
for (let idx in selectedUnits) {
selectedUnits[idx].setHotgroup(hotgroup);
}
this.#showActionMessage(selectedUnits, `added to hotgroup ${hotgroup}`);
addToHotgroup(hotgroup: number, units: Unit[] | null = null) {
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();
}
@ -810,9 +889,14 @@ export class UnitsManager {
* @param explosion If true, the unit will be deleted using an explosion
* @returns
*/
selectedUnitsDelete(explosion: boolean = false, explosionType: string = "") {
var selectedUnits = this.getSelectedUnits({ excludeProtected: true }); /* Can be applied to humans too */
const selectionContainsAHuman = selectedUnits.some((unit: Unit) => {
delete(explosion: boolean = false, explosionType: string = "", units: Unit[] | null = null) {
if (units === null)
units = this.getSelectedUnits({ excludeProtected: true }); /* Can be applied to humans too */
if (units.length === 0)
return;
const selectionContainsAHuman = units.some((unit: Unit) => {
return unit.getHuman() === true;
});
@ -821,14 +905,12 @@ export class UnitsManager {
}
const doDelete = (explosion = false, explosionType = "", immediate = false) => {
for (let idx in selectedUnits) {
selectedUnits[idx].delete(explosion, explosionType, immediate);
}
this.#showActionMessage(selectedUnits, `deleted`);
units?.forEach((unit: Unit) => unit.delete(explosion, explosionType, immediate));
this.#showActionMessage(units as Unit[], `deleted`);
}
if (selectedUnits.length >= DELETE_SLOW_THRESHOLD)
this.#showSlowDeleteDialog(selectedUnits).then((action: any) => {
if (units.length >= DELETE_SLOW_THRESHOLD)
this.#showSlowDeleteDialog(units).then((action: any) => {
if (action === "delete-slow")
doDelete(explosion, explosionType, false);
else if (action === "delete-immediate")
@ -845,19 +927,25 @@ export class UnitsManager {
* @param rotation Rotation of the group, in radians
* @returns Array of positions for each unit, in order
*/
selectedUnitsComputeGroupDestination(latlng: LatLng, rotation: number) {
var selectedUnits = this.getSelectedUnits({ excludeHumans: true, excludeProtected: true, onlyOnePerGroup: true });
computeGroupDestination(latlng: LatLng, rotation: number, units: Unit[] | null = null) {
if (units === null)
units = this.getSelectedUnits({ excludeHumans: true, excludeProtected: true, onlyOnePerGroup: true });
if (units.length === 0)
return {};
/* Compute the center of the group */
var len = units.length;
var center = { x: 0, y: 0 };
selectedUnits.forEach((unit: Unit) => {
units.forEach((unit: Unit) => {
var mercator = latLngToMercator(unit.getPosition().lat, unit.getPosition().lng);
center.x += mercator.x / selectedUnits.length;
center.y += mercator.y / selectedUnits.length;
center.x += mercator.x / len;
center.y += mercator.y / len;
});
/* Compute the distances from the center of the group */
var unitDestinations: { [key: number]: LatLng } = {};
selectedUnits.forEach((unit: Unit) => {
units.forEach((unit: Unit) => {
var mercator = latLngToMercator(unit.getPosition().lat, unit.getPosition().lng);
var distancesFromCenter = { dx: mercator.x - center.x, dy: mercator.y - center.y };
@ -879,9 +967,15 @@ export class UnitsManager {
/** Copy the selected units and store their properties in memory
*
*/
selectedUnitsCopy() {
copy(units: Unit[] | null = null) {
if (units === null)
units = this.getSelectedUnits({ excludeHumans: true });
if (units.length === 0)
return;
/* A JSON is used to deepcopy the units, creating a "snapshot" of their properties at the time of the copy */
this.#copiedUnits = JSON.parse(JSON.stringify(this.getSelectedUnits().map((unit: Unit) => { return unit.getData() }))); /* Can be applied to humans too */
this.#copiedUnits = JSON.parse(JSON.stringify(units.map((unit: Unit) => { return unit.getData() }))); /* Can be applied to humans too */
(getApp().getPopupsManager().get("infoPopup") as Popup).setText(`${this.#copiedUnits.length} units copied`);
}
@ -890,7 +984,7 @@ export class UnitsManager {
*
* @returns True if units were pasted successfully
*/
pasteUnits() {
paste() {
let spawnPoints = 0;
/* If spawns are restricted, check that the user has the necessary spawn points */
@ -1111,7 +1205,7 @@ export class UnitsManager {
#onKeyUp(event: KeyboardEvent) {
if (!keyEventWasInInput(event)) {
if (event.key === "Delete")
this.selectedUnitsDelete();
this.delete();
else if (event.key === "a" && event.ctrlKey)
Object.values(this.getUnits()).filter((unit: Unit) => { return !unit.getHidden() }).forEach((unit: Unit) => unit.setSelected(true));
}
@ -1160,9 +1254,9 @@ export class UnitsManager {
(getApp().getPopupsManager().get("infoPopup") as Popup).setText(`${units[0].getUnitName()} and ${units.length - 1} other units ${message}`);
}
#showSlowDeleteDialog(selectedUnits: Unit[]) {
#showSlowDeleteDialog(units: Unit[]) {
let button: HTMLButtonElement | null = null;
const deletionTime = Math.round(selectedUnits.length * DELETE_CYCLE_TIME).toString();
const deletionTime = Math.round(units.length * DELETE_CYCLE_TIME).toString();
const dialog = this.#slowDeleteDialog;
const element = dialog.getElement();
const listener = (ev: MouseEvent) => {
@ -1170,7 +1264,7 @@ export class UnitsManager {
button = ev.target;
}
element.querySelectorAll(".deletion-count").forEach(el => el.innerHTML = selectedUnits.length.toString());
element.querySelectorAll(".deletion-count").forEach(el => el.innerHTML = units.length.toString());
element.querySelectorAll(".deletion-time").forEach(el => el.innerHTML = deletionTime);
dialog.show();
@ -1190,9 +1284,9 @@ export class UnitsManager {
#showNumberOfSelectedProtectedUnits() {
const map = getApp().getMap();
const selectedUnits = this.getSelectedUnits();
const numSelectedUnits = selectedUnits.length;
const numProtectedUnits = selectedUnits.filter((unit: Unit) => map.unitIsProtected(unit)).length;
const units = this.getSelectedUnits();
const numSelectedUnits = units.length;
const numProtectedUnits = units.filter((unit: Unit) => map.unitIsProtected(unit)).length;
if (numProtectedUnits === 1 && numSelectedUnits === numProtectedUnits)
(getApp().getPopupsManager().get("infoPopup") as Popup).setText(`Notice: unit is protected`);