Merge and fix

This commit is contained in:
PeekabooSteam
2023-11-16 11:28:32 +00:00
89 changed files with 18208 additions and 16222 deletions

View File

@@ -259,6 +259,7 @@ export enum DataIndexes {
operateAs,
shotsScatter,
shotsIntensity,
health,
endOfData = 255
};
@@ -269,4 +270,6 @@ export const MGRS_PRECISION_10M = 5;
export const MGRS_PRECISION_1M = 6;
export const DELETE_CYCLE_TIME = 0.05;
export const DELETE_SLOW_THRESHOLD = 50;
export const DELETE_SLOW_THRESHOLD = 50;
export const GROUPING_ZOOM_TRANSITION = 13;

View File

@@ -58,7 +58,7 @@ export class MapContextMenu extends ContextMenu {
document.addEventListener("contextMenuExplosion", (e: any) => {
this.hide();
getApp().getServerManager().spawnExplosion(e.detail.strength, this.getLatLng());
getApp().getServerManager().spawnExplosion(e.detail.strength ?? 0, e.detail.explosionType, this.getLatLng());
});
document.addEventListener("editCoalitionArea", (e: any) => {

View File

@@ -36,18 +36,37 @@ export class Dropdown {
return this.#container;
}
setOptions(optionsList: string[], sort:""|"string"|"number" = "string") {
if ( sort === "number" ) {
this.#optionsList = optionsList.sort( (optionA:string, optionB:string) => {
const a = parseInt( optionA );
const b = parseInt( optionB );
if ( a > b )
setOptions(optionsList: string[], sort: "" | "string" | "number" | "string+number" = "string") {
if (sort === "number") {
this.#optionsList = optionsList.sort((optionA: string, optionB: string) => {
const a = parseInt(optionA);
const b = parseInt(optionB);
if (a > b)
return 1;
else
return ( b > a ) ? -1 : 0;
return (b > a) ? -1 : 0;
});
} else if ( sort === "string" ) {
} else if (sort === "string+number") {
this.#optionsList = optionsList.sort((optionA: string, optionB: string) => {
var regex = /\d+/g;
var matchesA = optionA.match(regex);
var matchesB = optionB.match(regex);
if ((matchesA != null && matchesA?.length > 0) && (matchesB != null && matchesB?.length > 0) && optionA[0] == optionB[0]) {
const a = parseInt(matchesA[0] ?? 0);
const b = parseInt(matchesB[0] ?? 0);
if (a > b)
return 1;
else
return (b > a) ? -1 : 0;
} else {
if (optionA > optionB)
return 1;
else
return (optionB > optionA) ? -1 : 0;
}
});
} else if (sort === "string") {
this.#optionsList = optionsList.sort();
}
@@ -169,17 +188,17 @@ export class Dropdown {
}
#toggle() {
this.#container.classList.contains("is-open")? this.close(): this.open();
this.#container.classList.contains("is-open") ? this.close() : this.open();
}
#createElement(defaultText: string | undefined) {
var div = document.createElement("div");
div.classList.add("ol-select");
var value = document.createElement("div");
value.classList.add("ol-select-value");
value.innerText = defaultText? defaultText: "";
value.innerText = defaultText ? defaultText : "";
var options = document.createElement("div");
options.classList.add("ol-select-options");

View File

@@ -154,9 +154,28 @@ export class UnitSpawnMenu {
this.#unitLiveryDropdown.reset();
if (this.#orderByRole)
this.#unitLabelDropdown.setOptions(this.#unitDatabase.getByRole(this.spawnOptions.roleType).map((blueprint) => { return blueprint.label }));
this.#unitLabelDropdown.setOptions(this.#unitDatabase.getByRole(this.spawnOptions.roleType).map((blueprint) => { return blueprint.label }), "string+number");
else
this.#unitLabelDropdown.setOptions(this.#unitDatabase.getByType(this.spawnOptions.roleType).map((blueprint) => { return blueprint.label }));
this.#unitLabelDropdown.setOptions(this.#unitDatabase.getByType(this.spawnOptions.roleType).map((blueprint) => { return blueprint.label }), "string+number");
/* Add the tags to the options */
var elements: HTMLElement[] = [];
for (let idx = 0; idx < this.#unitLabelDropdown.getOptionElements().length; idx++) {
let element = this.#unitLabelDropdown.getOptionElements()[idx] as HTMLElement;
let entry = this.#unitDatabase.getByLabel(element.textContent ?? "");
if (entry) {
element.querySelectorAll("button")[0]?.append(...(entry.tags?.split(",").map((tag: string) => {
tag = tag.trim();
let el = document.createElement("div");
el.classList.add("pill", `ol-tag`, `ol-tag-${tag.replace(/[\W_]+/g,"-")}`);
el.textContent = tag;
element.appendChild(el);
return el;
}) ?? []));
elements.push(element);
}
}
this.#container.dispatchEvent(new Event("resize"));
this.spawnOptions.name = "";

View File

@@ -86,6 +86,7 @@ export interface UnitSpawnTable {
export interface ObjectIconOptions {
showState: boolean,
showVvi: boolean,
showHealth: boolean,
showHotgroup: boolean,
showUnitIcon: boolean,
showShortLabel: boolean,
@@ -182,6 +183,7 @@ export interface UnitData {
operateAs: string;
shotsScatter: number;
shotsIntensity: number;
health: number;
}
export interface LoadoutItemBlueprint {
@@ -219,6 +221,7 @@ export interface UnitBlueprint {
shotsBaseScatter?: number;
description?: string;
abilities?: string;
tags?: string;
acquisitionRange?: number;
engagementRange?: number;
targetingRange?: number;
@@ -228,6 +231,8 @@ export interface UnitBlueprint {
canRearm?: boolean;
canAAA?: boolean;
indirectFire?: boolean;
markerFile?: string;
unitWhenGrouped?: string;
}
export interface UnitSpawnOptions {

View File

@@ -68,6 +68,7 @@ export class Map extends L.Map {
#temporaryMarkers: TemporaryUnitMarker[] = [];
#selecting: boolean = false;
#isZooming: boolean = false;
#previousZoom: number = 0;
#destinationGroupRotation: number = 0;
#computeDestinationRotation: boolean = false;
@@ -102,8 +103,6 @@ export class Map extends L.Map {
constructor(ID: string){
/* Init the leaflet map */
super(ID, {
zoomSnap: 0,
zoomDelta: 0.25,
preferCanvas: true,
doubleClickZoom: false,
zoomControl: false,
@@ -503,6 +502,10 @@ export class Map extends L.Map {
return this.#visibilityOptions;
}
getPreviousZoom() {
return this.#previousZoom;
}
/* Event handlers */
#onClick(e: any) {
if (!this.#preventLeftClick) {
@@ -703,6 +706,7 @@ export class Map extends L.Map {
}
#onZoomStart(e: any) {
this.#previousZoom = this.getZoom();
if (this.#centerUnit != null)
this.#panToUnit(this.#centerUnit);
this.#isZooming = true;

View File

@@ -36,6 +36,7 @@ export class TemporaryUnitMarker extends CustomMarker {
createIcon() {
const category = getMarkerCategoryByName(this.#name);
const databaseEntry = getUnitDatabaseByCategory(category)?.getByName(this.#name);
/* Set the icon */
var icon = new DivIcon({
@@ -54,7 +55,8 @@ export class TemporaryUnitMarker extends CustomMarker {
var unitIcon = document.createElement("div");
unitIcon.classList.add("unit-icon");
var img = document.createElement("img");
img.src = `/resources/theme/images/units/${category}.svg`;
img.src = `/resources/theme/images/units/${databaseEntry?.markerFile ?? category}.svg`;
img.onload = () => SVGInjector(img);
unitIcon.appendChild(img);
unitIcon.toggleAttribute("data-rotate-to-heading", false);
@@ -64,7 +66,7 @@ export class TemporaryUnitMarker extends CustomMarker {
if (category == "aircraft" || category == "helicopter") {
var shortLabel = document.createElement("div");
shortLabel.classList.add("unit-short-label");
shortLabel.innerText = getUnitDatabaseByCategory(category)?.getByName(this.#name)?.shortLabel || "";
shortLabel.innerText = databaseEntry?.shortLabel || "";
el.append(shortLabel);
}

View File

@@ -0,0 +1,56 @@
// @ts-nocheck
// This is a horrible hack. But it is needed at the moment to ovveride a default behaviour of Leaflet. TODO please fix me the proper way.
import { Circle, Point, Polyline } from 'leaflet';
/**
* This custom Circle object implements a faster render method for very big circles. When zoomed in, the default ctx.arc method
* is very slow since the circle is huge. Also, when zoomed in most of the circle points will be outside the screen and not needed. This
* simpler, faster renderer approximates the circle with line segements and only draws those currently visibile.
* A more refined version using arcs could be implemented but this works good enough.
*/
export class RangeCircle extends Circle {
_updatePath() {
if (!this._renderer._drawing || this._empty()) { return; }
var p = this._point,
ctx = this._renderer._ctx,
r = Math.max(Math.round(this._radius), 1),
s = (Math.max(Math.round(this._radiusY), 1) || r) / r;
if (s !== 1) {
ctx.save();
ctx.scale(1, s);
}
let pathBegun = false;
let dtheta = Math.PI * 2 / 120;
for (let theta = 0; theta <= Math.PI * 2; theta += dtheta) {
let p1 = new Point(p.x + r * Math.cos(theta), p.y / s + r * Math.sin(theta));
let p2 = new Point(p.x + r * Math.cos(theta + dtheta), p.y / s + r * Math.sin(theta + dtheta));
let l1 = this._map.layerPointToLatLng(p1);
let l2 = this._map.layerPointToLatLng(p2);
let line = new Polyline([l1, l2]);
if (this._map.getBounds().intersects(line.getBounds())) {
if (!pathBegun) {
ctx.beginPath();
ctx.moveTo(p1.x, p1.y);
pathBegun = true;
}
ctx.lineTo(p2.x, p2.y);
}
else {
if (pathBegun) {
this._renderer._fillStroke(ctx, this);
pathBegun = false;
}
}
}
if (pathBegun)
this._renderer._fillStroke(ctx, this);
if (s !== 1)
ctx.restore();
}
}

View File

@@ -192,6 +192,10 @@ export class OlympusApp {
this.#unitsManager = new UnitsManager();
this.#weaponsManager = new WeaponsManager();
// Toolbars
this.getToolbarsManager().add("primaryToolbar", new PrimaryToolbar("primary-toolbar"))
.add("commandModeToolbar", new CommandModeToolbar("command-mode-toolbar"));
// Panels
this.getPanelsManager()
.add("connectionStatus", new ConnectionStatusPanel("connection-status-panel"))
@@ -206,11 +210,7 @@ export class OlympusApp {
// Popups
this.getPopupsManager()
.add("infoPopup", new Popup("info-popup"));
// Toolbars
this.getToolbarsManager().add("primaryToolbar", new PrimaryToolbar("primary-toolbar"))
.add("commandModeToolbar", new CommandModeToolbar("command-mode-toolbar"));
this.#pluginsManager = new PluginsManager();
/* Load the config file from the app server*/

View File

@@ -339,18 +339,14 @@ export function getMarkerCategoryByName(name: string) {
else if (helicopterDatabase.getByName(name) != null)
return "helicopter";
else if (groundUnitDatabase.getByName(name) != null){
var type = groundUnitDatabase.getByName(name)?.type;
if (type === "SAM")
var type = groundUnitDatabase.getByName(name)?.type ?? "";
if (/\bAAA|SAM\b/.test(type) || /\bmanpad|stinger\b/i.test(type))
return "groundunit-sam";
else if (type === "SAM Search radar" || type === "SAM Track radar" || type === "SAM Search/Track radar")
return "groundunit-sam-radar";
else if (type === "SAM Launcher")
return "groundunit-sam-launcher";
else if (type === "Radar")
return "groundunit-ewr";
else
return "groundunit-other";
}
else if (navyUnitDatabase.getByName(name) != null)
return "navyunit";
else
return "groundunit-other"; // TODO add other unit types
}

View File

@@ -9,6 +9,7 @@ import { Switch } from "../controls/switch";
import { ROEDescriptions, ROEs, altitudeIncrements, emissionsCountermeasures, emissionsCountermeasuresDescriptions, maxAltitudeValues, maxSpeedValues, minAltitudeValues, minSpeedValues, reactionsToThreat, reactionsToThreatDescriptions, shotsIntensityDescriptions, shotsScatterDescriptions, speedIncrements } from "../constants/constants";
import { ftToM, knotsToMs, mToFt, msToKnots } from "../other/utils";
import { GeneralSettings, Radio, TACAN } from "../interfaces";
import { PrimaryToolbar } from "../toolbars/primarytoolbar";
export class UnitControlPanel extends Panel {
#altitudeSlider: Slider;
@@ -27,6 +28,7 @@ export class UnitControlPanel extends Panel {
#advancedSettingsDialog: HTMLElement;
#units: Unit[] = [];
#selectedUnitsTypes: string[] = [];
#deleteDropdown: Dropdown;
/**
*
@@ -112,6 +114,7 @@ export class UnitControlPanel extends Panel {
this.#radioDecimalsDropdown = new Dropdown("radio-decimals", () => {});
this.#radioDecimalsDropdown.setOptions([".000", ".250", ".500", ".750"]);
this.#radioCallsignDropdown = new Dropdown("radio-callsign", () => {});
this.#deleteDropdown = new Dropdown("delete-options", () => { });
/* Events and timer */
window.setInterval(() => {this.update();}, 25);
@@ -136,7 +139,13 @@ export class UnitControlPanel extends Panel {
this.#updateRapidControls();
});
window.addEventListener("resize", (e: any) => this.#calculateMaxHeight());
const element = document.getElementById("toolbar-container");
if (element)
new ResizeObserver(() => this.#calculateTop()).observe(element);
this.#calculateMaxHeight()
this.hide();
}
@@ -154,6 +163,7 @@ export class UnitControlPanel extends Panel {
this.#followRoadsSwitch.resetExpectedValue();
this.#altitudeSlider.resetExpectedValue();
this.#speedSlider.resetExpectedValue();
this.#calculateMaxHeight();
}
addButtons() {
@@ -470,4 +480,17 @@ export class UnitControlPanel extends Panel {
button.addEventListener("click", callback);
return button;
}
#calculateTop() {
const element = document.getElementById("toolbar-container");
if (element)
this.getElement().style.top = `${element.offsetTop + element.offsetHeight + 10}px`;
}
#calculateMaxHeight() {
const element = document.getElementById("unit-control-panel-content");
this.#calculateTop();
if (element)
element.style.maxHeight = `${window.innerHeight - this.getElement().offsetTop - 10}px`;
}
}

View File

@@ -160,8 +160,8 @@ export class ServerManager {
this.PUT(data, callback);
}
spawnExplosion(intensity: number, latlng: LatLng, callback: CallableFunction = () => {}) {
var command = { "intensity": intensity, "location": latlng };
spawnExplosion(intensity: number, explosionType: string, latlng: LatLng, callback: CallableFunction = () => {}) {
var command = { "explosionType": explosionType, "intensity": intensity, "location": latlng };
var data = { "explosion": command }
this.PUT(data, callback);
}
@@ -212,8 +212,8 @@ export class ServerManager {
this.PUT(data, callback);
}
deleteUnit(ID: number, explosion: boolean, immediate: boolean, callback: CallableFunction = () => {}) {
var command = { "ID": ID, "explosion": explosion, "immediate": immediate };
deleteUnit(ID: number, explosion: boolean, explosionType: string, immediate: boolean, callback: CallableFunction = () => {}) {
var command = { "ID": ID, "explosion": explosion, "explosionType": explosionType, "immediate": immediate };
var data = { "deleteUnit": command }
this.PUT(data, callback);
}

45
client/src/unit/group.ts Normal file
View File

@@ -0,0 +1,45 @@
import { Unit } from "./unit";
export class Group {
#members: Unit[] = [];
#name: string;
constructor(name: string) {
this.#name = name;
document.addEventListener("unitDeath", (e: any) => {
if (this.#members.includes(e.detail))
this.getLeader()?.onGroupChanged(e.detail);
});
}
getName() {
return this.#name;
}
addMember(member: Unit) {
if (!this.#members.includes(member)) {
this.#members.push(member);
member.setGroup(this);
this.getLeader()?.onGroupChanged(member);
}
}
removeMember(member: Unit) {
if (this.#members.includes(member)) {
delete this.#members[this.#members.indexOf(member)];
member.setGroup(null);
this.getLeader()?.onGroupChanged(member);
}
}
getMembers() {
return this.#members;
}
getLeader() {
return this.#members.find((unit: Unit) => { return (unit.getIsLeader() && unit.getAlive())})
}
}

View File

@@ -5,12 +5,14 @@ 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, 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, GROUND_UNIT_AIR_DEFENCE_REGEX } from '../constants/constants';
import { DataExtractor } from '../server/dataextractor';
import { groundUnitDatabase } from './databases/groundunitdatabase';
import { navyUnitDatabase } from './databases/navyunitdatabase';
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';
var pathIcon = new Icon({
iconUrl: '/resources/theme/images/markers/marker-icon.png',
@@ -20,12 +22,11 @@ var pathIcon = new Icon({
/**
* Unit class which controls unit behaviour
*
* Just about everything is a unit - even missiles!
*/
export class Unit extends CustomMarker {
export abstract class Unit extends CustomMarker {
ID: number;
/* Data controlled directly by the backend. No setters are provided to avoid misalignments */
#alive: boolean = false;
#human: boolean = false;
#controlled: boolean = false;
@@ -87,8 +88,10 @@ export class Unit extends CustomMarker {
#operateAs: string = "blue";
#shotsScatter: number = 2;
#shotsIntensity: number = 2;
#health: number = 100;
#selectable: boolean;
/* Other members used to draw the unit, mostly ancillary stuff like targets, ranges and so on */
#group: Group | null = null;
#selected: boolean = false;
#hidden: boolean = false;
#highlighted: boolean = false;
@@ -96,8 +99,8 @@ export class Unit extends CustomMarker {
#pathMarkers: Marker[] = [];
#pathPolyline: Polyline;
#contactsPolylines: Polyline[] = [];
#engagementCircle: Circle;
#acquisitionCircle: Circle;
#engagementCircle: RangeCircle;
#acquisitionCircle: RangeCircle;
#miniMapMarker: CircleMarker | null = null;
#targetPositionMarker: TargetMarker;
#targetPositionPolyline: Polyline;
@@ -105,6 +108,7 @@ export class Unit extends CustomMarker {
#hotgroup: number | null = null;
#detectionMethods: number[] = [];
/* Getters for backend driven data */
getAlive() { return this.#alive };
getHuman() { return this.#human };
getControlled() { return this.#controlled };
@@ -121,8 +125,8 @@ export class Unit extends CustomMarker {
getHorizontalVelocity() { return this.#horizontalVelocity };
getVerticalVelocity() { return this.#verticalVelocity };
getHeading() { return this.#heading };
getIsActiveTanker() { return this.#isActiveTanker };
getIsActiveAWACS() { return this.#isActiveAWACS };
getIsActiveTanker() { return this.#isActiveTanker };
getOnOff() { return this.#onOff };
getFollowRoads() { return this.#followRoads };
getFuel() { return this.#fuel };
@@ -145,8 +149,9 @@ export class Unit extends CustomMarker {
getActivePath() { return this.#activePath };
getIsLeader() { return this.#isLeader };
getOperateAs() { return this.#operateAs };
getShotsScatter() { return this.#shotsScatter};
getShotsIntensity() { return this.#shotsIntensity};
getShotsScatter() { return this.#shotsScatter };
getShotsIntensity() { return this.#shotsIntensity };
getHealth() { return this.#health };
static getConstructor(type: string) {
if (type === "GroundUnit") return GroundUnit;
@@ -159,15 +164,15 @@ export class Unit extends CustomMarker {
super(new LatLng(0, 0), { riseOnHover: true, keyboard: false });
this.ID = ID;
this.#selectable = true;
this.#pathPolyline = new Polyline([], { color: '#2d3e50', weight: 3, opacity: 0.5, smoothFactor: 1 });
this.#pathPolyline.addTo(getApp().getMap());
this.#targetPositionMarker = new TargetMarker(new LatLng(0, 0));
this.#targetPositionPolyline = new Polyline([], { color: '#FF0000', weight: 3, opacity: 0.5, smoothFactor: 1 });
this.#engagementCircle = new Circle(this.getPosition(), { radius: 0, weight: 4, opacity: 1, fillOpacity: 0, dashArray: "4 8", interactive: false, bubblingMouseEvents: false });
this.#acquisitionCircle = new Circle(this.getPosition(), { radius: 0, weight: 2, opacity: 1, fillOpacity: 0, dashArray: "8 12", interactive: false, bubblingMouseEvents: false });
this.#engagementCircle = new RangeCircle(this.getPosition(), { radius: 0, weight: 4, opacity: 1, fillOpacity: 0, dashArray: "4 8", interactive: false, bubblingMouseEvents: false });
this.#acquisitionCircle = new RangeCircle(this.getPosition(), { radius: 0, weight: 2, opacity: 1, fillOpacity: 0, dashArray: "8 12", interactive: false, bubblingMouseEvents: false });
/* Leaflet events listeners */
this.on('click', (e) => this.#onClick(e));
this.on('dblclick', (e) => this.#onDoubleClick(e));
this.on('contextmenu', (e) => this.#onContextMenu(e));
@@ -181,7 +186,7 @@ export class Unit extends CustomMarker {
this.setHighlighted(false);
document.dispatchEvent(new CustomEvent("unitMouseout", { detail: this }));
});
getApp().getMap().on("zoomend", () => { this.#onZoom(); })
getApp().getMap().on("zoomend", (e: any) => { this.#onZoom(e); })
/* Deselect units if they are hidden */
document.addEventListener("toggleCoalitionVisibility", (ev: CustomEventInit) => {
@@ -192,13 +197,14 @@ export class Unit extends CustomMarker {
window.setTimeout(() => { this.setSelected(this.getSelected() && !this.getHidden()) }, 300);
});
/* Update the marker when the visibility options change */
document.addEventListener("mapVisibilityOptionsChanged", (ev: CustomEventInit) => {
this.#updateMarker();
/* Circles don't like to be updated when the map is zooming */
if (!getApp().getMap().isZooming())
if (!getApp().getMap().isZooming())
this.#drawRanges();
else
else
this.once("zoomend", () => { this.#drawRanges(); })
if (this.getSelected())
@@ -206,10 +212,25 @@ export class Unit extends CustomMarker {
});
}
getCategory() {
// Overloaded by child classes
return "";
}
/********************** Abstract methods *************************/
/** Get the unit category string
*
* @returns string The unit category
*/
abstract getCategory(): string;
/** Get the icon options
* Used to configure how the marker appears on the map
*
* @returns ObjectIconOptions
*/
abstract getIconOptions(): ObjectIconOptions;
/** Get the actions that this unit can perform
*
* @returns Object containing the available actions
*/
abstract getActions(): {[key: string]: { text: string, tooltip: string, type: string}};
/** Get the category but for display use - for the user. (i.e. has spaces in it)
*
@@ -220,9 +241,15 @@ export class Unit extends CustomMarker {
}
/********************** 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
*
* @param dataExtractor The DataExtractor object pointing to the binary buffer which contains the raw data coming from the backend
*/
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) {
datumIndex = dataExtractor.extractUInt8();
@@ -231,7 +258,7 @@ export class Unit extends CustomMarker {
case DataIndexes.alive: this.setAlive(dataExtractor.extractBool()); updateMarker = true; break;
case DataIndexes.human: this.#human = dataExtractor.extractBool(); break;
case DataIndexes.controlled: this.#controlled = dataExtractor.extractBool(); updateMarker = true; break;
case DataIndexes.coalition: this.#coalition = enumToCoalition(dataExtractor.extractUInt8()); updateMarker = true; this.#clearRanges(); break;
case DataIndexes.coalition: let newCoalition = enumToCoalition(dataExtractor.extractUInt8()); updateMarker = true; if (newCoalition != this.#coalition) this.#clearRanges(); this.#coalition = newCoalition; break; // If the coalition has changed, redraw the range circles to update the colour
case DataIndexes.country: this.#country = dataExtractor.extractUInt8(); break;
case DataIndexes.name: this.#name = dataExtractor.extractString(); break;
case DataIndexes.unitName: this.#unitName = dataExtractor.extractString(); break;
@@ -266,32 +293,37 @@ export class Unit extends CustomMarker {
case DataIndexes.ammo: this.#ammo = dataExtractor.extractAmmo(); break;
case DataIndexes.contacts: this.#contacts = dataExtractor.extractContacts(); document.dispatchEvent(new CustomEvent("contactsUpdated", { detail: this })); break;
case DataIndexes.activePath: this.#activePath = dataExtractor.extractActivePath(); break;
case DataIndexes.isLeader: this.#isLeader = dataExtractor.extractBool(); updateMarker = true; break;
case DataIndexes.isLeader: this.#isLeader = dataExtractor.extractBool(); break;
case DataIndexes.operateAs: this.#operateAs = enumToCoalition(dataExtractor.extractUInt8()); break;
case DataIndexes.shotsScatter: this.#shotsScatter = dataExtractor.extractUInt8(); break;
case DataIndexes.shotsIntensity: this.#shotsIntensity = dataExtractor.extractUInt8(); break;
case DataIndexes.health: this.#health = dataExtractor.extractUInt8(); updateMarker = true; break;
}
}
/* Dead units can't be selected */
/* Dead and hidden units can't be selected */
this.setSelected(this.getSelected() && this.#alive && !this.getHidden())
/* Update the marker if required */
if (updateMarker)
this.#updateMarker();
/* Redraw the marker if isLeader has changed. TODO I don't love this approach, observables may be more elegant */
if (oldIsLeader !== this.#isLeader) {
this.#redrawMarker();
/* Reapply selection */
if (this.getSelected()) {
this.setSelected(false);
this.setSelected(true);
}
}
/* 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)
document.dispatchEvent(new CustomEvent("unitUpdated", { detail: this }));
}
drawLines() {
/* Leaflet does not like it when you change coordinates when the map is zooming */
if (!getApp().getMap().isZooming()) {
this.#drawPath();
this.#drawContacts();
this.#drawTarget();
}
}
/** Get unit data collated into an object
*
* @returns object populated by unit information which can also be retrieved using getters
@@ -342,7 +374,8 @@ export class Unit extends CustomMarker {
isLeader: this.#isLeader,
operateAs: this.#operateAs,
shotsScatter: this.#shotsScatter,
shotsIntensity: this.#shotsIntensity
shotsIntensity: this.#shotsIntensity,
health: this.#health
}
}
@@ -362,27 +395,6 @@ export class Unit extends CustomMarker {
return getUnitDatabaseByCategory(this.getMarkerCategory());
}
/** Get the icon options
* Used to configure how the marker appears on the map
*
* @returns ObjectIconOptions
*/
getIconOptions(): ObjectIconOptions {
// Default values, overloaded by child classes if needed
return {
showState: false,
showVvi: false,
showHotgroup: false,
showUnitIcon: true,
showShortLabel: false,
showFuel: false,
showAmmo: false,
showSummary: true,
showCallsign: true,
rotateToHeading: false
}
}
/** Set the unit as alive or dead
*
* @param newAlive (boolean) true = alive, false = dead
@@ -398,16 +410,11 @@ export class Unit extends CustomMarker {
* @param selected (boolean)
*/
setSelected(selected: boolean) {
/* Only alive units can be selected. Some units are not selectable (weapons) */
if ((this.#alive || !selected) && this.getSelectable() && this.getSelected() != selected && this.belongsToCommandedCoalition()) {
/* Only alive units can be selected that belong to the commanded coalition can be selected */
if ((this.#alive || !selected) && this.belongsToCommandedCoalition() && this.getSelected() != selected) {
this.#selected = selected;
/* Circles don't like to be updated when the map is zooming */
if (!getApp().getMap().isZooming())
this.#drawRanges();
else
this.once("zoomend", () => { this.#drawRanges(); })
/* If selected, update the marker to show the selected effects, else clear all the drawings that are only shown for selected units. */
if (selected) {
this.#updateMarker();
}
@@ -417,21 +424,27 @@ export class Unit extends CustomMarker {
this.#clearTarget();
}
this.getElement()?.querySelector(`.unit`)?.toggleAttribute("data-is-selected", selected);
if (this.getCategory() === "GroundUnit" && getApp().getMap().getZoom() < 13) {
if (this.#isLeader)
/* When the group leader is selected, if grouping is active, all the other group members are also selected */
if (this.getCategory() === "GroundUnit" && getApp().getMap().getZoom() < GROUPING_ZOOM_TRANSITION) {
if (this.#isLeader) {
/* Redraw the marker in case the leader unit was replaced by a group marker, like for SAM Sites */
this.#redrawMarker();
this.getGroupMembers().forEach((unit: Unit) => unit.setSelected(selected));
else
}
else {
this.#updateMarker();
}
}
// Trigger events after all (de-)selecting has been done
/* Activate the selection effects on the marker */
this.getElement()?.querySelector(`.unit`)?.toggleAttribute("data-is-selected", selected);
/* Trigger events after all (de-)selecting has been done */
if (selected) {
document.dispatchEvent(new CustomEvent("unitSelection", { detail: this }));
} else {
document.dispatchEvent(new CustomEvent("unitDeselection", { detail: this }));
}
}
}
@@ -443,22 +456,6 @@ export class Unit extends CustomMarker {
return this.#selected;
}
/** Set whether this unit is selectable
*
* @param selectable (boolean)
*/
setSelectable(selectable: boolean) {
this.#selectable = selectable;
}
/** Get whether this unit is selectable
*
* @returns boolean
*/
getSelectable() {
return this.#selectable;
}
/** Set the number of the hotgroup to which the unit belongs
*
* @param hotgroup (number)
@@ -481,9 +478,9 @@ export class Unit extends CustomMarker {
* @param highlighted (boolean)
*/
setHighlighted(highlighted: boolean) {
if (this.getSelectable() && this.#highlighted != highlighted) {
this.getElement()?.querySelector(`[data-object|="unit"]`)?.toggleAttribute("data-is-highlighted", highlighted);
if (this.#highlighted != highlighted) {
this.#highlighted = highlighted;
this.getElement()?.querySelector(`[data-object|="unit"]`)?.toggleAttribute("data-is-highlighted", highlighted);
this.getGroupMembers().forEach((unit: Unit) => unit.setHighlighted(highlighted));
}
}
@@ -501,7 +498,19 @@ export class Unit extends CustomMarker {
* @returns Unit[]
*/
getGroupMembers() {
return Object.values(getApp().getUnitsManager().getUnits()).filter((unit: Unit) => { return unit != this && unit.getGroupName() === this.getGroupName(); });
if (this.#group !== null)
return this.#group.getMembers().filter((unit: Unit) => { return unit != this; })
return [];
}
/** Return the leader of the group
*
* @returns Unit The leader of the group
*/
getGroupLeader() {
if (this.#group !== null)
return this.#group.getLeader();
return null;
}
/** Returns whether the user is allowed to command this unit, based on coalition
@@ -520,6 +529,31 @@ export class Unit extends CustomMarker {
return this.getDatabase()?.getSpawnPointsByName(this.getName());
}
getDatabaseEntry() {
return this.getDatabase()?.getByName(this.#name);
}
getGroup() {
return this.#group;
}
setGroup(group: Group | null) {
this.#group = group;
}
drawLines() {
/* Leaflet does not like it when you change coordinates when the map is zooming */
if (!getApp().getMap().isZooming()) {
this.#drawPath();
this.#drawContacts();
this.#drawTarget();
}
}
checkZoomRedraw() {
return false;
}
/********************** Icon *************************/
createIcon(): void {
/* Set the icon */
@@ -530,6 +564,7 @@ export class Unit extends CustomMarker {
});
this.setIcon(icon);
/* Create the base element */
var el = document.createElement("div");
el.classList.add("unit");
el.setAttribute("data-object", `unit-${this.getMarkerCategory()}`);
@@ -537,8 +572,8 @@ export class Unit extends CustomMarker {
var iconOptions = this.getIconOptions();
// Generate and append elements depending on active options
// Velocity vector
/* Generate and append elements depending on active options */
/* Velocity vector */
if (iconOptions.showVvi) {
var vvi = document.createElement("div");
vvi.classList.add("unit-vvi");
@@ -546,7 +581,7 @@ export class Unit extends CustomMarker {
el.append(vvi);
}
// Hotgroup indicator
/* Hotgroup indicator */
if (iconOptions.showHotgroup) {
var hotgroup = document.createElement("div");
hotgroup.classList.add("unit-hotgroup");
@@ -556,42 +591,42 @@ export class Unit extends CustomMarker {
el.append(hotgroup);
}
// Main icon
/* Main icon */
if (iconOptions.showUnitIcon) {
var unitIcon = document.createElement("div");
unitIcon.classList.add("unit-icon");
var img = document.createElement("img");
var imgSrc;
/* 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)))
imgSrc = this.getMarkerCategory();
marker = this.getDatabaseEntry()?.markerFile ?? this.getMarkerCategory();
else
imgSrc = "aircraft";
img.src = `/resources/theme/images/units/${imgSrc}.svg`;
marker = "aircraft";
img.src = `/resources/theme/images/units/${marker}.svg`;
img.onload = () => SVGInjector(img);
unitIcon.appendChild(img);
unitIcon.toggleAttribute("data-rotate-to-heading", iconOptions.rotateToHeading);
el.append(unitIcon);
}
// State icon
/* State icon */
if (iconOptions.showState) {
var state = document.createElement("div");
state.classList.add("unit-state");
el.appendChild(state);
}
// Short label
/* Short label */
if (iconOptions.showShortLabel) {
var shortLabel = document.createElement("div");
shortLabel.classList.add("unit-short-label");
shortLabel.innerText = getUnitDatabaseByCategory(this.getMarkerCategory())?.getByName(this.#name)?.shortLabel || "";
shortLabel.innerText = this.getDatabaseEntry()?.shortLabel || "";
el.append(shortLabel);
}
// Fuel indicator
/* Fuel indicator */
if (iconOptions.showFuel) {
var fuelIndicator = document.createElement("div");
fuelIndicator.classList.add("unit-fuel");
@@ -601,7 +636,17 @@ export class Unit extends CustomMarker {
el.append(fuelIndicator);
}
// Ammo indicator
/* Health indicator */
if (iconOptions.showHealth) {
var healthIndicator = document.createElement("div");
healthIndicator.classList.add("unit-health");
var healthLevel = document.createElement("div");
healthLevel.classList.add("unit-health-level");
healthIndicator.appendChild(healthLevel);
el.append(healthIndicator);
}
/* Ammo indicator */
if (iconOptions.showAmmo) {
var ammoIndicator = document.createElement("div");
ammoIndicator.classList.add("unit-ammo");
@@ -610,7 +655,7 @@ export class Unit extends CustomMarker {
el.append(ammoIndicator);
}
// Unit summary
/* Unit summary */
if (iconOptions.showSummary) {
var summary = document.createElement("div");
summary.classList.add("unit-summary");
@@ -628,25 +673,29 @@ export class Unit extends CustomMarker {
}
this.getElement()?.appendChild(el);
/* Circles don't like to be updated when the map is zooming */
if (!getApp().getMap().isZooming())
this.#drawRanges();
else
this.once("zoomend", () => { this.#drawRanges(); })
}
/********************** Visibility *************************/
updateVisibility() {
const hiddenUnits = getApp().getMap().getHiddenTypes();
var hidden = ((this.#human && hiddenUnits.includes("human")) ||
(this.#controlled == false && hiddenUnits.includes("dcs")) ||
(hiddenUnits.includes(this.getMarkerCategory())) ||
(hiddenUnits.includes(this.#coalition)) ||
const hiddenTypes = getApp().getMap().getHiddenTypes();
var hidden = (
/* Hide the unit if it is a human and humans are hidden */
(this.#human && hiddenTypes.includes("human")) ||
/* Hide the unit if it is DCS controlled and DCS controlled units are hidden */
(this.#controlled == false && hiddenTypes.includes("dcs")) ||
/* Hide the unit if this specific category is hidden */
(hiddenTypes.includes(this.getMarkerCategory())) ||
/* Hide the unit if this coalition is hidden */
(hiddenTypes.includes(this.#coalition)) ||
/* 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))) ||
(getApp().getMap().getVisibilityOptions()[HIDE_GROUP_MEMBERS] && !this.#isLeader && this.getCategory() == "GroundUnit" && getApp().getMap().getZoom() < 13 && (this.belongsToCommandedCoalition() || (!this.belongsToCommandedCoalition() && this.#detectionMethods.length == 0)))) &&
!(this.getSelected());
/* 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()
);
/* Force dead units to be hidden */
this.setHidden(hidden || !this.#alive);
}
@@ -666,11 +715,12 @@ export class Unit extends CustomMarker {
getApp().getMap().removeLayer(this);
}
/* Draw the range circles if the unit is not hidden */
if (!this.getHidden()) {
/* Circles don't like to be updated when the map is zooming */
if (!getApp().getMap().isZooming())
if (!getApp().getMap().isZooming())
this.#drawRanges();
else
else
this.once("zoomend", () => { this.#drawRanges(); })
} else {
this.#clearRanges();
@@ -705,7 +755,7 @@ export class Unit extends CustomMarker {
if (typeof (roles) === "string")
roles = [roles];
var loadouts = this.getDatabase()?.getByName(this.#name)?.loadouts;
var loadouts = this.getDatabaseEntry()?.loadouts;
if (loadouts) {
return loadouts.some((loadout: LoadoutBlueprint) => {
return (roles as string[]).some((role: string) => { return loadout.roles.includes(role) });
@@ -719,11 +769,11 @@ export class Unit extends CustomMarker {
}
canTargetPoint() {
return this.getDatabase()?.getByName(this.#name)?.canTargetPoint === true;
return this.getDatabaseEntry()?.canTargetPoint === true;
}
canRearm() {
return this.getDatabase()?.getByName(this.#name)?.canRearm === true;
return this.getDatabaseEntry()?.canRearm === true;
}
canLandAtPoint() {
@@ -731,11 +781,11 @@ export class Unit extends CustomMarker {
}
canAAA() {
return this.getDatabase()?.getByName(this.#name)?.canAAA === true;
return this.getDatabaseEntry()?.canAAA === true;
}
indirectFire() {
return this.getDatabase()?.getByName(this.#name)?.indirectFire === true;
return this.getDatabaseEntry()?.indirectFire === true;
}
isTanker() {
@@ -845,8 +895,8 @@ export class Unit extends CustomMarker {
getApp().getServerManager().setOperateAs(this.ID, coalitionToEnum(operateAs));
}
delete(explosion: boolean, immediate: boolean) {
getApp().getServerManager().deleteUnit(this.ID, explosion, immediate);
delete(explosion: boolean, explosionType: string, immediate: boolean) {
getApp().getServerManager().deleteUnit(this.ID, explosion, explosionType, immediate);
}
refuel() {
@@ -924,11 +974,6 @@ export class Unit extends CustomMarker {
}
/***********************************************/
getActions(): { [key: string]: { text: string, tooltip: string, type: string } } {
/* To be implemented by child classes */ // TODO make Unit an abstract class
return {};
}
executeAction(e: any, action: string) {
if (action === "center-map")
getApp().getMap().centerOnUnit(this.ID);
@@ -952,20 +997,21 @@ export class Unit extends CustomMarker {
return this;
}
onGroupChanged(member: Unit) {
this.#redrawMarker();
}
/***********************************************/
#onClick(e: any) {
// Exit if we were waiting for a doubleclick
/* Exit if we were waiting for a doubleclick */
if (this.#waitingForDoubleClick) {
return;
}
// We'll wait for a doubleclick
/* We'll wait for a doubleclick */
this.#waitingForDoubleClick = true;
this.#doubleClickTimer = window.setTimeout(() => {
// Still waiting so no doubleclick; do the click action
/* Still waiting so no doubleclick; do the click action */
if (this.#waitingForDoubleClick) {
if (getApp().getMap().getState() === IDLE || getApp().getMap().getState() === MOVE_UNIT || e.originalEvent.ctrlKey) {
if (!e.originalEvent.ctrlKey)
@@ -975,17 +1021,17 @@ export class Unit extends CustomMarker {
}
}
// No longer waiting for a doubleclick
/* No longer waiting for a doubleclick */
this.#waitingForDoubleClick = false;
}, 200);
}
#onDoubleClick(e: any) {
// Let single clicks work again
/* Let single clicks work again */
this.#waitingForDoubleClick = false;
clearTimeout(this.#doubleClickTimer);
// Select all matching units in the viewport
/* Select all matching units in the viewport */
const unitsManager = getApp().getUnitsManager();
Object.values(unitsManager.getUnits()).forEach((unit: Unit) => {
if (unit.getAlive() === true && unit.getName() === this.getName() && unit.isInViewport())
@@ -1040,14 +1086,14 @@ export class Unit extends CustomMarker {
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" },
'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) => {
@@ -1138,6 +1184,10 @@ export class Unit extends CustomMarker {
element.querySelector(".unit-fuel-level")?.setAttribute("style", `width: ${this.#fuel}%`);
element.querySelector(".unit")?.toggleAttribute("data-has-low-fuel", this.#fuel < 20);
/* Set health data */
element.querySelector(".unit-health-level")?.setAttribute("style", `width: ${this.#health}%`);
element.querySelector(".unit")?.toggleAttribute("data-has-low-health", this.#health < 20);
/* Set dead/alive flag */
element.querySelector(".unit")?.toggleAttribute("data-is-dead", !this.#alive);
@@ -1148,7 +1198,7 @@ export class Unit extends CustomMarker {
else if (!this.#controlled) { // Unit is under DCS control (not Olympus)
element.querySelector(".unit")?.setAttribute("data-state", "dcs");
}
else if ((this.getCategory() == "Aircraft" || this.getCategory() == "Helicopter") && !this.#hasTask){
else if ((this.getCategory() == "Aircraft" || this.getCategory() == "Helicopter") && !this.#hasTask) {
element.querySelector(".unit")?.setAttribute("data-state", "no-task");
}
else { // Unit is under Olympus control
@@ -1221,6 +1271,14 @@ export class Unit extends CustomMarker {
}
}
#redrawMarker() {
this.removeFrom(getApp().getMap());
this.#updateMarker();
/* Activate the selection effects on the marker */
this.getElement()?.querySelector(`.unit`)?.toggleAttribute("data-is-selected", this.getSelected());
}
#drawPath() {
if (this.#activePath != undefined && getApp().getMap().getVisibilityOptions()[SHOW_UNIT_PATHS]) {
var points = [];
@@ -1316,13 +1374,13 @@ export class Unit extends CustomMarker {
/* Get the acquisition and engagement ranges of the entire group, not for each unit */
if (this.getIsLeader()) {
var engagementRange = this.getDatabase()?.getByName(this.getName())?.engagementRange?? 0;
var acquisitionRange = this.getDatabase()?.getByName(this.getName())?.acquisitionRange?? 0;
var engagementRange = this.getDatabase()?.getByName(this.getName())?.engagementRange ?? 0;
var acquisitionRange = this.getDatabase()?.getByName(this.getName())?.acquisitionRange ?? 0;
this.getGroupMembers().forEach((unit: Unit) => {
if (unit.getAlive()) {
let unitEngagementRange = unit.getDatabase()?.getByName(unit.getName())?.engagementRange?? 0;
let unitAcquisitionRange = unit.getDatabase()?.getByName(unit.getName())?.acquisitionRange?? 0;
let unitEngagementRange = unit.getDatabase()?.getByName(unit.getName())?.engagementRange ?? 0;
let unitAcquisitionRange = unit.getDatabase()?.getByName(unit.getName())?.acquisitionRange ?? 0;
if (unitEngagementRange > engagementRange)
engagementRange = unitEngagementRange;
@@ -1332,13 +1390,14 @@ export class Unit extends CustomMarker {
}
})
if (acquisitionRange !== this.#acquisitionCircle.getRadius())
this.#acquisitionCircle.setRadius(acquisitionRange);
if (acquisitionRange !== this.#acquisitionCircle.getRadius()) {
this.#acquisitionCircle.setRadius(acquisitionRange);
}
if (engagementRange !== this.#engagementCircle.getRadius())
this.#engagementCircle.setRadius(engagementRange);
this.#engagementCircle.options.fillOpacity = this.getSelected() && getApp().getMap().getVisibilityOptions()[FILL_SELECTED_RING]? 0.3: 0;
this.#engagementCircle.options.fillOpacity = this.getSelected() && getApp().getMap().getVisibilityOptions()[FILL_SELECTED_RING] ? 0.3 : 0;
/* Acquisition circles */
var shortAcquisitionRangeCheck = (acquisitionRange > nmToM(3) || !getApp().getMap().getVisibilityOptions()[HIDE_UNITS_SHORT_RANGE_RINGS]);
@@ -1358,13 +1417,14 @@ export class Unit extends CustomMarker {
break;
}
}
this.#acquisitionCircle.setLatLng(this.getPosition());
if (this.getPosition() != this.#acquisitionCircle.getLatLng())
this.#acquisitionCircle.setLatLng(this.getPosition());
}
else {
if (getApp().getMap().hasLayer(this.#acquisitionCircle))
this.#acquisitionCircle.removeFrom(getApp().getMap());
}
/* Engagement circles */
var shortEngagementRangeCheck = (engagementRange > nmToM(3) || !getApp().getMap().getVisibilityOptions()[HIDE_UNITS_SHORT_RANGE_RINGS]);
if (getApp().getMap().getVisibilityOptions()[SHOW_UNITS_ENGAGEMENT_RINGS] && shortEngagementRangeCheck && (this.belongsToCommandedCoalition() || this.getDetectionMethods().some(value => [VISUAL, OPTIC, IRST, RWR].includes(value)))) {
@@ -1382,7 +1442,8 @@ export class Unit extends CustomMarker {
break;
}
}
this.#engagementCircle.setLatLng(this.getPosition());
if (this.getPosition() != this.#engagementCircle.getLatLng())
this.#engagementCircle.setLatLng(this.getPosition());
}
else {
if (getApp().getMap().hasLayer(this.#engagementCircle))
@@ -1430,17 +1491,20 @@ export class Unit extends CustomMarker {
this.#targetPositionPolyline.removeFrom(getApp().getMap());
}
#onZoom() {
#onZoom(e: any) {
if (this.checkZoomRedraw())
this.#redrawMarker();
this.#updateMarker();
}
}
export class AirUnit extends Unit {
export abstract class AirUnit extends Unit {
getIconOptions() {
var belongsToCommandedCoalition = this.belongsToCommandedCoalition();
return {
showState: belongsToCommandedCoalition,
showVvi: (belongsToCommandedCoalition || this.getDetectionMethods().some(value => [VISUAL, OPTIC, RADAR, IRST, DLINK].includes(value))),
showHealth: false,
showHotgroup: belongsToCommandedCoalition,
showUnitIcon: (belongsToCommandedCoalition || this.getDetectionMethods().some(value => [VISUAL, OPTIC, RADAR, IRST, DLINK].includes(value))),
showShortLabel: (belongsToCommandedCoalition || this.getDetectionMethods().some(value => [VISUAL, OPTIC].includes(value))),
@@ -1514,9 +1578,10 @@ export class GroundUnit extends Unit {
return {
showState: belongsToCommandedCoalition,
showVvi: false,
showHealth: true,
showHotgroup: belongsToCommandedCoalition,
showUnitIcon: (belongsToCommandedCoalition || this.getDetectionMethods().some(value => [VISUAL, OPTIC, RADAR, IRST, DLINK].includes(value))),
showShortLabel: false,
showShortLabel: this.getDatabaseEntry()?.type === "SAM Site",
showFuel: false,
showAmmo: false,
showSummary: false,
@@ -1566,6 +1631,31 @@ export class GroundUnit extends Unit {
var blueprint = groundUnitDatabase.getByName(this.getName());
return blueprint?.type ? blueprint.type : "";
}
/* When a unit is a leader of a group, the map is zoomed out and grouping when zoomed out is enabled, check if the unit should be shown as a specific group. This is used to show a SAM battery instead of the group leader */
getDatabaseEntry() {
let unitWhenGrouped = null;
if (!this.getSelected() && this.getIsLeader() && getApp().getMap().getVisibilityOptions()[HIDE_GROUP_MEMBERS] && getApp().getMap().getZoom() < GROUPING_ZOOM_TRANSITION) {
unitWhenGrouped = this.getDatabase()?.getByName(this.getName())?.unitWhenGrouped ?? null;
let member = this.getGroupMembers().reduce((prev: Unit | null, unit: Unit, index: number) => {
if (unit.getDatabaseEntry()?.unitWhenGrouped != undefined)
return unit
return prev;
}, null);
unitWhenGrouped = (member !== null ? member?.getDatabaseEntry()?.unitWhenGrouped : unitWhenGrouped);
}
if (unitWhenGrouped)
return this.getDatabase()?.getByName(unitWhenGrouped);
else
return this.getDatabase()?.getByName(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 */
checkZoomRedraw(): boolean {
return (this.getIsLeader() && getApp().getMap().getVisibilityOptions()[HIDE_GROUP_MEMBERS] &&
(getApp().getMap().getZoom() >= GROUPING_ZOOM_TRANSITION && getApp().getMap().getPreviousZoom() < GROUPING_ZOOM_TRANSITION ||
getApp().getMap().getZoom() < GROUPING_ZOOM_TRANSITION && getApp().getMap().getPreviousZoom() >= GROUPING_ZOOM_TRANSITION))
}
}
export class NavyUnit extends Unit {
@@ -1578,6 +1668,7 @@ export class NavyUnit extends Unit {
return {
showState: belongsToCommandedCoalition,
showVvi: false,
showHealth: true,
showHotgroup: true,
showUnitIcon: (belongsToCommandedCoalition || this.getDetectionMethods().some(value => [VISUAL, OPTIC, RADAR, IRST, DLINK].includes(value))),
showShortLabel: false,

View File

@@ -15,6 +15,7 @@ import { Popup } from "../popups/popup";
import { HotgroupPanel } from "../panels/hotgrouppanel";
import { Contact, UnitData, UnitSpawnTable } from "../interfaces";
import { Dialog } from "../dialog/dialog";
import { Group } from "./group";
import { UnitDataFileExport } from "./unitdatafileexport";
import { UnitDataFileImport } from "./unitdatafileimport";
@@ -27,8 +28,9 @@ export class UnitsManager {
#deselectionEventDisabled: boolean = false;
#requestDetectionUpdate: boolean = false;
#selectionEventDisabled: boolean = false;
#slowDeleteDialog!:Dialog;
#slowDeleteDialog!: Dialog;
#units: { [ID: number]: Unit };
#groups: { [groupName: string]: Group } = {};
#unitDataExport!:UnitDataFileExport;
#unitDataImport!:UnitDataFileImport;
@@ -40,7 +42,7 @@ export class UnitsManager {
document.addEventListener('contactsUpdated', (e: CustomEvent) => { this.#requestDetectionUpdate = true });
document.addEventListener('copy', () => this.selectedUnitsCopy());
document.addEventListener('deleteSelectedUnits', () => this.selectedUnitsDelete());
document.addEventListener('explodeSelectedUnits', () => this.selectedUnitsDelete(true));
document.addEventListener('explodeSelectedUnits', (e: any) => this.selectedUnitsDelete(true, e.detail.type));
document.addEventListener('exportToFile', () => this.exportToFile());
document.addEventListener('importFromFile', () => this.importFromFile());
document.addEventListener('keyup', (event) => this.#onKeyUp(event));
@@ -49,10 +51,9 @@ export class UnitsManager {
document.addEventListener('selectedUnitsChangeSpeed', (e: any) => { this.selectedUnitsChangeSpeed(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() });
document.addEventListener("toggleMarkerProtection", (ev:CustomEventInit) => { this.#showNumberOfSelectedProtectedUnits() });
this.#slowDeleteDialog = new Dialog( "slow-delete-dialog" );
this.#slowDeleteDialog = new Dialog("slow-delete-dialog");
}
/**
@@ -133,6 +134,22 @@ export class UnitsManager {
this.#units[ID]?.setData(dataExtractor);
}
/* Update the unit groups */
for (let ID in this.#units) {
const unit = this.#units[ID];
const groupName = unit.getGroupName();
if (groupName !== "") {
/* If the group does not yet exist, create it */
if (!(groupName in this.#groups))
this.#groups[groupName] = new Group(groupName);
/* If the unit was not assigned to a group yet, assign it */
if (unit.getGroup() === null)
this.#groups[groupName].addMember(unit);
}
}
/* If we are not in Game Master mode, visibility of units by the user is determined by the detections of the units themselves. This is performed here.
This operation is computationally expensive, therefore it is only performed when #requestDetectionUpdate is true. This happens whenever a change in the detectionUpdates is detected
*/
@@ -212,9 +229,9 @@ export class UnitsManager {
*
* @param hotgroup The hotgroup number
*/
selectUnitsByHotgroup(hotgroup: number, deselectAllUnits: boolean = true ) {
selectUnitsByHotgroup(hotgroup: number, deselectAllUnits: boolean = true) {
if ( deselectAllUnits ) {
if (deselectAllUnits) {
this.deselectAllUnits();
}
@@ -226,8 +243,8 @@ export class UnitsManager {
* @param options Selection options
* @returns Array of selected units
*/
getSelectedUnits(options?: { excludeHumans?: boolean, excludeProtected?:boolean, onlyOnePerGroup?: boolean, showProtectionReminder?:boolean }) {
let selectedUnits:Unit[] = [];
getSelectedUnits(options?: { excludeHumans?: boolean, excludeProtected?: boolean, onlyOnePerGroup?: boolean, showProtectionReminder?: boolean }) {
let selectedUnits: Unit[] = [];
let numProtectedUnits = 0;
for (const [ID, unit] of Object.entries(this.#units)) {
if (unit.getSelected()) {
@@ -536,7 +553,7 @@ export class UnitsManager {
* @param operateAsBool If true, units will operate as blue
*/
selectedUnitsSetOperateAs(operateAsBool: boolean) {
var operateAs = operateAsBool? "blue": "red";
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);
@@ -588,7 +605,7 @@ export class UnitsManager {
var selectedUnits = this.getSelectedUnits({ excludeHumans: true, excludeProtected: true, onlyOnePerGroup: true, showProtectionReminder: true });
if ( selectedUnits.length === 0)
if (selectedUnits.length === 0)
return;
var count = 1;
@@ -675,7 +692,7 @@ export class UnitsManager {
});
this.#showActionMessage(selectedUnits, `unit simulating fire fight`);
}
/** Instruct units to enter into scenic AAA mode. Units will shoot in the air without aiming
*
*/
@@ -764,7 +781,7 @@ export class UnitsManager {
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 */);
getApp().getServerManager().cloneUnits(units, true, 0 /* No spawn points, we delete the original units */);
} else {
(getApp().getPopupsManager().get("infoPopup") as Popup).setText(`Groups can only be created from units of the same category`);
}
@@ -797,8 +814,8 @@ export class UnitsManager {
* @param explosion If true, the unit will be deleted using an explosion
* @returns
*/
selectedUnitsDelete(explosion: boolean = false) {
var selectedUnits = this.getSelectedUnits({excludeProtected:true}); /* Can be applied to humans too */
selectedUnitsDelete(explosion: boolean = false, explosionType: string = "") {
var selectedUnits = this.getSelectedUnits({ excludeProtected: true }); /* Can be applied to humans too */
const selectionContainsAHuman = selectedUnits.some((unit: Unit) => {
return unit.getHuman() === true;
});
@@ -807,22 +824,22 @@ export class UnitsManager {
return;
}
const doDelete = (explosion = false, immediate = false) => {
const doDelete = (explosion = false, explosionType = "", immediate = false) => {
for (let idx in selectedUnits) {
selectedUnits[idx].delete(explosion, immediate);
selectedUnits[idx].delete(explosion, explosionType, immediate);
}
this.#showActionMessage(selectedUnits, `deleted`);
}
if (selectedUnits.length >= DELETE_SLOW_THRESHOLD)
this.#showSlowDeleteDialog(selectedUnits).then((action:any) => {
this.#showSlowDeleteDialog(selectedUnits).then((action: any) => {
if (action === "delete-slow")
doDelete(explosion, false);
doDelete(explosion, explosionType, false);
else if (action === "delete-immediate")
doDelete(explosion, true);
doDelete(explosion, explosionType, true);
})
else
doDelete(explosion);
doDelete(explosion, explosionType);
}
@@ -871,7 +888,7 @@ export class UnitsManager {
this.#copiedUnits = JSON.parse(JSON.stringify(this.getSelectedUnits().map((unit: Unit) => { return unit.getData() }))); /* Can be applied to humans too */
(getApp().getPopupsManager().get("infoPopup") as Popup).setText(`${this.#copiedUnits.length} units copied`);
}
/*********************** Unit manipulation functions ************************/
/** Paste the copied units
*
@@ -892,7 +909,7 @@ export class UnitsManager {
if (unitSpawnPoints !== undefined)
spawnPoints += unitSpawnPoints;
})
if (spawnPoints > getApp().getMissionManager().getAvailableSpawnPoints()) {
(getApp().getPopupsManager().get("infoPopup") as Popup).setText("Not enough spawn points available!");
return false;
@@ -927,7 +944,7 @@ export class UnitsManager {
markers.push(getApp().getMap().addTemporaryMarker(position, unit.name, unit.coalition));
units.push({ ID: unit.ID, location: position });
});
getApp().getServerManager().cloneUnits(units, false, spawnPoints, (res: any) => {
if (res.commandHash !== undefined) {
markers.forEach((marker: TemporaryUnitMarker) => {
@@ -975,7 +992,7 @@ export class UnitsManager {
if (Math.random() < IADSDensities[type]) {
/* Get a random blueprint depending on the selected parameters and spawn the unit */
const unitBlueprint = randomUnitBlueprint(groundUnitDatabase, { type: type, eras: activeEras, ranges: activeRanges });
if (unitBlueprint)
if (unitBlueprint)
this.spawnUnits("GroundUnit", [{ unitType: unitBlueprint.name, location: latlng, liveryID: "" }], coalitionArea.getCoalition(), true);
}
}
@@ -1013,24 +1030,24 @@ export class UnitsManager {
* @param callback CallableFunction called when the command is received by the server
* @returns True if the spawn command was successfully sent
*/
spawnUnits(category: string, units: UnitSpawnTable[], coalition: string = "blue", immediate: boolean = true, airbase: string = "", country: string = "", callback: CallableFunction = () => {}) {
spawnUnits(category: string, units: UnitSpawnTable[], coalition: string = "blue", immediate: boolean = true, airbase: string = "", country: string = "", callback: CallableFunction = () => { }) {
var spawnPoints = 0;
var spawnFunction = () => {};
var spawnFunction = () => { };
var spawnsRestricted = getApp().getMissionManager().getCommandModeOptions().restrictSpawns && getApp().getMissionManager().getRemainingSetupTime() < 0 && getApp().getMissionManager().getCommandModeOptions().commandMode !== GAME_MASTER;
if (category === "Aircraft") {
if (airbase == "" && spawnsRestricted) {
(getApp().getPopupsManager().get("infoPopup") as Popup).setText("Aircrafts can be air spawned during the SETUP phase only");
return false;
}
spawnPoints = units.reduce((points: number, unit: UnitSpawnTable) => {return points + aircraftDatabase.getSpawnPointsByName(unit.unitType)}, 0);
spawnPoints = units.reduce((points: number, unit: UnitSpawnTable) => { return points + aircraftDatabase.getSpawnPointsByName(unit.unitType) }, 0);
spawnFunction = () => getApp().getServerManager().spawnAircrafts(units, coalition, airbase, country, immediate, spawnPoints, callback);
} else if (category === "Helicopter") {
if (airbase == "" && spawnsRestricted) {
(getApp().getPopupsManager().get("infoPopup") as Popup).setText("Helicopters can be air spawned during the SETUP phase only");
return false;
}
spawnPoints = units.reduce((points: number, unit: UnitSpawnTable) => {return points + helicopterDatabase.getSpawnPointsByName(unit.unitType)}, 0);
spawnPoints = units.reduce((points: number, unit: UnitSpawnTable) => { return points + helicopterDatabase.getSpawnPointsByName(unit.unitType) }, 0);
spawnFunction = () => getApp().getServerManager().spawnHelicopters(units, coalition, airbase, country, immediate, spawnPoints, callback);
} else if (category === "GroundUnit") {
@@ -1038,7 +1055,7 @@ export class UnitsManager {
(getApp().getPopupsManager().get("infoPopup") as Popup).setText("Ground units can be spawned during the SETUP phase only");
return false;
}
spawnPoints = units.reduce((points: number, unit: UnitSpawnTable) => {return points + groundUnitDatabase.getSpawnPointsByName(unit.unitType)}, 0);
spawnPoints = units.reduce((points: number, unit: UnitSpawnTable) => { return points + groundUnitDatabase.getSpawnPointsByName(unit.unitType) }, 0);
spawnFunction = () => getApp().getServerManager().spawnGroundUnits(units, coalition, country, immediate, spawnPoints, callback);
} else if (category === "NavyUnit") {
@@ -1046,7 +1063,7 @@ export class UnitsManager {
(getApp().getPopupsManager().get("infoPopup") as Popup).setText("Navy units can be spawned during the SETUP phase only");
return false;
}
spawnPoints = units.reduce((points: number, unit: UnitSpawnTable) => {return points + navyUnitDatabase.getSpawnPointsByName(unit.unitType)}, 0);
spawnPoints = units.reduce((points: number, unit: UnitSpawnTable) => { return points + navyUnitDatabase.getSpawnPointsByName(unit.unitType) }, 0);
spawnFunction = () => getApp().getServerManager().spawnNavyUnits(units, coalition, country, immediate, spawnPoints, callback);
}
@@ -1112,30 +1129,30 @@ export class UnitsManager {
else if (units.length > 1)
(getApp().getPopupsManager().get("infoPopup") as Popup).setText(`${units[0].getUnitName()} and ${units.length - 1} other units ${message}`);
}
#showSlowDeleteDialog(selectedUnits:Unit[]) {
let button:HTMLButtonElement | null = null;
const deletionTime = Math.round( selectedUnits.length * DELETE_CYCLE_TIME ).toString();
#showSlowDeleteDialog(selectedUnits: Unit[]) {
let button: HTMLButtonElement | null = null;
const deletionTime = Math.round(selectedUnits.length * DELETE_CYCLE_TIME).toString();
const dialog = this.#slowDeleteDialog;
const element = dialog.getElement();
const listener = (ev:MouseEvent) => {
const listener = (ev: MouseEvent) => {
if (ev.target instanceof HTMLButtonElement && ev.target.matches("[data-action]"))
button = ev.target;
}
element.querySelectorAll(".deletion-count").forEach( el => el.innerHTML = selectedUnits.length.toString() );
element.querySelectorAll(".deletion-time").forEach( el => el.innerHTML = deletionTime );
element.querySelectorAll(".deletion-count").forEach(el => el.innerHTML = selectedUnits.length.toString());
element.querySelectorAll(".deletion-time").forEach(el => el.innerHTML = deletionTime);
dialog.show();
return new Promise((resolve) => {
element.addEventListener("click", listener);
const interval = setInterval(() => {
if (button instanceof HTMLButtonElement ) {
if (button instanceof HTMLButtonElement) {
clearInterval(interval);
dialog.hide();
element.removeEventListener("click", listener);
resolve( button.getAttribute("data-action") );
resolve(button.getAttribute("data-action"));
}
}, 250);
});
@@ -1143,18 +1160,18 @@ 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 selectedUnits = this.getSelectedUnits();
const numSelectedUnits = selectedUnits.length;
const numProtectedUnits = selectedUnits.filter((unit: Unit) => map.unitIsProtected(unit)).length;
if (numProtectedUnits === 1 && numSelectedUnits === numProtectedUnits)
(getApp().getPopupsManager().get("infoPopup") as Popup).setText(`Notice: unit is protected`);
if (numProtectedUnits > 1)
(getApp().getPopupsManager().get("infoPopup") as Popup).setText(`Notice: selection contains ${numProtectedUnits} protected units.`);
}
#unitIsProtected(unit:Unit) {
#unitIsProtected(unit: Unit) {
return getApp().getMap().unitIsProtected(unit)
}
}

View File

@@ -100,6 +100,7 @@ export class Weapon extends CustomMarker {
return {
showState: false,
showVvi: false,
showHealth: false,
showHotgroup: false,
showUnitIcon: true,
showShortLabel: false,
@@ -276,6 +277,7 @@ export class Missile extends Weapon {
return {
showState: false,
showVvi: (!this.belongsToCommandedCoalition() && !this.getDetectionMethods().some(value => [VISUAL, OPTIC].includes(value)) && this.getDetectionMethods().some(value => [RADAR, IRST, DLINK].includes(value))),
showHealth: false,
showHotgroup: false,
showUnitIcon: (this.belongsToCommandedCoalition() || this.getDetectionMethods().some(value => [VISUAL, OPTIC, RADAR, IRST, DLINK].includes(value))),
showShortLabel: false,
@@ -308,6 +310,7 @@ export class Bomb extends Weapon {
return {
showState: false,
showVvi: (!this.belongsToCommandedCoalition() && !this.getDetectionMethods().some(value => [VISUAL, OPTIC].includes(value)) && this.getDetectionMethods().some(value => [RADAR, IRST, DLINK].includes(value))),
showHealth: false,
showHotgroup: false,
showUnitIcon: (this.belongsToCommandedCoalition() || this.getDetectionMethods().some(value => [VISUAL, OPTIC, RADAR, IRST, DLINK].includes(value))),
showShortLabel: false,