Merge branch 'main' into 412-improve-importexport

This commit is contained in:
Pax1601
2023-11-26 21:13:24 +01:00
82 changed files with 30904 additions and 17914 deletions

View File

@@ -0,0 +1,60 @@
import { Unit } from "./unit";
export interface ContextActionOptions {
isScenic?: boolean
}
export class ContextAction {
#id: string = "";
#label: string = "";
#description: string = "";
#callback: CallableFunction | null = null;
#units: Unit[] = [];
#hideContextAfterExecution: boolean = true
#options: ContextActionOptions;
constructor(id: string, label: string, description: string, callback: CallableFunction, hideContextAfterExecution: boolean = true, options: ContextActionOptions) {
this.#id = id;
this.#label = label;
this.#description = description;
this.#callback = callback;
this.#hideContextAfterExecution = hideContextAfterExecution;
this.#options = {
"isScenic": false,
...options
}
}
addUnit(unit: Unit) {
this.#units.push(unit);
}
getId() {
return this.#id;
}
getLabel() {
return this.#label;
}
getOptions() {
return this.#options;
}
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,25 @@
import { ContextAction, ContextActionOptions } 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, options?:ContextActionOptions) {
options = options || {};
if (!(id in this.#contextActions)) {
this.#contextActions[id] = new ContextAction(id, label, description, callback, hideContextAfterExecution, options);
}
this.#contextActions[id].addUnit(unit);
}
getContextActions() {
return this.#contextActions;
}
}

View File

