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"> + diff --git a/client/views/panels/unitcontrol.ejs b/client/views/panels/unitcontrol.ejs index 85f287ac..ec9d2b3b 100644 --- a/client/views/panels/unitcontrol.ejs +++ b/client/views/panels/unitcontrol.ejs @@ -1,113 +1,115 @@ -
+
+
+ +
+

Selected Units

-

Selected Units

+
-
+
+ + +
+
-
- - +
+ +
+

Controls

+
+
+
Speed
+
+
+
+
+
+ +
+
+
+
+
Altitude +
+
+
+
+
+
+ +
+
+
Multiple categories selected
+
+ +
+

Rules of engagement

+
+ +
+
+ +
+

Reaction to threat

+
+ +
+
+ +
+

Radar & ECM

+
+ +
+
+ +
+

Shots scatter

+
+ +
+
+ +
+

Shots intensity

+
+ +
+
+ +
+

Enable tanker

+
+
+ +
+

Airborne Early Warning

+
+
+ +
+

Operate as

+
+
+ +
+

Unit active

+
+
+ +
+

Follow roads

+
+
+ +
+ +
+ + +
- -
- -
-

Controls

-
-
-
Speed
-
-
-
-
-
- -
-
-
-
-
Altitude -
-
-
-
-
-
- -
-
-
Multiple categories selected
-
- -
-

Rules of engagement

-
- -
-
- -
-

Reaction to threat

-
- -
-
- -
-

Radar & ECM

-
- -
-
- -
-

Shots scatter

-
- -
-
- -
-

Shots intensity

-
- -
-
- -
-

Enable tanker

-
-
- -
-

Airborne Early Warning

-
-
- -
-

Operate as

-
-
- -
-

Unit active

-
-
- -
-

Follow roads

-
-
- -
- -
- - - -
-
diff --git a/client/views/panels/unitinfo.ejs b/client/views/panels/unitinfo.ejs index 25e35ed1..f1daa12b 100644 --- a/client/views/panels/unitinfo.ejs +++ b/client/views/panels/unitinfo.ejs @@ -1,4 +1,5 @@
+

diff --git a/client/views/toolbars/commandmode.ejs b/client/views/toolbars/commandmode.ejs index 573c6467..1954bc66 100644 --- a/client/views/toolbars/commandmode.ejs +++ b/client/views/toolbars/commandmode.ejs @@ -1,4 +1,5 @@
-
Options
+
Options
- + +