diff --git a/.gitignore b/.gitignore
index 7a2f4b1f..2f1bffe6 100644
--- a/.gitignore
+++ b/.gitignore
@@ -14,3 +14,4 @@ node_modules
/client/plugins/controltips/index.js
hgt
/client/public/databases/units/old
+/client/plugins/databasemanager/index.js
diff --git a/client/@types/olympus/index.d.ts b/client/@types/olympus/index.d.ts
index f1215d76..3cf448d8 100644
--- a/client/@types/olympus/index.d.ts
+++ b/client/@types/olympus/index.d.ts
@@ -659,7 +659,7 @@ declare module "unit/databases/unitdatabase" {
[key: string]: UnitBlueprint;
};
getRoles(): string[];
- getTypes(): string[];
+ getTypes(unitFilter?: CallableFunction): string[];
getEras(): string[];
getByRange(range: string): UnitBlueprint[];
getByType(type: string): UnitBlueprint[];
@@ -810,6 +810,7 @@ declare module "controls/unitspawnmenu" {
#private;
protected showRangeCircles: boolean;
protected spawnOptions: UnitSpawnOptions;
+ protected unitTypeFilter: (unit: any) => boolean;
constructor(ID: string, unitDatabase: UnitDatabase, orderByRole: boolean);
getContainer(): HTMLElement;
getVisible(): boolean;
@@ -849,6 +850,7 @@ declare module "controls/unitspawnmenu" {
}
export class GroundUnitSpawnMenu extends UnitSpawnMenu {
protected showRangeCircles: boolean;
+ protected unitTypeFilter: (unit: any) => boolean;
/**
*
* @param ID - the ID of the HTML element which will contain the context menu
@@ -856,6 +858,14 @@ declare module "controls/unitspawnmenu" {
constructor(ID: string);
deployUnits(spawnOptions: UnitSpawnOptions, unitsCount: number): void;
}
+ export class AirDefenceUnitSpawnMenu extends GroundUnitSpawnMenu {
+ protected unitTypeFilter: (unit: any) => boolean;
+ /**
+ *
+ * @param ID - the ID of the HTML element which will contain the context menu
+ */
+ constructor(ID: string);
+ }
export class NavyUnitSpawnMenu extends UnitSpawnMenu {
/**
*
@@ -894,7 +904,7 @@ declare module "contextmenus/mapcontextmenu" {
* @param y Y screen coordinate of the top left corner of the context menu
* @param latlng Leaflet latlng object of the mouse click
*/
- show(x: number, y: number, latlng: LatLng): void;
+ show(x: number, y: number, latlng: LatLng): false | undefined;
/** If the user rightclicked on a CoalitionArea, it will be given the ability to edit it.
*
* @param coalitionArea The CoalitionArea the user can edit
@@ -1510,6 +1520,7 @@ declare module "map/map" {
[key: string]: boolean;
};
unitIsProtected(unit: Unit): boolean;
+ getMapMarkerControls(): MapMarkerControl[];
}
}
declare module "mission/bullseye" {
diff --git a/client/bin/www b/client/bin/www
index 887394f4..bb2e7ac7 100644
--- a/client/bin/www
+++ b/client/bin/www
@@ -1,5 +1,9 @@
#!/usr/bin/env node
+var fs = require('fs');
+let rawdata = fs.readFileSync('../olympus.json');
+let config = JSON.parse(rawdata);
+
/**
* Module dependencies.
*/
@@ -12,8 +16,14 @@ var http = require('http');
* Get port from environment and store in Express.
*/
-var port = normalizePort(process.env.PORT || '3000');
+var configPort = null;
+if (config["client"] != undefined && config["client"]["port"] != undefined) {
+ configPort = config["client"]["port"];
+}
+
+var port = normalizePort(configPort || '3000');
app.set('port', port);
+console.log("Express server listening on port: " + port)
/**
* Create HTTP server.
diff --git a/client/plugins/databasemanager/index.js b/client/plugins/databasemanager/index.js
index 432c8142..d434ec3d 100644
--- a/client/plugins/databasemanager/index.js
+++ b/client/plugins/databasemanager/index.js
@@ -508,7 +508,7 @@ class GroundUnitEditor extends uniteditor_1.UnitEditor {
* @param blueprint The blueprint to edit
*/
setBlueprint(blueprint) {
- var _a, _b, _c, _d, _e, _f, _g, _h, _j, _k, _l, _m, _o, _p, _q, _r, _s;
+ var _a, _b, _c, _d, _e, _f, _g, _h, _j, _k, _l, _m, _o, _p, _q, _r, _s, _t, _u;
__classPrivateFieldSet(this, _GroundUnitEditor_blueprint, blueprint, "f");
if (__classPrivateFieldGet(this, _GroundUnitEditor_blueprint, "f") !== null) {
this.contentDiv2.replaceChildren();
@@ -526,18 +526,20 @@ class GroundUnitEditor extends uniteditor_1.UnitEditor {
(0, utils_1.addStringInput)(this.contentDiv2, "Acquisition range [m]", (_c = String(blueprint.acquisitionRange)) !== null && _c !== void 0 ? _c : "", "number", (value) => { blueprint.acquisitionRange = parseFloat(value); });
(0, utils_1.addStringInput)(this.contentDiv2, "Engagement range [m]", (_d = String(blueprint.engagementRange)) !== null && _d !== void 0 ? _d : "", "number", (value) => { blueprint.engagementRange = parseFloat(value); });
(0, utils_1.addStringInput)(this.contentDiv2, "Targeting range [m]", (_e = String(blueprint.targetingRange)) !== null && _e !== void 0 ? _e : "", "number", (value) => { blueprint.targetingRange = parseFloat(value); });
- (0, utils_1.addStringInput)(this.contentDiv2, "Barrel height [m]", (_f = String(blueprint.barrelHeight)) !== null && _f !== void 0 ? _f : "", "number", (value) => { blueprint.barrelHeight = parseFloat(value); });
- (0, utils_1.addStringInput)(this.contentDiv2, "Muzzle velocity [m/s]", (_g = String(blueprint.muzzleVelocity)) !== null && _g !== void 0 ? _g : "", "number", (value) => { blueprint.muzzleVelocity = parseFloat(value); });
- (0, utils_1.addStringInput)(this.contentDiv2, "Aim time [s]", (_h = String(blueprint.aimTime)) !== null && _h !== void 0 ? _h : "", "number", (value) => { blueprint.aimTime = parseFloat(value); });
- (0, utils_1.addStringInput)(this.contentDiv2, "Burst quantity", (_j = String(blueprint.shotsToFire)) !== null && _j !== void 0 ? _j : "", "number", (value) => { blueprint.shotsToFire = Math.round(parseFloat(value)); });
- (0, utils_1.addStringInput)(this.contentDiv2, "Burst base interval [s]", (_k = String(blueprint.shotsBaseInterval)) !== null && _k !== void 0 ? _k : "", "number", (value) => { blueprint.shotsBaseInterval = Math.round(parseFloat(value)); });
- (0, utils_1.addStringInput)(this.contentDiv2, "Base scatter [°]", (_l = String(blueprint.shotsBaseScatter)) !== null && _l !== void 0 ? _l : "", "number", (value) => { blueprint.shotsBaseScatter = Math.round(parseFloat(value)); });
- (0, utils_1.addCheckboxInput)(this.contentDiv2, "Can target point", (_m = blueprint.canTargetPoint) !== null && _m !== void 0 ? _m : false, (value) => { blueprint.canTargetPoint = value; });
- (0, utils_1.addCheckboxInput)(this.contentDiv2, "Can rearm", (_o = blueprint.canRearm) !== null && _o !== void 0 ? _o : false, (value) => { blueprint.canRearm = value; });
- (0, utils_1.addCheckboxInput)(this.contentDiv2, "Can operate as AAA", (_p = blueprint.canAAA) !== null && _p !== void 0 ? _p : false, (value) => { blueprint.canAAA = value; });
- (0, utils_1.addCheckboxInput)(this.contentDiv2, "Indirect fire (e.g. mortar)", (_q = blueprint.indirectFire) !== null && _q !== void 0 ? _q : false, (value) => { blueprint.indirectFire = value; });
- (0, utils_1.addStringInput)(this.contentDiv2, "Description", (_r = blueprint.description) !== null && _r !== void 0 ? _r : "", "text", (value) => { blueprint.description = value; });
- (0, utils_1.addStringInput)(this.contentDiv2, "Abilities", (_s = blueprint.abilities) !== null && _s !== void 0 ? _s : "", "text", (value) => { blueprint.abilities = value; });
+ (0, utils_1.addStringInput)(this.contentDiv2, "Aim method range [m]", (_f = String(blueprint.aimMethodRange)) !== null && _f !== void 0 ? _f : "", "number", (value) => { blueprint.aimMethodRange = parseFloat(value); });
+ (0, utils_1.addStringInput)(this.contentDiv2, "Barrel height [m]", (_g = String(blueprint.barrelHeight)) !== null && _g !== void 0 ? _g : "", "number", (value) => { blueprint.barrelHeight = parseFloat(value); });
+ (0, utils_1.addStringInput)(this.contentDiv2, "Muzzle velocity [m/s]", (_h = String(blueprint.muzzleVelocity)) !== null && _h !== void 0 ? _h : "", "number", (value) => { blueprint.muzzleVelocity = parseFloat(value); });
+ (0, utils_1.addStringInput)(this.contentDiv2, "Aim time [s]", (_j = String(blueprint.aimTime)) !== null && _j !== void 0 ? _j : "", "number", (value) => { blueprint.aimTime = parseFloat(value); });
+ (0, utils_1.addStringInput)(this.contentDiv2, "Shots to fire", (_k = String(blueprint.shotsToFire)) !== null && _k !== void 0 ? _k : "", "number", (value) => { blueprint.shotsToFire = Math.round(parseFloat(value)); });
+ (0, utils_1.addStringInput)(this.contentDiv2, "Shots base interval [s]", (_l = String(blueprint.shotsBaseInterval)) !== null && _l !== void 0 ? _l : "", "number", (value) => { blueprint.shotsBaseInterval = Math.round(parseFloat(value)); });
+ (0, utils_1.addStringInput)(this.contentDiv2, "Shots base scatter [°]", (_m = String(blueprint.shotsBaseScatter)) !== null && _m !== void 0 ? _m : "", "number", (value) => { blueprint.shotsBaseScatter = Math.round(parseFloat(value)); });
+ (0, utils_1.addStringInput)(this.contentDiv2, "Alertness time constant [s]", (_o = String(blueprint.alertnessTimeConstant)) !== null && _o !== void 0 ? _o : "", "number", (value) => { blueprint.alertnessTimeConstant = Math.round(parseFloat(value)); });
+ (0, utils_1.addCheckboxInput)(this.contentDiv2, "Can target point", (_p = blueprint.canTargetPoint) !== null && _p !== void 0 ? _p : false, (value) => { blueprint.canTargetPoint = value; });
+ (0, utils_1.addCheckboxInput)(this.contentDiv2, "Can rearm", (_q = blueprint.canRearm) !== null && _q !== void 0 ? _q : false, (value) => { blueprint.canRearm = value; });
+ (0, utils_1.addCheckboxInput)(this.contentDiv2, "Can operate as AAA", (_r = blueprint.canAAA) !== null && _r !== void 0 ? _r : false, (value) => { blueprint.canAAA = value; });
+ (0, utils_1.addCheckboxInput)(this.contentDiv2, "Indirect fire (e.g. mortar)", (_s = blueprint.indirectFire) !== null && _s !== void 0 ? _s : false, (value) => { blueprint.indirectFire = value; });
+ (0, utils_1.addStringInput)(this.contentDiv2, "Description", (_t = blueprint.description) !== null && _t !== void 0 ? _t : "", "text", (value) => { blueprint.description = value; });
+ (0, utils_1.addStringInput)(this.contentDiv2, "Abilities", (_u = blueprint.abilities) !== null && _u !== void 0 ? _u : "", "text", (value) => { blueprint.abilities = value; });
}
}
/** Add a new empty blueprint
@@ -751,9 +753,9 @@ class UnitEditor {
this.database = JSON.parse(JSON.stringify({ blueprints: database.getBlueprints(true) }));
}
/** Show the editor
- *
+ * @param filter String filter
*/
- show() {
+ show(filter = "") {
this.visible = true;
this.contentDiv1.replaceChildren();
this.contentDiv2.replaceChildren();
@@ -763,17 +765,16 @@ class UnitEditor {
var title = document.createElement("label");
title.innerText = "Units list";
this.contentDiv1.appendChild(title);
- (0, utils_1.addBlueprintsScroll)(this.contentDiv1, this.database, (key) => {
- if (this.database != null)
- this.setBlueprint(this.database.blueprints[key]);
- });
- (0, utils_1.addNewElementInput)(this.contentDiv1, (ev, input) => {
- if (input.value != "")
- this.addBlueprint((input).value);
- });
+ var filterInput = document.createElement("input");
+ filterInput.value = filter;
+ this.contentDiv1.appendChild(filterInput);
+ filterInput.onchange = (e) => {
+ this.show(e.target.value);
+ };
+ this.addBlueprints(filter);
}
}
- /** Hid the editor
+ /** Hide the editor
*
*/
hide() {
@@ -789,6 +790,22 @@ class UnitEditor {
getDatabase() {
return this.database;
}
+ /**
+ *
+ * @param filter String filter
+ */
+ addBlueprints(filter = "") {
+ if (this.database) {
+ (0, utils_1.addBlueprintsScroll)(this.contentDiv1, this.database, filter, (key) => {
+ if (this.database != null)
+ this.setBlueprint(this.database.blueprints[key]);
+ });
+ (0, utils_1.addNewElementInput)(this.contentDiv1, (ev, input) => {
+ if (input.value != "")
+ this.addBlueprint((input).value);
+ });
+ }
+ }
}
exports.UnitEditor = UnitEditor;
@@ -958,36 +975,56 @@ exports.addNewElementInput = addNewElementInput;
*
* @param div The HTMLElement that will contain the list
* @param database The database that will be used to fill the list of blueprints
+ * @param filter A string filter that will be executed to filter the blueprints to add
* @param callback Callback called when the user clicks on one of the elements
*/
-function addBlueprintsScroll(div, database, callback) {
+function addBlueprintsScroll(div, database, filter, callback) {
var scrollDiv = document.createElement("div");
scrollDiv.classList.add("dm-scroll-container");
if (database !== null) {
var blueprints = database.blueprints;
for (let key of Object.keys(blueprints).sort((a, b) => a.localeCompare(b, undefined, { sensitivity: 'base' }))) {
- var rowDiv = document.createElement("div");
- scrollDiv.appendChild(rowDiv);
- var text = document.createElement("label");
- text.textContent = key;
- text.onclick = () => callback(key);
- rowDiv.appendChild(text);
- let checkbox = document.createElement("input");
- checkbox.type = "checkbox";
- checkbox.checked = blueprints[key].enabled;
- checkbox.onclick = () => {
- console.log(checkbox.checked);
- blueprints[key].enabled = checkbox.checked;
- };
- rowDiv.appendChild(checkbox);
- /* This button allows to remove an element from the list. It requires a refresh. */
- var button = document.createElement("button");
- button.innerText = "X";
- button.onclick = () => {
- delete blueprints[key];
- div.dispatchEvent(new Event("refresh"));
- };
- rowDiv.appendChild(button);
+ var addKey = true;
+ if (filter !== "") {
+ try {
+ var blueprint = blueprints[key];
+ addKey = eval(filter);
+ }
+ catch (_a) {
+ console.error("An error has occurred evaluating the blueprint filter");
+ }
+ }
+ if (addKey) {
+ var rowDiv = document.createElement("div");
+ scrollDiv.appendChild(rowDiv);
+ let text = document.createElement("label");
+ text.textContent = key;
+ text.onclick = () => {
+ callback(key);
+ const collection = document.getElementsByClassName("blueprint-selected");
+ for (let i = 0; i < collection.length; i++) {
+ collection[i].classList.remove("blueprint-selected");
+ }
+ text.classList.add("blueprint-selected");
+ };
+ rowDiv.appendChild(text);
+ let checkbox = document.createElement("input");
+ checkbox.type = "checkbox";
+ checkbox.checked = blueprints[key].enabled;
+ checkbox.onclick = () => {
+ console.log(checkbox.checked);
+ blueprints[key].enabled = checkbox.checked;
+ };
+ rowDiv.appendChild(checkbox);
+ /* This button allows to remove an element from the list. It requires a refresh. */
+ var button = document.createElement("button");
+ button.innerText = "X";
+ button.onclick = () => {
+ delete blueprints[key];
+ div.dispatchEvent(new Event("refresh"));
+ };
+ rowDiv.appendChild(button);
+ }
}
}
div.appendChild(scrollDiv);
diff --git a/client/plugins/databasemanager/src/utils.ts b/client/plugins/databasemanager/src/utils.ts
index 004a8b84..18639612 100644
--- a/client/plugins/databasemanager/src/utils.ts
+++ b/client/plugins/databasemanager/src/utils.ts
@@ -287,7 +287,11 @@ export function arrayToString(array: string[]) {
return "[" + array.join( ", " ) + "]";
}
-
+/** Converts an a single string like [val1, val2, val3] into an array
+ *
+ * @param input The input string
+ * @returns The array
+ */
export function stringToArray(input: string) {
return input.match( /(\w)+/g ) ?? [];
}
\ No newline at end of file
diff --git a/client/public/stylesheets/layout/layout.css b/client/public/stylesheets/layout/layout.css
index 8dc6e68d..ba4adc98 100644
--- a/client/public/stylesheets/layout/layout.css
+++ b/client/public/stylesheets/layout/layout.css
@@ -1,7 +1,3 @@
-:root {
- --right-panel-width:190px;
-}
-
/* Page style */
#map-container {
height: 100%;
@@ -17,54 +13,10 @@
top: 10px;
z-index: 99999;
column-gap: 10px;
+ row-gap: 10px;
margin-right: 320px;
height: fit-content;
-}
-
-@media (max-width: 1820px) {
- #toolbar-container {
- flex-direction: column;
- align-items: start;
- row-gap: 10px;
- }
-}
-
-#primary-toolbar {
- align-items: center;
- display: flex;
- height: fit-content;
- min-width: 650px;
-}
-
-@media (max-width: 1820px) {
- #primary-toolbar {
- row-gap: 10px;
- flex-wrap: wrap;
- }
-}
-
-#command-mode-toolbar {
- align-items: center;
- display: flex;
-}
-
-#app-icon>.ol-select-options {
- width: fit-content;
-}
-
-#toolbar-summary {
- background-image: url("/images/icon-round.png");
- background-position: 20px 22px;
- background-repeat: no-repeat;
- background-size: 45px 45px;
- display: flex;
- flex-direction: column;
- padding: 20px;
- text-indent: 60px;
-}
-
-#toolbar-summary {
- white-space: nowrap;
+ flex-wrap: wrap;
}
#connection-status-panel {
@@ -72,7 +24,7 @@
font-size: 12px;
position: absolute;
right: 10px;
- width: var( --right-panel-width );
+ width: 190px;
z-index: 9999;
}
@@ -84,35 +36,21 @@
position: absolute;
right: 10px;
row-gap: 10px;
- width: var( --right-panel-width );
+ width: 190px;
z-index: 9999;
}
#unit-control-panel {
height: fit-content;
+ width: fit-content;
left: 10px;
position: absolute;
- top: 80px;
- width: 320px;
z-index: 9999;
}
-@media (max-width: 1820px) {
- #unit-control-panel {
- top: 150px;
- }
-}
-
-@media (max-width: 1350px) {
- #unit-control-panel {
- top: 190px;
- }
-}
-
#unit-info-panel {
bottom: 20px;
font-size: 12px;
- left: 10px;
position: absolute;
width: fit-content;
z-index: 9999;
@@ -120,12 +58,8 @@
display: flex;
flex-direction: row;
justify-content: space-evenly;
-}
-
-@media (max-width: 1525px) {
- #unit-info-panel {
- flex-direction: column;
- }
+ right: 210px;
+ height: 180px;
}
#info-popup {
diff --git a/client/public/stylesheets/markers/units.css b/client/public/stylesheets/markers/units.css
index 7f9ae8e9..09f02d65 100644
--- a/client/public/stylesheets/markers/units.css
+++ b/client/public/stylesheets/markers/units.css
@@ -97,6 +97,18 @@
position: absolute;
}
+/*** Health indicator ***/
+[data-object|="unit"] .unit-health {
+ background: white;
+ border: var(--unit-health-border-width) solid var(--secondary-dark-steel);
+ border-radius: var(--border-radius-sm);
+ display: none;
+ height: var(--unit-health-height);
+ position: absolute;
+ translate: var(--unit-health-x) var(--unit-health-y);
+ width: var(--unit-health-width);
+}
+
/*** Fuel indicator ***/
[data-object|="unit"] .unit-fuel {
background: white;
@@ -109,7 +121,8 @@
width: var(--unit-fuel-width);
}
-[data-object|="unit"] .unit-fuel-level {
+[data-object|="unit"] .unit-fuel-level,
+[data-object|="unit"] .unit-health-level {
background-color: var(--secondary-light-grey);
height: 100%;
width: 100%;
@@ -178,6 +191,7 @@
/*** Common ***/
[data-object|="unit"]:hover .unit-ammo,
+[data-object|="unit"]:hover .unit-health ,
[data-object|="unit"]:hover .unit-fuel {
display: flex;
}
@@ -188,13 +202,14 @@
}
}
-[data-object|="unit"][data-has-low-fuel] .unit-fuel {
+[data-object|="unit"][data-has-low-fuel] .unit-fuel, [data-object|="unit"][data-has-low-health] .unit-health {
animation: pulse 1.5s linear infinite;
}
[data-object|="unit"][data-is-in-hotgroup] .unit-hotgroup,
[data-object|="unit"][data-is-selected] .unit-ammo,
[data-object|="unit"][data-is-selected] .unit-fuel,
+[data-object|="unit"][data-is-selected] .unit-health,
[data-object|="unit"][data-is-selected] .unit-selected-spotlight {
display: flex;
}
@@ -211,6 +226,7 @@
}
[data-object|="unit"][data-coalition="blue"] .unit-fuel-level,
+[data-object|="unit"][data-coalition="blue"] .unit-health-level,
[data-object|="unit"][data-coalition="blue"][data-has-fox-1] .unit-ammo>div:nth-child(1),
[data-object|="unit"][data-coalition="blue"][data-has-fox-2] .unit-ammo>div:nth-child(2),
[data-object|="unit"][data-coalition="blue"][data-has-fox-3] .unit-ammo>div:nth-child(3),
@@ -227,6 +243,7 @@
}
[data-object|="unit"][data-coalition="red"] .unit-fuel-level,
+[data-object|="unit"][data-coalition="red"] .unit-health-level,
[data-object|="unit"][data-coalition="red"][data-has-fox-1] .unit-ammo>div:nth-child(1),
[data-object|="unit"][data-coalition="red"][data-has-fox-2] .unit-ammo>div:nth-child(2),
[data-object|="unit"][data-coalition="red"][data-has-fox-3] .unit-ammo>div:nth-child(3),
@@ -307,6 +324,19 @@
background-image: url("/resources/theme/images/states/awacs.svg");
}
+[data-object|="unit"] .unit-health::before {
+ background-image: url("/resources/theme/images/icons/health.svg");
+ background-repeat: no-repeat;
+ background-size: contain;
+ content: " ";
+ height: 6px;
+ left: 0;
+ position: absolute;
+ top: 0;
+ translate: -10px -2px;
+ width: 6px;
+}
+
/*** Dead unit ***/
[data-object|="unit"][data-is-dead] .unit-selected-spotlight,
@@ -316,6 +346,7 @@
[data-object|="unit"][data-is-dead] .unit-hotgroup-id,
[data-object|="unit"][data-is-dead] .unit-state,
[data-object|="unit"][data-is-dead] .unit-fuel,
+[data-object|="unit"][data-is-dead] .unit-health,
[data-object|="unit"][data-is-dead] .unit-ammo,
[data-object|="unit"][data-is-dead]:hover .unit-fuel,
[data-object|="unit"][data-is-dead]:hover .unit-ammo {
diff --git a/client/public/stylesheets/olympus.css b/client/public/stylesheets/olympus.css
index 9edb8f83..aa3c4808 100644
--- a/client/public/stylesheets/olympus.css
+++ b/client/public/stylesheets/olympus.css
@@ -11,6 +11,8 @@
@import url("other/contextmenus.css");
@import url("other/popup.css");
+@import url("other/toolbar.css");
+
@import url("markers/airbase.css");
@import url("markers/bullseye.css");
diff --git a/client/public/stylesheets/other/toolbar.css b/client/public/stylesheets/other/toolbar.css
new file mode 100644
index 00000000..c90df5f8
--- /dev/null
+++ b/client/public/stylesheets/other/toolbar.css
@@ -0,0 +1,74 @@
+
+#primary-toolbar {
+ align-items: center;
+ display: flex;
+ height: fit-content;
+}
+
+#command-mode-toolbar {
+ align-items: center;
+ display: flex;
+}
+
+#app-icon>.ol-select-options {
+ width: fit-content;
+}
+
+#toolbar-summary {
+ background-image: url("/images/icon-round.png");
+ background-position: 20px 22px;
+ background-repeat: no-repeat;
+ background-size: 45px 45px;
+ display: flex;
+ flex-direction: column;
+ padding: 20px;
+ text-indent: 60px;
+}
+
+#toolbar-summary {
+ white-space: nowrap;
+}
+
+#toolbar-container>*:nth-child(2)>svg {
+ display: none;
+ width: 0px;
+ height: 0px;
+}
+
+#toolbar-container>*:nth-child(3)>svg {
+ display: none;
+}
+
+@media (max-width: 1145px) {
+ #toolbar-container {
+ flex-direction: column;
+ align-items: start;
+ }
+
+ #toolbar-container>*:nth-child(1):not(:hover) {
+ width: fit-content;
+ height: fit-content;
+ }
+
+ #toolbar-container>*:nth-child(1):not(:hover)>*:not(:first-child) {
+ display: none;
+ }
+
+ #toolbar-container>*:not(:first-child):not(:hover) {
+ height: 52px;
+ align-items: center;
+ justify-content: center;
+ aspect-ratio: 1/1;
+ }
+
+ #toolbar-container>*:not(:first-child):not(:hover)>svg {
+ display: block;
+ width: 24px;
+ height: 24px;
+ filter: invert();
+ }
+
+ #toolbar-container>*:not(:first-child):not(:hover)>*:not(:first-child) {
+ display: none;
+ }
+}
diff --git a/client/public/stylesheets/panels/unitcontrol.css b/client/public/stylesheets/panels/unitcontrol.css
index 99509503..69ecb6b5 100644
--- a/client/public/stylesheets/panels/unitcontrol.css
+++ b/client/public/stylesheets/panels/unitcontrol.css
@@ -3,9 +3,48 @@ body.feature-forceShowUnitControlPanel #unit-control-panel {
}
#unit-control-panel {
+ display: flex;
+ flex-direction: row;
+ column-gap: 10px;
+ row-gap: 10px;
+}
+
+#unit-control-panel>div:nth-child(2) {
display: flex;
flex-direction: column;
row-gap: 10px;
+ width: 300px;
+}
+
+#unit-control-panel>*:nth-child(1) {
+ display: none;
+ padding: 14px;
+}
+
+@media (max-width: 1145px) {
+ #unit-control-panel>*:nth-child(1) {
+ display: flex;
+ }
+
+ #unit-control-panel>*:nth-child(1) svg {
+ display: flex;
+ width: 24px;
+ height: 24px;
+ filter: invert(100%);
+ }
+
+ #unit-control-panel:hover>*:nth-child(1) {
+ display: none;
+ }
+
+ #unit-control-panel:not(:hover) {
+ width: fit-content;
+ }
+
+ #unit-control-panel:not(:hover)>*:nth-child(2),
+ #unit-control-panel:not(:hover)>*:nth-child(3) {
+ display: none;
+ }
}
#unit-control-panel h3 {
diff --git a/client/public/stylesheets/panels/unitinfo.css b/client/public/stylesheets/panels/unitinfo.css
index b970dfa4..3535b5b5 100644
--- a/client/public/stylesheets/panels/unitinfo.css
+++ b/client/public/stylesheets/panels/unitinfo.css
@@ -1,47 +1,47 @@
#unit-info-panel>* {
position: relative;
- min-height: 100px;
bottom: 0px;
}
-@media (min-width: 1525px) {
- #unit-info-panel>.panel-section {
- border-right: 1px solid #555;
- padding: 0 30px;
- }
-
- #unit-info-panel>.panel-section:first-child {
- padding-left: 0px;
- }
-
- #unit-info-panel>.panel-section:last-child {
- padding-right: 0px;
- }
-
- #unit-info-panel>.panel-section:last-of-type {
- border-right-width: 0;
- }
+#unit-info-panel>*:nth-child(1) {
+ display: flex;
+ width: 24px;
+ height: 24px;
+ margin: 6px;
+ filter: invert(100%);
}
-@media (max-width: 1525px) {
- #unit-info-panel>.panel-section {
- border-bottom: 1px solid #555;
- padding: 30px 0px;
- }
-
- #unit-info-panel>.panel-section:first-child {
- padding-top: 0px;
- }
-
- #unit-info-panel>.panel-section:last-child {
- padding-bottom: 0px;
- }
-
- #unit-info-panel>.panel-section:last-of-type {
- border-bottom-width: 0;
- }
+#unit-info-panel:hover>*:nth-child(1) {
+ display: none;
}
+#unit-info-panel:not(:hover) {
+ width: fit-content;
+ height: fit-content;
+ padding: 10px;
+ margin: 0px;
+}
+
+#unit-info-panel:not(:hover)>*:not(:first-child) {
+ display: none;
+}
+
+#unit-info-panel>.panel-section {
+ border-right: 1px solid #555;
+ padding: 0 30px;
+}
+
+#unit-info-panel>.panel-section:first-of-type {
+ padding-left: 0px;
+}
+
+#unit-info-panel>.panel-section:last-of-type{
+ padding-right: 0px;
+}
+
+#unit-info-panel>.panel-section:last-of-type {
+ border-right-width: 0;
+}
#general {
display: flex;
@@ -49,6 +49,7 @@
justify-content: space-between;
row-gap: 4px;
position: relative;
+ width: 300px;
}
#unit-label {
@@ -63,6 +64,10 @@
#unit-name {
margin-bottom: 4px;
padding: 0px 0;
+ width: 100%;
+ text-overflow: ellipsis;
+ text-wrap: nowrap;
+ overflow: hidden;
}
#current-task {
@@ -87,6 +92,7 @@
display: flex;
flex-direction: column;
justify-content: space-between;
+ width: 300px;
}
#loadout-silhouette {
@@ -101,9 +107,9 @@
column-gap: 8px;
display: flex;
flex-flow: column nowrap;
- max-height: 108px;
- padding-right:40px;
+ height: 100px;
row-gap: 6px;
+ padding-right: 10px;
}
#loadout-items>* {
diff --git a/client/public/stylesheets/style/style.css b/client/public/stylesheets/style/style.css
index eb16edde..1ff9293e 100644
--- a/client/public/stylesheets/style/style.css
+++ b/client/public/stylesheets/style/style.css
@@ -714,11 +714,8 @@ nav.ol-panel> :last-child {
display: flex;
flex-direction: column;
row-gap: 5px;
- position: absolute;
height: fit-content;
width: fit-content;
- left: calc(100% + 10px);
- top: 0px;
}
#rapid-controls button {
diff --git a/client/public/themes/olympus/images/icons/circle-info-solid.svg b/client/public/themes/olympus/images/icons/circle-info-solid.svg
new file mode 100644
index 00000000..652acbee
--- /dev/null
+++ b/client/public/themes/olympus/images/icons/circle-info-solid.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/client/public/themes/olympus/images/icons/gamepad-solid.svg b/client/public/themes/olympus/images/icons/gamepad-solid.svg
new file mode 100644
index 00000000..2fc91782
--- /dev/null
+++ b/client/public/themes/olympus/images/icons/gamepad-solid.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/client/public/themes/olympus/images/icons/health.svg b/client/public/themes/olympus/images/icons/health.svg
new file mode 100644
index 00000000..850b69a3
--- /dev/null
+++ b/client/public/themes/olympus/images/icons/health.svg
@@ -0,0 +1,4 @@
+
diff --git a/client/public/themes/olympus/images/icons/person-military-pointing-solid.svg b/client/public/themes/olympus/images/icons/person-military-pointing-solid.svg
new file mode 100644
index 00000000..919b3a6f
--- /dev/null
+++ b/client/public/themes/olympus/images/icons/person-military-pointing-solid.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/client/public/themes/olympus/theme.css b/client/public/themes/olympus/theme.css
index 41953812..03a6ef40 100644
--- a/client/public/themes/olympus/theme.css
+++ b/client/public/themes/olympus/theme.css
@@ -67,6 +67,12 @@
--unit-height: 50px;
--unit-width: 50px;
+ --unit-health-border-width: 2px;
+ --unit-health-height: 6px;
+ --unit-health-width: 36px;
+ --unit-health-x: 0px;
+ --unit-health-y: 26px;
+
/*** Air units ***/
--unit-ammo-gap: calc(2px + var(--unit-stroke-width));
--unit-ammo-border-radius: 50%;
diff --git a/client/src/constants/constants.ts b/client/src/constants/constants.ts
index 8eb1663f..fe1a07fc 100644
--- a/client/src/constants/constants.ts
+++ b/client/src/constants/constants.ts
@@ -262,6 +262,7 @@ export enum DataIndexes {
operateAs,
shotsScatter,
shotsIntensity,
+ health,
endOfData = 255
};
diff --git a/client/src/contextmenus/mapcontextmenu.ts b/client/src/contextmenus/mapcontextmenu.ts
index 89de4c8b..866e2543 100644
--- a/client/src/contextmenus/mapcontextmenu.ts
+++ b/client/src/contextmenus/mapcontextmenu.ts
@@ -4,7 +4,7 @@ import { ContextMenu } from "./contextmenu";
import { Switch } from "../controls/switch";
import { GAME_MASTER } from "../constants/constants";
import { CoalitionArea } from "../map/coalitionarea/coalitionarea";
-import { AircraftSpawnMenu, GroundUnitSpawnMenu, HelicopterSpawnMenu, NavyUnitSpawnMenu } from "../controls/unitspawnmenu";
+import { AirDefenceUnitSpawnMenu, AircraftSpawnMenu, GroundUnitSpawnMenu, HelicopterSpawnMenu, NavyUnitSpawnMenu } from "../controls/unitspawnmenu";
import { Airbase } from "../mission/airbase";
import { SmokeMarker } from "../map/markers/smokemarker";
@@ -15,6 +15,7 @@ export class MapContextMenu extends ContextMenu {
#coalitionSwitch: Switch;
#aircraftSpawnMenu: AircraftSpawnMenu;
#helicopterSpawnMenu: HelicopterSpawnMenu;
+ #airDefenceUnitSpawnMenu: AirDefenceUnitSpawnMenu;
#groundUnitSpawnMenu: GroundUnitSpawnMenu;
#navyUnitSpawnMenu: NavyUnitSpawnMenu;
#coalitionArea: CoalitionArea | null = null;
@@ -35,6 +36,7 @@ export class MapContextMenu extends ContextMenu {
/* Create the spawn menus for the different unit types */
this.#aircraftSpawnMenu = new AircraftSpawnMenu("aircraft-spawn-menu");
this.#helicopterSpawnMenu = new HelicopterSpawnMenu("helicopter-spawn-menu");
+ this.#airDefenceUnitSpawnMenu = new AirDefenceUnitSpawnMenu("air-defence-spawn-menu");
this.#groundUnitSpawnMenu = new GroundUnitSpawnMenu("groundunit-spawn-menu");
this.#navyUnitSpawnMenu = new NavyUnitSpawnMenu("navyunit-spawn-menu");
@@ -73,21 +75,25 @@ export class MapContextMenu extends ContextMenu {
this.#aircraftSpawnMenu.getContainer().addEventListener("resize", () => this.clip());
this.#helicopterSpawnMenu.getContainer().addEventListener("resize", () => this.clip());
+ this.#airDefenceUnitSpawnMenu.getContainer().addEventListener("resize", () => this.clip());
this.#groundUnitSpawnMenu.getContainer().addEventListener("resize", () => this.clip());
this.#navyUnitSpawnMenu.getContainer().addEventListener("resize", () => this.clip());
this.#aircraftSpawnMenu.getContainer().addEventListener("hide", () => this.hide());
this.#helicopterSpawnMenu.getContainer().addEventListener("hide", () => this.hide());
+ this.#airDefenceUnitSpawnMenu.getContainer().addEventListener("hide", () => this.hide());
this.#groundUnitSpawnMenu.getContainer().addEventListener("hide", () => this.hide());
this.#navyUnitSpawnMenu.getContainer().addEventListener("hide", () => this.hide());
this.getContainer()?.addEventListener("show", () => this.#aircraftSpawnMenu.showCirclesPreviews());
this.getContainer()?.addEventListener("show", () => this.#helicopterSpawnMenu.showCirclesPreviews());
+ this.getContainer()?.addEventListener("show", () => this.#airDefenceUnitSpawnMenu.showCirclesPreviews());
this.getContainer()?.addEventListener("show", () => this.#groundUnitSpawnMenu.showCirclesPreviews());
this.getContainer()?.addEventListener("show", () => this.#navyUnitSpawnMenu.showCirclesPreviews());
this.getContainer()?.addEventListener("hide", () => this.#aircraftSpawnMenu.clearCirclesPreviews());
this.getContainer()?.addEventListener("hide", () => this.#helicopterSpawnMenu.clearCirclesPreviews());
+ this.getContainer()?.addEventListener("hide", () => this.#airDefenceUnitSpawnMenu.clearCirclesPreviews());
this.getContainer()?.addEventListener("hide", () => this.#groundUnitSpawnMenu.clearCirclesPreviews());
this.getContainer()?.addEventListener("hide", () => this.#navyUnitSpawnMenu.clearCirclesPreviews());
}
@@ -106,11 +112,13 @@ export class MapContextMenu extends ContextMenu {
this.#aircraftSpawnMenu.setLatLng(latlng);
this.#helicopterSpawnMenu.setLatLng(latlng);
+ this.#airDefenceUnitSpawnMenu.setLatLng(latlng);
this.#groundUnitSpawnMenu.setLatLng(latlng);
this.#navyUnitSpawnMenu.setLatLng(latlng);
this.#aircraftSpawnMenu.setCountries();
this.#helicopterSpawnMenu.setCountries();
+ this.#airDefenceUnitSpawnMenu.setCountries();
this.#groundUnitSpawnMenu.setCountries();
this.#navyUnitSpawnMenu.setCountries();
@@ -146,7 +154,7 @@ export class MapContextMenu extends ContextMenu {
#showSubMenu(type: string) {
if (type === "more")
this.getContainer()?.querySelector("#more-options-button-bar")?.classList.toggle("hide");
- else if (["aircraft", "helicopter", "groundunit"].includes(type))
+ else if (["aircraft", "helicopter", "air-defence", "groundunit"].includes(type))
this.getContainer()?.querySelector("#more-options-button-bar")?.classList.toggle("hide", true);
this.getContainer()?.querySelector("#aircraft-spawn-menu")?.classList.toggle("hide", type !== "aircraft");
@@ -155,6 +163,8 @@ export class MapContextMenu extends ContextMenu {
this.getContainer()?.querySelector("#helicopter-spawn-button")?.classList.toggle("is-open", type === "helicopter");
this.getContainer()?.querySelector("#groundunit-spawn-menu")?.classList.toggle("hide", type !== "groundunit");
this.getContainer()?.querySelector("#groundunit-spawn-button")?.classList.toggle("is-open", type === "groundunit");
+ this.getContainer()?.querySelector("#air-defence-spawn-menu")?.classList.toggle("hide", type !== "air-defence");
+ this.getContainer()?.querySelector("#air-defence-spawn-button")?.classList.toggle("is-open", type === "air-defence");
this.getContainer()?.querySelector("#navyunit-spawn-menu")?.classList.toggle("hide", type !== "navyunit");
this.getContainer()?.querySelector("#navyunit-spawn-button")?.classList.toggle("is-open", type === "navyunit");
this.getContainer()?.querySelector("#smoke-spawn-menu")?.classList.toggle("hide", type !== "smoke");
@@ -170,6 +180,9 @@ export class MapContextMenu extends ContextMenu {
this.#helicopterSpawnMenu.reset();
this.#helicopterSpawnMenu.setCountries();
this.#helicopterSpawnMenu.clearCirclesPreviews();
+ this.#airDefenceUnitSpawnMenu.reset();
+ this.#airDefenceUnitSpawnMenu.setCountries();
+ this.#airDefenceUnitSpawnMenu.clearCirclesPreviews();
this.#groundUnitSpawnMenu.reset();
this.#groundUnitSpawnMenu.setCountries();
this.#groundUnitSpawnMenu.clearCirclesPreviews();
@@ -203,11 +216,13 @@ export class MapContextMenu extends ContextMenu {
this.#aircraftSpawnMenu.reset();
this.#helicopterSpawnMenu.reset();
+ this.#airDefenceUnitSpawnMenu.reset();
this.#groundUnitSpawnMenu.reset();
this.#navyUnitSpawnMenu.reset();
this.#aircraftSpawnMenu.clearCirclesPreviews();
this.#helicopterSpawnMenu.clearCirclesPreviews();
+ this.#airDefenceUnitSpawnMenu.clearCirclesPreviews();
this.#groundUnitSpawnMenu.clearCirclesPreviews();
this.#navyUnitSpawnMenu.clearCirclesPreviews();
@@ -224,6 +239,7 @@ export class MapContextMenu extends ContextMenu {
this.getContainer()?.querySelectorAll('[data-coalition]').forEach((element: any) => { element.setAttribute("data-coalition", getApp().getActiveCoalition()) });
this.#aircraftSpawnMenu.setCountries();
this.#helicopterSpawnMenu.setCountries();
+ this.#airDefenceUnitSpawnMenu.setCountries();
this.#groundUnitSpawnMenu.setCountries();
this.#navyUnitSpawnMenu.setCountries();
}
@@ -237,6 +253,7 @@ export class MapContextMenu extends ContextMenu {
this.getContainer()?.querySelectorAll('[data-coalition]').forEach((element: any) => { element.setAttribute("data-coalition", getApp().getActiveCoalition()) });
this.#aircraftSpawnMenu.setCountries();
this.#helicopterSpawnMenu.setCountries();
+ this.#airDefenceUnitSpawnMenu.setCountries();
this.#groundUnitSpawnMenu.setCountries();
this.#navyUnitSpawnMenu.setCountries();
}
diff --git a/client/src/controls/unitspawnmenu.ts b/client/src/controls/unitspawnmenu.ts
index 0ed023d1..b0ee3fd4 100644
--- a/client/src/controls/unitspawnmenu.ts
+++ b/client/src/controls/unitspawnmenu.ts
@@ -31,6 +31,7 @@ export class UnitSpawnMenu {
#unitDatabase: UnitDatabase;
#countryCodes: any;
#orderByRole: boolean;
+ protected unitTypeFilter = (unit:any) => { return true; };
/* Controls */
#unitRoleTypeDropdown: Dropdown;
@@ -258,7 +259,7 @@ export class UnitSpawnMenu {
if (this.#orderByRole)
this.#unitRoleTypeDropdown.setOptions(this.#unitDatabase.getRoles());
else
- this.#unitRoleTypeDropdown.setOptions(this.#unitDatabase.getTypes());
+ this.#unitRoleTypeDropdown.setOptions(this.#unitDatabase.getTypes(this.unitTypeFilter));
this.#unitLoadoutListEl.replaceChildren();
this.#unitLoadoutDropdown.reset();
@@ -585,6 +586,7 @@ export class HelicopterSpawnMenu extends UnitSpawnMenu {
export class GroundUnitSpawnMenu extends UnitSpawnMenu {
protected showRangeCircles: boolean = true;
+ protected unitTypeFilter = (unit:any) => {return !(/\bAAA|SAM\b/.test(unit.type) || /\bmanpad|stinger\b/i.test(unit.type))};
/**
*
@@ -623,6 +625,20 @@ export class GroundUnitSpawnMenu extends UnitSpawnMenu {
}
}
+export class AirDefenceUnitSpawnMenu extends GroundUnitSpawnMenu {
+
+ protected unitTypeFilter = (unit:any) => {return /\bAAA|SAM\b/.test(unit.type) || /\bmanpad|stinger\b/i.test(unit.type)};
+
+ /**
+ *
+ * @param ID - the ID of the HTML element which will contain the context menu
+ */
+ constructor(ID: string){
+ super(ID);
+ this.setMaxUnitCount(4);
+ }
+}
+
export class NavyUnitSpawnMenu extends UnitSpawnMenu {
/**
*
diff --git a/client/src/interfaces.ts b/client/src/interfaces.ts
index c13419f5..9cd04796 100644
--- a/client/src/interfaces.ts
+++ b/client/src/interfaces.ts
@@ -86,6 +86,7 @@ export interface UnitSpawnTable {
export interface ObjectIconOptions {
showState: boolean,
showVvi: boolean,
+ showHealth: boolean,
showHotgroup: boolean,
showUnitIcon: boolean,
showShortLabel: boolean,
@@ -181,6 +182,7 @@ export interface UnitData {
operateAs: string;
shotsScatter: number;
shotsIntensity: number;
+ health: number;
}
export interface LoadoutItemBlueprint {
diff --git a/client/src/olympusapp.ts b/client/src/olympusapp.ts
index 22f2ebf8..66acf5e6 100644
--- a/client/src/olympusapp.ts
+++ b/client/src/olympusapp.ts
@@ -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*/
diff --git a/client/src/panels/unitcontrolpanel.ts b/client/src/panels/unitcontrolpanel.ts
index 78081126..1ec769f6 100644
--- a/client/src/panels/unitcontrolpanel.ts
+++ b/client/src/panels/unitcontrolpanel.ts
@@ -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;
@@ -136,7 +137,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();
}
@@ -470,4 +477,16 @@ 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");
+ if (element)
+ element.style.maxHeight = `${window.innerHeight - this.getElement().offsetTop - 10}px`;
+ }
}
\ No newline at end of file
diff --git a/client/src/unit/databases/unitdatabase.ts b/client/src/unit/databases/unitdatabase.ts
index ba7c41cc..94c8acca 100644
--- a/client/src/unit/databases/unitdatabase.ts
+++ b/client/src/unit/databases/unitdatabase.ts
@@ -94,10 +94,12 @@ export class UnitDatabase {
}
/* Returns a list of all possible types in a database */
- getTypes() {
+ getTypes(unitFilter?:CallableFunction) {
var filteredBlueprints = this.getBlueprints();
var types: string[] = [];
for (let unit in filteredBlueprints) {
+ if ( typeof unitFilter === "function" && !unitFilter(filteredBlueprints[unit]))
+ continue;
var type = filteredBlueprints[unit].type;
if (type && type !== "" && !types.includes(type))
types.push(type);
diff --git a/client/src/unit/unit.ts b/client/src/unit/unit.ts
index 22f6b214..5f529327 100644
--- a/client/src/unit/unit.ts
+++ b/client/src/unit/unit.ts
@@ -18,6 +18,11 @@ var pathIcon = new Icon({
iconAnchor: [13, 41]
});
+/**
+ * Unit class which controls unit behaviour
+ *
+ * Just about everything is a unit - even missiles!
+ */
export class Unit extends CustomMarker {
ID: number;
@@ -82,6 +87,7 @@ export class Unit extends CustomMarker {
#operateAs: string = "blue";
#shotsScatter: number = 2;
#shotsIntensity: number = 2;
+ #health: number = 100;
#selectable: boolean;
#selected: boolean = false;
@@ -116,8 +122,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 };
@@ -140,8 +146,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;
@@ -191,9 +198,9 @@ export class Unit extends CustomMarker {
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())
@@ -257,6 +264,7 @@ export class Unit extends CustomMarker {
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;
}
}
@@ -279,6 +287,10 @@ export class Unit extends CustomMarker {
}
}
+ /** Get unit data collated into an object
+ *
+ * @returns object populated by unit information which can also be retrieved using getters
+ */
getData(): UnitData {
return {
category: this.getCategory(),
@@ -324,23 +336,38 @@ export class Unit extends CustomMarker {
isLeader: this.#isLeader,
operateAs: this.#operateAs,
shotsScatter: this.#shotsScatter,
- shotsIntensity: this.#shotsIntensity
+ shotsIntensity: this.#shotsIntensity,
+ health: this.#health
}
}
+ /**
+ *
+ * @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
+ */
getDatabase(): UnitDatabase | null {
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,
+ showHealth: true,
showHotgroup: false,
showUnitIcon: true,
showShortLabel: false,
@@ -352,21 +379,29 @@ export class Unit extends CustomMarker {
}
}
+ /** Set the unit as alive or dead
+ *
+ * @param newAlive (boolean) true = alive, false = dead
+ */
setAlive(newAlive: boolean) {
if (newAlive != this.#alive)
document.dispatchEvent(new CustomEvent("unitDeath", { detail: this }));
this.#alive = newAlive;
}
+ /** Set the unit as user-selected
+ *
+ * @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()) {
this.#selected = selected;
/* 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 (selected) {
@@ -396,27 +431,51 @@ export class Unit extends CustomMarker {
}
}
+ /** Is this unit selected?
+ *
+ * @returns boolean
+ */
getSelected() {
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)
+ */
setHotgroup(hotgroup: number | null) {
this.#hotgroup = hotgroup;
this.#updateMarker();
}
+ /** Get the unit's hotgroup number
+ *
+ * @returns number
+ */
getHotgroup() {
return this.#hotgroup;
}
+ /** Set the unit as highlighted
+ *
+ * @param highlighted (boolean)
+ */
setHighlighted(highlighted: boolean) {
if (this.getSelectable() && this.#highlighted != highlighted) {
this.getElement()?.querySelector(`[data-object|="unit"]`)?.toggleAttribute("data-is-highlighted", highlighted);
@@ -425,18 +484,28 @@ export class Unit extends CustomMarker {
}
}
+ /** Get whether the unit is highlighted or not
+ *
+ * @returns boolean
+ */
getHighlighted() {
return this.#highlighted;
}
+ /** Get the other members of the group which this unit is in
+ *
+ * @returns Unit[]
+ */
getGroupMembers() {
return Object.values(getApp().getUnitsManager().getUnits()).filter((unit: Unit) => { return unit != this && unit.getGroupName() === this.getGroupName(); });
}
+ /** Returns whether the user is allowed to command this unit, based on coalition
+ *
+ * @returns boolean
+ */
belongsToCommandedCoalition() {
- if (getApp().getMissionManager().getCommandModeOptions().commandMode !== GAME_MASTER && getApp().getMissionManager().getCommandedCoalition() !== this.#coalition)
- return false;
- return true;
+ return (getApp().getMissionManager().getCommandModeOptions().commandMode !== GAME_MASTER && getApp().getMissionManager().getCommandedCoalition() !== this.#coalition) ? false : true;
}
getType() {
@@ -528,6 +597,16 @@ export class Unit extends CustomMarker {
el.append(fuelIndicator);
}
+ // 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");
@@ -557,9 +636,9 @@ 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())
+ if (!getApp().getMap().isZooming())
this.#drawRanges();
- else
+ else
this.once("zoomend", () => { this.#drawRanges(); })
}
@@ -595,9 +674,9 @@ export class Unit extends CustomMarker {
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();
@@ -851,7 +930,7 @@ export class Unit extends CustomMarker {
}
/***********************************************/
- getActions(): { [key: string]: { text: string, tooltip: string, type: string } } {
+ getActions(): { [key: string]: { text: string, tooltip: string, type: string } } {
/* To be implemented by child classes */ // TODO make Unit an abstract class
return {};
}
@@ -967,14 +1046,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) => {
@@ -1065,6 +1144,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);
@@ -1075,7 +1158,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
@@ -1243,13 +1326,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;
@@ -1260,12 +1343,12 @@ export class Unit extends CustomMarker {
})
if (acquisitionRange !== this.#acquisitionCircle.getRadius())
- this.#acquisitionCircle.setRadius(acquisitionRange);
+ 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]);
@@ -1291,7 +1374,7 @@ export class Unit extends CustomMarker {
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)))) {
@@ -1368,6 +1451,7 @@ export class AirUnit extends Unit {
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))),
@@ -1441,6 +1525,7 @@ 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,
@@ -1505,6 +1590,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,
diff --git a/client/src/weapon/weapon.ts b/client/src/weapon/weapon.ts
index f673c07e..57496337 100644
--- a/client/src/weapon/weapon.ts
+++ b/client/src/weapon/weapon.ts
@@ -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,
diff --git a/client/views/contextmenus/map.ejs b/client/views/contextmenus/map.ejs
index 983a5c31..3e37ba57 100644
--- a/client/views/contextmenus/map.ejs
+++ b/client/views/contextmenus/map.ejs
@@ -6,6 +6,8 @@
data-on-click-params='{ "type": "aircraft" }' class="ol-contexmenu-button">
+