@@ -1,5 +1,6 @@
import { getApp } from "../..";
import { GAME_MASTER } from "../../constants/constants";
import { UnitBlueprint } from "../../interfaces";
import { UnitDatabase } from "./unitdatabase"
export class AircraftDatabase extends UnitDatabase {

View File

@@ -3,7 +3,7 @@ import { getApp } from "../..";
import { GAME_MASTER } from "../../constants/constants";
import { UnitBlueprint } from "../../interfaces";
export class UnitDatabase {
export abstract class UnitDatabase {
blueprints: { [key: string]: UnitBlueprint } = {};
#url: string;
@@ -31,9 +31,7 @@ export class UnitDatabase {
}
}
getCategory() {
return "";
}
abstract getCategory(): string;
/* Gets a specific blueprint by name */
getByName(name: string) {
@@ -240,4 +238,15 @@ export class UnitDatabase {
getSpawnPointsByName(name: string) {
return Infinity;
}
getUnkownUnit(name: string): UnitBlueprint {
return {
name: name,
enabled: true,
coalition: 'neutral',
era: 'N/A',
label: name,
shortLabel: ''
}
}
}

View File

@@ -1,11 +1,11 @@
import { Marker, LatLng, Polyline, Icon, DivIcon, CircleMarker, Map, Point, Circle } from 'leaflet';
import { Marker, LatLng, Polyline, Icon, DivIcon, CircleMarker, Map, Point } from 'leaflet';
import { getApp } from '..';
import { enumToCoalition, enumToEmissioNCountermeasure, getMarkerCategoryByName, enumToROE, enumToReactionToThreat, enumToState, getUnitDatabaseByCategory, mToFt, msToKnots, rad2deg, bearing, deg2rad, ftToM, getGroundElevation, coalitionToEnum, nmToFt, nmToM } from '../other/utils';
import { enumToCoalition, enumToEmissioNCountermeasure, enumToROE, enumToReactionToThreat, enumToState, getUnitDatabaseByCategory, mToFt, msToKnots, rad2deg, bearing, deg2rad, ftToM, getGroundElevation, coalitionToEnum, nmToFt, nmToM } from '../other/utils';
import { CustomMarker } from '../map/markers/custommarker';
import { SVGInjector } from '@tanem/svg-injector';
import { UnitDatabase } from './databases/unitdatabase';
import { TargetMarker } from '../map/markers/targetmarker';
import { DLINK, DataIndexes, GAME_MASTER, HIDE_GROUP_MEMBERS, IDLE, IRST, MOVE_UNIT, OPTIC, RADAR, ROEs, RWR, SHOW_UNIT_CONTACTS, SHOW_UNITS_ENGAGEMENT_RINGS, SHOW_UNIT_PATHS, SHOW_UNIT_TARGETS, VISUAL, emissionsCountermeasures, reactionsToThreat, states, SHOW_UNITS_ACQUISITION_RINGS, HIDE_UNITS_SHORT_RANGE_RINGS, FILL_SELECTED_RING, GROUPING_ZOOM_TRANSITION, GROUND_UNIT_AIR_DEFENCE_REGEX } from '../constants/constants';
import { DLINK, DataIndexes, GAME_MASTER, HIDE_GROUP_MEMBERS, IDLE, IRST, MOVE_UNIT, OPTIC, RADAR, ROEs, RWR, SHOW_UNIT_CONTACTS, SHOW_UNITS_ENGAGEMENT_RINGS, SHOW_UNIT_PATHS, SHOW_UNIT_TARGETS, VISUAL, emissionsCountermeasures, reactionsToThreat, states, SHOW_UNITS_ACQUISITION_RINGS, HIDE_UNITS_SHORT_RANGE_RINGS, FILL_SELECTED_RING, GROUPING_ZOOM_TRANSITION, MAX_SHOTS_SCATTER, SHOTS_SCATTER_DEGREES, GROUND_UNIT_AIR_DEFENCE_REGEX } from '../constants/constants';
import { DataExtractor } from '../server/dataextractor';
import { groundUnitDatabase } from './databases/groundunitdatabase';
import { navyUnitDatabase } from './databases/navyunitdatabase';
@@ -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 * as turf from "@turf/turf";
var pathIcon = new Icon({
iconUrl: '/resources/theme/images/markers/marker-icon.png',
@@ -190,15 +192,17 @@ export abstract class Unit extends CustomMarker {
/* Deselect units if they are hidden */
document.addEventListener("toggleCoalitionVisibility", (ev: CustomEventInit) => {
window.setTimeout(() => { this.setSelected(this.getSelected() && !this.getHidden()) }, 300);
this.#updateMarker();
this.setSelected(this.getSelected() && !this.getHidden());
});
document.addEventListener("toggleUnitVisibility", (ev: CustomEventInit) => {
window.setTimeout(() => { this.setSelected(this.getSelected() && !this.getHidden()) }, 300);
document.addEventListener("toggleMarkerVisibility", (ev: CustomEventInit) => {
this.#updateMarker();
this.setSelected(this.getSelected() && !this.getHidden());
});
/* Update the marker when the visibility options change */
document.addEventListener("mapVisibilityOptionsChanged", (ev: CustomEventInit) => {
/* Update the marker when the options change */
document.addEventListener("mapOptionsChanged", (ev: CustomEventInit) => {
this.#updateMarker();
/* Circles don't like to be updated when the map is zooming */
@@ -212,7 +216,7 @@ export abstract class Unit extends CustomMarker {
});
}
/********************** Abstract methods *************************/
/********************** Abstract methods *************************/
/** Get the unit category string
*
* @returns string The unit category
@@ -228,9 +232,20 @@ 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;
/**
*
* @returns string containing the marker category
*/
abstract getMarkerCategory(): string;
/**
*
* @returns string containing the default marker
*/
abstract getDefaultMarker(): string;
/** Get the category but for display use - for the user. (i.e. has spaces in it)
*
@@ -248,7 +263,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) {
@@ -320,7 +335,7 @@ export abstract class Unit extends CustomMarker {
}
/* If the unit is selected or if the view is centered on this unit, sent the update signal so that other elements like the UnitControlPanel can be updated. */
if (this.getSelected() || getApp().getMap().getCenterUnit() === this)
if (this.getSelected() || getApp().getMap().getCenteredOnUnit() === this)
document.dispatchEvent(new CustomEvent("unitUpdated", { detail: this }));
}
@@ -379,14 +394,6 @@ export abstract class Unit extends CustomMarker {
}
}
/**
*
* @returns string containing the marker category
*/
getMarkerCategory(): string {
return getMarkerCategoryByName(this.getName());
}
/** Get a database of information also in this unit's category
*
* @returns UnitDatabase
@@ -421,7 +428,7 @@ export abstract class Unit extends CustomMarker {
else {
this.#clearContacts();
this.#clearPath();
this.#clearTarget();
this.#clearTargetPosition();
}
/* When the group leader is selected, if grouping is active, all the other group members are also selected */
@@ -456,7 +463,7 @@ export abstract class Unit extends CustomMarker {
return this.#selected;
}
/** Set the number of the hotgroup to which the unit belongs
/** Set the number of the hotgroup to which the unit belongss
*
* @param hotgroup (number)
*/
@@ -498,7 +505,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 [];
}
@@ -508,7 +515,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;
}
@@ -530,7 +537,7 @@ export abstract class Unit extends CustomMarker {
}
getDatabaseEntry() {
return this.getDatabase()?.getByName(this.#name);
return this.getDatabase()?.getByName(this.#name) ?? this.getDatabase()?.getUnkownUnit(this.getName());
}
getGroup() {
@@ -572,7 +579,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");
@@ -600,7 +607,7 @@ export abstract class Unit extends CustomMarker {
/* If a unit does not belong to the commanded coalition or it is not visually detected, show it with the generic aircraft square */
var marker;
if (this.belongsToCommandedCoalition() || this.getDetectionMethods().some(value => [VISUAL, OPTIC].includes(value)))
marker = this.getDatabaseEntry()?.markerFile ?? this.getMarkerCategory();
marker = this.getDatabaseEntry()?.markerFile ?? this.getDefaultMarker();
else
marker = "aircraft";
img.src = `/resources/theme/images/units/${marker}.svg`;
@@ -690,13 +697,11 @@ 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)))) &&
!(this.getSelected()
);
(getApp().getMap().getVisibilityOptions()[HIDE_GROUP_MEMBERS] && !this.#isLeader && !this.getSelected() && this.getCategory() == "GroundUnit" && getApp().getMap().getZoom() < GROUPING_ZOOM_TRANSITION &&
(this.belongsToCommandedCoalition() || (!this.belongsToCommandedCoalition() && this.#detectionMethods.length == 0))));
/* Force dead units to be hidden */
this.setHidden(hidden || !this.#alive);
this.setHidden(hidden || !this.getAlive());
}
setHidden(hidden: boolean) {
@@ -776,15 +781,11 @@ export abstract class Unit extends CustomMarker {
return this.getDatabaseEntry()?.canRearm === true;
}
canLandAtPoint() {
return this.getCategory() === "Helicopter";
}
canAAA() {
return this.getDatabaseEntry()?.canAAA === true;
}
indirectFire() {
isIndirectFire() {
return this.getDatabaseEntry()?.indirectFire === true;
}
@@ -941,6 +942,7 @@ export abstract class Unit extends CustomMarker {
});
}
// TODO: Remove coalition
scenicAAA() {
var coalition = "neutral";
if (this.getCoalition() === "red")
@@ -950,6 +952,7 @@ export abstract class Unit extends CustomMarker {
getApp().getServerManager().scenicAAA(this.ID, coalition);
}
// TODO: Remove coalition
missOnPurpose() {
var coalition = "neutral";
if (this.getCoalition() === "red")
@@ -973,24 +976,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);
@@ -1001,6 +986,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 */
@@ -1039,102 +1074,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);
}
}
@@ -1313,11 +1266,13 @@ export abstract class Unit extends CustomMarker {
}
#clearPath() {
for (let WP in this.#pathMarkers) {
getApp().getMap().removeLayer(this.#pathMarkers[WP]);
if (this.#pathPolyline.getLatLngs().length != 0) {
for (let WP in this.#pathMarkers) {
getApp().getMap().removeLayer(this.#pathMarkers[WP]);
}
this.#pathMarkers = [];
this.#pathPolyline.setLatLngs([]);
}
this.#pathMarkers = [];
this.#pathPolyline.setLatLngs([]);
}
#drawContacts() {
@@ -1471,7 +1426,7 @@ export abstract class Unit extends CustomMarker {
}
}
else
this.#clearTarget();
this.#clearTargetPosition();
}
#drawTargetPosition(targetPosition: LatLng) {
@@ -1480,10 +1435,25 @@ export abstract class Unit extends CustomMarker {
if (!getApp().getMap().hasLayer(this.#targetPositionPolyline))
this.#targetPositionPolyline.addTo(getApp().getMap());
this.#targetPositionMarker.setLatLng(new LatLng(targetPosition.lat, targetPosition.lng));
this.#targetPositionPolyline.setLatLngs([new LatLng(this.#position.lat, this.#position.lng), new LatLng(targetPosition.lat, targetPosition.lng)])
if (this.getState() === 'simulate-fire-fight' && this.getShotsScatter() != MAX_SHOTS_SCATTER) {
let turfUnitPosition = turf.point([this.getPosition().lng, this.getPosition().lat]);
let turfTargetPosition = turf.point([targetPosition.lng, targetPosition.lat]);
let bearing = turf.bearing(turfUnitPosition, turfTargetPosition);
let scatterDistance = turf.distance(turfUnitPosition, turfTargetPosition) * Math.tan((MAX_SHOTS_SCATTER - this.getShotsScatter()) * deg2rad(SHOTS_SCATTER_DEGREES));
let destination1 = turf.destination(turfTargetPosition, scatterDistance, bearing + 90);
let destination2 = turf.destination(turfTargetPosition, scatterDistance, bearing - 90);
this.#targetPositionPolyline.setStyle({dashArray: "4, 8"});
this.#targetPositionPolyline.setLatLngs([new LatLng(destination1.geometry.coordinates[1], destination1.geometry.coordinates[0]), new LatLng(this.#position.lat, this.#position.lng), new LatLng(destination2.geometry.coordinates[1], destination2.geometry.coordinates[0])])
} else {
this.#targetPositionPolyline.setStyle({dashArray: ""});
this.#targetPositionPolyline.setLatLngs([new LatLng(this.#position.lat, this.#position.lng), new LatLng(targetPosition.lat, targetPosition.lng)])
}
}
#clearTarget() {
#clearTargetPosition() {
if (getApp().getMap().hasLayer(this.#targetPositionMarker))
this.#targetPositionMarker.removeFrom(getApp().getMap());
@@ -1492,7 +1462,7 @@ export abstract class Unit extends CustomMarker {
}
#onZoom(e: any) {
if (this.checkZoomRedraw())
if (this.checkZoomRedraw())
this.#redrawMarker();
this.#updateMarker();
}
@@ -1516,35 +1486,24 @@ 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) {
contextActionSet.addContextAction(this, "attack", "Attack unit", "Attack the unit using A/A or A/G weapons", (units: Unit[]) => { getApp().getUnitsManager().attackUnit(targetUnit.ID, units) });
contextActionSet.addContextAction(this, "follow", "Follow unit", "Follow this unit in formation", (units: Unit[]) => { targetUnit.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;
}
}
@@ -1556,6 +1515,22 @@ export class Aircraft extends AirUnit {
getCategory() {
return "Aircraft";
}
appendContextActions(contextActionSet: ContextActionSet, targetUnit: Unit | null, targetPosition: LatLng | null) {
super.appendContextActions(contextActionSet, targetUnit, targetPosition);
if (targetPosition === null && this.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) });
}
}
getMarkerCategory() {
return "aircraft";
}
getDefaultMarker() {
return "aircraft";
}
}
export class Helicopter extends AirUnit {
@@ -1566,6 +1541,21 @@ 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) });
}
getMarkerCategory() {
return "helicopter";
}
getDefaultMarker() {
return "helicopter";
}
}
export class GroundUnit extends Unit {
@@ -1590,37 +1580,35 @@ 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.\nWARNING: works correctly only on neutral units, blue or red units will aim", (units: Unit[]) => { getApp().getUnitsManager().simulateFireFight(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.\nWARNING: works correctly only on neutral units, blue or red units will aim", (units: Unit[]) => { getApp().getUnitsManager().scenicAAA(units) }, undefined, {
"isScenic": true
});
contextActionSet.addContextAction(this, "miss-aaa", "Miss on purpose", "Shoot AAA towards the closest enemy unit, but don't aim precisely.\nWARNING: works correctly only on neutral units, blue or red units will aim", (units: Unit[]) => { getApp().getUnitsManager().missOnPurpose(units) }, undefined, {
"isScenic": true
});
}
}
return options;
}
getCategory() {
@@ -1645,9 +1633,9 @@ export class GroundUnit extends Unit {
unitWhenGrouped = (member !== null ? member?.getDatabaseEntry()?.unitWhenGrouped : unitWhenGrouped);
}
if (unitWhenGrouped)
return this.getDatabase()?.getByName(unitWhenGrouped);
return this.getDatabase()?.getByName(unitWhenGrouped) ?? this.getDatabase()?.getUnkownUnit(this.getName());
else
return this.getDatabase()?.getByName(this.getName());
return this.getDatabase()?.getByName(this.getName()) ?? this.getDatabase()?.getUnkownUnit(this.getName());
}
/* When we zoom past the grouping limit, grouping is enabled and the unit is a leader, we redraw the unit to apply any possible grouped marker */
@@ -1656,6 +1644,17 @@ export class GroundUnit extends Unit {
(getApp().getMap().getZoom() >= GROUPING_ZOOM_TRANSITION && getApp().getMap().getPreviousZoom() < GROUPING_ZOOM_TRANSITION ||
getApp().getMap().getZoom() < GROUPING_ZOOM_TRANSITION && getApp().getMap().getPreviousZoom() >= GROUPING_ZOOM_TRANSITION))
}
getMarkerCategory() {
if (/\bAAA|SAM\b/.test(this.getType()) || /\bmanpad|stinger\b/i.test(this.getType()))
return "groundunit-sam";
else
return "groundunit";
}
getDefaultMarker() {
return "groundunit";
}
}
export class NavyUnit extends Unit {
@@ -1680,36 +1679,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" };
}
}
/* All other options */
else {
/* Provision */
}
return options;
}
getMarkerCategory() {
return "navyunit";
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) });
}
}
getCategory() {
@@ -1720,4 +1704,12 @@ export class NavyUnit extends Unit {
var blueprint = navyUnitDatabase.getByName(this.getName());
return blueprint?.type ? blueprint.type : "";
}
getMarkerCategory() {
return "navyunit";
}
getDefaultMarker() {
return "navyunit";
}
}

