diff --git a/client/@types/olympus/index.d.ts b/client/@types/olympus/index.d.ts index 75ac7015..8a585b1d 100644 --- a/client/@types/olympus/index.d.ts +++ b/client/@types/olympus/index.d.ts @@ -1085,7 +1085,7 @@ declare module "unit/unit" { carpetBomb(latlng: LatLng): void; bombBuilding(latlng: LatLng): void; fireAtArea(latlng: LatLng): void; - simulateFireFight(latlng: LatLng, groundElevation: number | null): void; + simulateFireFight(latlng: LatLng, targetGroundElevation: number | null): void; /***********************************************/ onAdd(map: Map): this; } @@ -1967,6 +1967,19 @@ declare module "server/servermanager" { getPaused(): boolean; } } +declare module "panels/unitlistpanel" { + import { OlympusApp } from "olympusapp"; + import { Panel } from "panels/panel"; + export class UnitListPanel extends Panel { + #private; + constructor(olympusApp: OlympusApp, panelElement: string, contentElement: string); + doUpdate(): void; + getContentElement(): HTMLElement; + startUpdates(): void; + stopUpdates(): void; + toggle(): void; + } +} declare module "olympusapp" { import { Map } from "map/map"; import { MissionManager } from "mission/missionmanager"; diff --git a/client/public/stylesheets/olympus.css b/client/public/stylesheets/olympus.css index 7e51ec7a..9edb8f83 100644 --- a/client/public/stylesheets/olympus.css +++ b/client/public/stylesheets/olympus.css @@ -7,6 +7,7 @@ @import url("panels/unitcontrol.css"); @import url("panels/unitinfo.css"); @import url("panels/logpanel.css"); +@import url("panels/unitlist.css"); @import url("other/contextmenus.css"); @import url("other/popup.css"); diff --git a/client/public/stylesheets/panels/unitlist.css b/client/public/stylesheets/panels/unitlist.css new file mode 100644 index 00000000..c87f0ef6 --- /dev/null +++ b/client/public/stylesheets/panels/unitlist.css @@ -0,0 +1,55 @@ +#unit-list-panel { + bottom:20px; + display:flex; + flex-direction: column; + justify-self:center; + position: absolute; + z-index:999; +} + +#unit-list-panel h3 { + margin-bottom:4px; +} + +#unit-list-panel-content { + display:flex; + flex-flow: column nowrap; + max-height: 200px; + row-gap: 4px; +} + +#unit-list-panel .unit-list-unit { + column-gap: 4px; + display:flex; + flex-flow: row nowrap; + padding:2px 0; +} + +#unit-list-panel .unit-list-unit.headers { + column-gap: 2px; + display:flex; + flex-direction: row; +} + +#unit-list-panel .unit-list-unit.headers [data-sort-field] { + cursor:pointer; +} + +#unit-list-panel .unit-list-unit.headers > * { + background-color: var( --background-grey ); + margin-bottom: 2px; + text-align: center; +} + +#unit-list-panel .unit-list-unit > * { + overflow: hidden; + width:100px; +} + +#unit-list-panel .unit-list-unit > [data-unit-id] { + cursor:pointer; +} + +#unit-list-panel .unit-list-unit > [data-unit-id]:hover { + text-decoration: underline; +} \ No newline at end of file diff --git a/client/src/olympusapp.ts b/client/src/olympusapp.ts index a4013b57..c79f3c6d 100644 --- a/client/src/olympusapp.ts +++ b/client/src/olympusapp.ts @@ -25,6 +25,7 @@ import { helicopterDatabase } from "./unit/databases/helicopterdatabase"; import { groundUnitDatabase } from "./unit/databases/groundunitdatabase"; import { navyUnitDatabase } from "./unit/databases/navyunitdatabase"; import { ConfigurationOptions } from "./interfaces"; +import { UnitListPanel } from "./panels/unitlistpanel"; export class OlympusApp { /* Global data */ @@ -181,6 +182,7 @@ export class OlympusApp { .add("serverStatus", new ServerStatusPanel("server-status-panel")) .add("unitControl", new UnitControlPanel("unit-control-panel")) .add("unitInfo", new UnitInfoPanel("unit-info-panel")) + .add("unitList", new UnitListPanel( this, "unit-list-panel", "unit-list-panel-content" ) ) // Popups this.getPopupsManager().add("infoPopup", new Popup("info-popup")); diff --git a/client/src/panels/unitlistpanel.ts b/client/src/panels/unitlistpanel.ts new file mode 100644 index 00000000..f03721b6 --- /dev/null +++ b/client/src/panels/unitlistpanel.ts @@ -0,0 +1,208 @@ +import { OlympusApp } from "../olympusapp"; +import { ShortcutKeyboard } from "../shortcut/shortcut"; +import { Unit } from "../unit/unit"; +import { Panel } from "./panel"; + +export class UnitListPanel extends Panel { + + #contentElement: HTMLElement; + #currentSortAlgorithm: string = "unitName"; + #currentSortDirection: string = "ASC"; + #olympusApp: OlympusApp; + #units: Unit[] = []; + #updatesInterval!: ReturnType; + + constructor( olympusApp:OlympusApp, panelElement:string, contentElement:string ) { + + super( panelElement ); + + this.#olympusApp = olympusApp; + + const getElement = document.getElementById( contentElement ); + + if ( getElement instanceof HTMLElement ) { + this.#contentElement = getElement; + } else { + throw new Error( `UnitList: unable to find element "${contentElement}".` ); + } + + // Add the header click listener and sorting + [].slice.call( this.getElement().querySelectorAll( ".headers > *" ) ).forEach( ( header:HTMLElement ) => { + header.addEventListener( "click", ( ev:MouseEvent ) => { + const el = ev.target; + + if ( el instanceof HTMLElement ) { + const newSort = el.getAttribute( "data-sort-field" ) || "unitName"; + + if ( this.#currentSortAlgorithm === newSort ) { + this.#currentSortDirection = ( this.#currentSortDirection === "ASC" ) ? "DESC" : "ASC"; + } else { + this.#currentSortDirection = "ASC"; + } + + this.#currentSortAlgorithm = newSort; + + this.doUpdate(); + } + + }); + }); + + + // Dynamically listen for clicks in order to do stuff with units + this.getElement().addEventListener( "click", ( ev:MouseEvent ) => { + + const t = ev.target; + + if ( t instanceof HTMLElement ) { + + let id:number = Number( t.getAttribute( "data-unit-id" ) || 0 ); + + this.#olympusApp.getUnitsManager().selectUnit( id ); + } + + }); + + + new ShortcutKeyboard({ + "callback": () => { + this.toggle() + }, + "code": "KeyU" + }); + + this.startUpdates(); + + } + + doUpdate() { + + if ( !this.getVisible() ) { + return; + } + + this.#contentElement.innerHTML = ""; + + this.#units = Object.values( this.#olympusApp.getUnitsManager().getUnits() ); + + + if ( this.#currentSortAlgorithm === "coalition" ) { + this.#sortUnitsByCoalition(); + } + + if ( this.#currentSortAlgorithm === "name" ) { + this.#sortUnitsByName(); + } + + if ( this.#currentSortAlgorithm === "unitName" ) { + this.#sortUnitsByUnitName(); + } + + + Object.values( this.#units ).forEach( ( unit:Unit ) => { + + // Exclude dead units + if ( !unit.getAlive() ) { + return; + } + + this.#contentElement.innerHTML += ` +
+
${unit.getUnitName()}
+
${unit.getName()}
+
${unit.getCoalition()}
+
${unit.getHuman() ? "Human" : "AI"}
+
`; + + }); + + } + + getContentElement() { + return this.#contentElement; + } + + #sortStringsCompare( str1:string, str2:string ) { + + if ( str1 > str2 ) { + return ( this.#currentSortDirection === "ASC" ) ? 1 : -1; + } else if ( str1 < str2 ) { + return ( this.#currentSortDirection === "ASC" ) ? -1 : 1; + } + + return 0; + + } + + #sortUnitsByUnitName() { + + this.#units.sort( ( unit1:Unit, unit2:Unit ) => { + + const str1 = unit1.getUnitName().toLowerCase(); + const str2 = unit2.getUnitName().toLowerCase(); + + return this.#sortStringsCompare( str1, str2 ); + }); + + } + + #sortUnitsByCoalition() { + + this.#units.sort( ( unit1:Unit, unit2:Unit ) => { + + let str1 = unit1.getCoalition(); + let str2 = unit2.getCoalition(); + + let cmp = this.#sortStringsCompare( str1, str2 ); + + if ( cmp !== 0 ) { + return cmp; + } + + str1 = unit1.getUnitName().toLowerCase(); + str2 = unit2.getUnitName().toLowerCase(); + + return this.#sortStringsCompare( str1, str2 ); + + }); + + } + + #sortUnitsByName() { + + this.#units.sort( ( unit1:Unit, unit2:Unit ) => { + + const str1 = unit1.getName().toLowerCase(); + const str2 = unit2.getName().toLowerCase(); + + return this.#sortStringsCompare( str1, str2 ); + + }); + + } + + startUpdates() { + + this.doUpdate(); + + this.#updatesInterval = setInterval(() => { + this.doUpdate(); + }, 2000 ); + + } + + stopUpdates() { + clearInterval( this.#updatesInterval ); + } + + toggle() { + if ( this.getVisible() ) { + this.stopUpdates(); + } else { + this.startUpdates(); + } + + super.toggle(); + + } +} diff --git a/client/views/index.ejs b/client/views/index.ejs index 48c82765..88476a48 100644 --- a/client/views/index.ejs +++ b/client/views/index.ejs @@ -31,6 +31,7 @@ <%- include('panels/serverstatus.ejs') %> <%- include('panels/hotgroup.ejs') %> <%- include('panels/logpanel.ejs') %> + <%- include('panels/unitlist.ejs') %> <%- include('contextmenus/airbase.ejs') %> diff --git a/client/views/panels/unitlist.ejs b/client/views/panels/unitlist.ejs new file mode 100644 index 00000000..aa459c4e --- /dev/null +++ b/client/views/panels/unitlist.ejs @@ -0,0 +1,10 @@ +
+

Unit List

+
+
Name
+
Vehicle
+
Coalition
+
Human/AI
+
+
+
\ No newline at end of file