View File

@@ -6,7 +6,7 @@ import { CoalitionArea } from "../map/coalitionarea/coalitionarea";
import { groundUnitDatabase } from "./databases/groundunitdatabase";
import { DELETE_CYCLE_TIME, DELETE_SLOW_THRESHOLD, DataIndexes, GAME_MASTER, IADSDensities, IDLE, MOVE_UNIT } from "../constants/constants";
import { DataExtractor } from "../server/dataextractor";
import { citiesDatabase } from "./citiesDatabase";
import { citiesDatabase } from "./databases/citiesdatabase";
import { aircraftDatabase } from "./databases/aircraftdatabase";
import { helicopterDatabase } from "./databases/helicopterdatabase";
import { navyUnitDatabase } from "./databases/navyunitdatabase";
@@ -40,15 +40,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() });
@@ -134,8 +134,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();
@@ -145,7 +145,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);
}
}
@@ -264,6 +264,10 @@ export class UnitsManager {
if (options.showProtectionReminder === true && numProtectedUnits > selectedUnits.length && selectedUnits.length === 0) {
const messageText = (numProtectedUnits === 1) ? `Unit is protected` : `All selected units are protected`;
(getApp().getPopupsManager().get("infoPopup") as Popup).setText(messageText);
// Cheap way for now until we use more locks
let lock = <HTMLElement>document.querySelector("#unit-visibility-control button.lock");
lock.classList.add("prompt");
setTimeout(() => lock.classList.remove("prompt"), 4000);
}
if (options.onlyOnePerGroup) {
@@ -321,11 +325,13 @@ export class UnitsManager {
getUnitsVariable(variableGetter: CallableFunction, units: Unit[]) {
if (units.length == 0)
return undefined;
return units.map((unit: Unit) => {
return variableGetter(unit);
})?.reduce((a: any, b: any) => {
return a === b ? a : undefined
var value: any = variableGetter(units[0]);
units.forEach((unit: Unit) => {
if (variableGetter(unit) !== value)
value = undefined;
});
return value;
};
/** For a given unit, it returns if and how it is being detected by other units. NOTE: this function will return how a unit is being detected, i.e. how other units are detecting it. It will not return
@@ -353,23 +359,23 @@ export class UnitsManager {
* @param latlng Position of the new destination
* @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)
* @param units (Optional) Array of units to apply the control to. If not provided, the operation will be completed on all selected units.
*/
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())
@@ -382,18 +388,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())
@@ -409,179 +419,239 @@ export class UnitsManager {
/** Instruct all the selected units to land at a specific location
*
* @param latlng Location where to land at
* @param units (Optional) Array of units to apply the control to. If not provided, the operation will be completed on all selected units.
*/
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
* @param units (Optional) Array of units to apply the control to. If not provided, the operation will be completed on all selected units.
*/
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
* @param units (Optional) Array of units to apply the control to. If not provided, the operation will be completed on all selected units.
*/
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
* @param units (Optional) Array of units to apply the control to. If not provided, the operation will be completed on all selected units.
*/
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.
* @param units (Optional) Array of units to apply the control to. If not provided, the operation will be completed on all selected units.
*/
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
* @param units (Optional) Array of units to apply the control to. If not provided, the operation will be completed on all selected units.
*/
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.
* @param units (Optional) Array of units to apply the control to. If not provided, the operation will be completed on all selected units.
*/
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
* @param units (Optional) Array of units to apply the control to. If not provided, the operation will be completed on all selected units.
*/
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
* @param units (Optional) Array of units to apply the control to. If not provided, the operation will be completed on all selected units.
*/
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
* @param units (Optional) Array of units to apply the control to. If not provided, the operation will be completed on all selected units.
*/
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
* @param units (Optional) Array of units to apply the control to. If not provided, the operation will be completed on all selected units.
*/
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
* @param units (Optional) Array of units to apply the control to. If not provided, the operation will be completed on all selected units.
*/
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
* @param units (Optional) Array of units to apply the control to. If not provided, the operation will be completed on all selected units.
*/
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
* @param units (Optional) Array of units to apply the control to. If not provided, the operation will be completed on all selected units.
*/
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
*
* @param units (Optional) Array of units to apply the control to. If not provided, the operation will be completed on all selected units.
*/
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.
@@ -589,8 +659,15 @@ export class UnitsManager {
* @param ID ID of the unit to follow
* @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"
* @param units (Optional) Array of units to apply the control to. If not provided, the operation will be completed on all selected units.
*/
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 };
@@ -603,16 +680,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 */
@@ -634,51 +706,69 @@ 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
* @param units (Optional) Array of units to apply the control to. If not provided, the operation will be completed on all selected units.
*/
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
* @param units (Optional) Array of units to apply the control to. If not provided, the operation will be completed on all selected units.
*/
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
* @param units (Optional) Array of units to apply the control to. If not provided, the operation will be completed on all selected units.
*/
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
* @param units (Optional) Array of units to apply the control to. If not provided, the operation will be completed on all selected units.
*/
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 {
@@ -686,70 +776,86 @@ 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
*
* @param units (Optional) Array of units to apply the control to. If not provided, the operation will be completed on all selected units.
*/
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.
*
* @param units (Optional) Array of units to apply the control to. If not provided, the operation will be completed on all selected units.
*/
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
* @param units (Optional) Array of units to apply the control to. If not provided, the operation will be completed on all selected units.
*/
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
* @param units (Optional) Array of units to apply the control to. If not provided, the operation will be completed on all selected units.
*/
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
* @param units (Optional) Array of units to apply the control to. If not provided, the operation will be completed on all selected units.
*/
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 ************************/
@@ -773,15 +879,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`);
}
@@ -790,33 +899,40 @@ export class UnitsManager {
/** Set the hotgroup for the selected units. It will be the only hotgroup of the unit
*
* @param hotgroup Hotgroup number
* @param units (Optional) Array of units to apply the control to. If not provided, the operation will be completed on all selected units.
*/
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
* @param units (Optional) Array of units to apply the control to. If not provided, the operation will be completed on all selected units.
*/
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();
}
/** Delete the selected units
*
* @param explosion If true, the unit will be deleted using an explosion
* @param units (Optional) Array of units to apply the control to. If not provided, the operation will be completed on all selected units.
* @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, showProtectionReminder: true }); /* Can be applied to humans too */
if (units.length === 0)
return;
const selectionContainsAHuman = units.some((unit: Unit) => {
return unit.getHuman() === true;
});
@@ -825,14 +941,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")
@@ -847,21 +961,28 @@ export class UnitsManager {
*
* @param latlng Center of the group after the translation
* @param rotation Rotation of the group, in radians
* @param units (Optional) Array of units to apply the control to. If not provided, the operation will be completed on all selected units.
* @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 };
@@ -883,9 +1004,18 @@ export class UnitsManager {
/** Copy the selected units and store their properties in memory
*
*/
selectedUnitsCopy() {
copy(units: Unit[] | null = null) {
if ( !getApp().getContextManager().getCurrentContext().getAllowUnitCopying() )
return;
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`);
}
@@ -894,7 +1024,10 @@ export class UnitsManager {
*
* @returns True if units were pasted successfully
*/
pasteUnits() {
paste() {
if ( !getApp().getContextManager().getCurrentContext().getAllowUnitPasting() )
return;
let spawnPoints = 0;
/* If spawns are restricted, check that the user has the necessary spawn points */
@@ -1081,7 +1214,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));
}
@@ -1130,9 +1263,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) => {
@@ -1140,7 +1273,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();
@@ -1160,9 +1293,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.getIsUnitProtected(unit)).length;
if (numProtectedUnits === 1 && numSelectedUnits === numProtectedUnits)
(getApp().getPopupsManager().get("infoPopup") as Popup).setText(`Notice: unit is protected`);
@@ -1172,6 +1305,6 @@ export class UnitsManager {
}
#unitIsProtected(unit: Unit) {
return getApp().getMap().unitIsProtected(unit)
return getApp().getMap().getIsUnitProtected(unit)
}
}