diff --git a/client/@types/olympus/index.d.ts b/client/@types/olympus/index.d.ts index 75ac7015..8418600c 100644 --- a/client/@types/olympus/index.d.ts +++ b/client/@types/olympus/index.d.ts @@ -588,11 +588,11 @@ declare module "interfaces" { name?: string; shiftKey?: boolean; } - export interface KeyboardShortcutOptions extends ShortcutOptions { + export interface ShortcutKeyboardOptions extends ShortcutOptions { code: string; event?: "keydown" | "keyup"; } - export interface MouseShortcutOptions extends ShortcutOptions { + export interface ShortcutMouseOptions extends ShortcutOptions { button: number; event: "mousedown" | "mouseup"; } @@ -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; } @@ -1493,26 +1493,28 @@ declare module "plugin/pluginmanager" { } } declare module "shortcut/shortcut" { - import { KeyboardShortcutOptions, MouseShortcutOptions, ShortcutOptions } from "interfaces"; + import { ShortcutKeyboardOptions, ShortcutMouseOptions, ShortcutOptions } from "interfaces"; export abstract class Shortcut { #private; constructor(config: ShortcutOptions); getConfig(): ShortcutOptions; } export class ShortcutKeyboard extends Shortcut { - constructor(config: KeyboardShortcutOptions); + constructor(config: ShortcutKeyboardOptions); } export class ShortcutMouse extends Shortcut { - constructor(config: MouseShortcutOptions); + constructor(config: ShortcutMouseOptions); } } declare module "shortcut/shortcutmanager" { + import { ShortcutKeyboardOptions, ShortcutMouseOptions } from "interfaces"; import { Manager } from "other/manager"; - import { Shortcut } from "shortcut/shortcut"; export class ShortcutManager extends Manager { #private; constructor(); - add(name: string, shortcut: Shortcut): this; + add(name: string, shortcut: any): this; + addKeyboardShortcut(name: string, shortcutKeyboardOptions: ShortcutKeyboardOptions): this; + addMouseShortcut(name: string, shortcutMouseOptions: ShortcutMouseOptions): this; getKeysBeingHeld(): string[]; keyComboMatches(combo: string[]): boolean; onKeyDown(callback: CallableFunction): void; @@ -1967,6 +1969,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/plugins/controltips/src/controltipsplugin.ts b/client/plugins/controltips/src/controltipsplugin.ts index 5977d9e3..aec00a80 100644 --- a/client/plugins/controltips/src/controltipsplugin.ts +++ b/client/plugins/controltips/src/controltipsplugin.ts @@ -1,3 +1,5 @@ +import { OlympusPlugin } from "interfaces"; + const SHOW_CONTROL_TIPS = "Show control tips" export class ControlTipsPlugin implements OlympusPlugin { @@ -41,7 +43,7 @@ export class ControlTipsPlugin implements OlympusPlugin { this.#updateTips(); }); - document.addEventListener("unitDeselection", (ev: CustomEvent) => { + document.addEventListener("unitDeselection", (ev: CustomEventInit ) => { this.#updateTips(); }); @@ -55,7 +57,7 @@ export class ControlTipsPlugin implements OlympusPlugin { this.#updateTips(); }); - document.addEventListener("unitSelection", (ev: CustomEvent) => { + document.addEventListener("unitSelection", (ev: CustomEventInit ) => { this.#updateTips(); }); 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/connectionstatus.css b/client/public/stylesheets/panels/connectionstatus.css index 2b7dcec4..b785c023 100644 --- a/client/public/stylesheets/panels/connectionstatus.css +++ b/client/public/stylesheets/panels/connectionstatus.css @@ -16,4 +16,25 @@ #connection-status-panel[data-is-connected] dd::after { background: var(--accent-green); +} + +#connection-status-panel[data-is-paused] dt::before { + content: "Server paused"; +} + +#connection-status-panel[data-is-paused] dd { + animation: pulse 1s infinite; +} + +@keyframes pulse { + 0% { + opacity: 0; + } + 100% { + opacity: 1; + } +} + +#connection-status-panel[data-is-paused] dd::after { + background: var(--accent-amber); } \ No newline at end of file diff --git a/client/public/stylesheets/panels/unitcontrol.css b/client/public/stylesheets/panels/unitcontrol.css index 250a9e2f..819e2a68 100644 --- a/client/public/stylesheets/panels/unitcontrol.css +++ b/client/public/stylesheets/panels/unitcontrol.css @@ -248,6 +248,7 @@ body.feature-forceShowUnitControlPanel #unit-control-panel { #advanced-settings-dialog:not([data-show-tanker]) #tanker-checkbox, #advanced-settings-dialog:not([data-show-AWACS]) #AWACS-checkbox, #advanced-settings-dialog:not([data-show-TACAN]) #TACAN-options, -#advanced-settings-dialog:not([data-show-radio]) #radio-options { +#advanced-settings-dialog:not([data-show-radio]) #radio-options, +#advanced-settings-dialog:not([data-show-air-unit-checkboxes]) .air-unit-checkbox { display: none; } \ No newline at end of file diff --git a/client/public/stylesheets/panels/unitlist.css b/client/public/stylesheets/panels/unitlist.css new file mode 100644 index 00000000..7f60bdda --- /dev/null +++ b/client/public/stylesheets/panels/unitlist.css @@ -0,0 +1,92 @@ +#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-unit { + border-radius: var( --border-radius-sm ); + column-gap: 2px; + display:flex; + flex-flow: row nowrap; + justify-content: space-between; +} + + +.unit-list-unit:nth-of-type(even) { + background:#ffffff10; + overflow:visible; +} + +.unit-list-unit.headers { + margin-bottom:3px; + margin-right:10px; + overflow: hidden; +} + +.unit-list-unit.headers [data-sort-field] { + cursor:pointer; +} + +.unit-list-unit.headers > * { + background-color: var( --background-grey ); + text-align: center; +} + +.unit-list-unit > * { + font-size:13px; + overflow: hidden; + padding:2px; + width:80px; +} + +.unit-list-unit :first-child { + overflow-x: hidden; + text-overflow: ellipsis; + white-space: nowrap; + width:150px; +} + +.unit-list-unit :first-child:hover { + overflow:visible; +} + +.unit-list-unit :first-child:hover span { + position:relative; + z-index:9999; +} + +.unit-list-unit :first-child:hover span:hover { + background-color: white; + color: var( --background-steel ); +} + +.unit-list-unit :nth-child(2) { + width:120px; +} + +.unit-list-unit > [data-unit-id] { + cursor:pointer; +} + +#unit-list-panel-content > * { + cursor:pointer; +} + +#unit-list-panel-content > .unit-list-unit:hover { + background-color: var( --background-grey ); +} \ No newline at end of file diff --git a/client/public/stylesheets/style/style.css b/client/public/stylesheets/style/style.css index b262fe0c..638c90d1 100644 --- a/client/public/stylesheets/style/style.css +++ b/client/public/stylesheets/style/style.css @@ -1054,7 +1054,9 @@ nav.ol-panel> :last-child { } .ol-coalitionarea-handle-icon, -.ol-coalitionarea-middle-handle-icon { +.ol-coalitionarea-middle-handle-icon, +.ol-destination-preview-icon, +.ol-destination-preview-handle-icon { pointer-events: none; z-index: 9999; border-radius: 999px; @@ -1072,6 +1074,13 @@ nav.ol-panel> :last-child { height: 16px; } +.ol-destination-preview-handle-icon { + background-color: #247be2; + border: 2px solid white; + width: 18px; + height: 18px; +} + dl.ol-data-grid { align-items: center; display: flex; diff --git a/client/public/themes/olympus/theme.css b/client/public/themes/olympus/theme.css index 520d6927..df1c6244 100644 --- a/client/public/themes/olympus/theme.css +++ b/client/public/themes/olympus/theme.css @@ -20,6 +20,7 @@ --unit-background-red: #FF5858; /*** UI Colours **/ + --accent-amber: #ffd828; --accent-green: #8bff63; --accent-light-blue: #5ca7ff; --transparent-accent-light-blue: rgba(92, 167, 255, .33); diff --git a/client/src/interfaces.ts b/client/src/interfaces.ts index 0661f486..484593a3 100644 --- a/client/src/interfaces.ts +++ b/client/src/interfaces.ts @@ -259,12 +259,12 @@ export interface ShortcutOptions { shiftKey?: boolean; } -export interface KeyboardShortcutOptions extends ShortcutOptions { +export interface ShortcutKeyboardOptions extends ShortcutOptions { code: string; event?: "keydown" | "keyup"; } -export interface MouseShortcutOptions extends ShortcutOptions { +export interface ShortcutMouseOptions extends ShortcutOptions { button: number; event: "mousedown" | "mouseup"; } diff --git a/client/src/map/map.ts b/client/src/map/map.ts index 79484e46..30958ac6 100644 --- a/client/src/map/map.ts +++ b/client/src/map/map.ts @@ -21,6 +21,7 @@ import { AirbaseSpawnContextMenu } from "../contextmenus/airbasespawnmenu"; import { Popup } from "../popups/popup"; import { GestureHandling } from "leaflet-gesture-handling"; import { TouchBoxSelect } from "./touchboxselect"; +import { DestinationPreviewHandle } from "./markers/destinationpreviewHandle"; var hasTouchScreen = false; if ("maxTouchPoints" in navigator) @@ -67,6 +68,8 @@ export class Map extends L.Map { #targetCursor: TargetMarker = new TargetMarker(new L.LatLng(0, 0), { interactive: false }); #destinationPreviewCursors: DestinationPreviewMarker[] = []; #drawingCursor: DrawingCursor = new DrawingCursor(); + #destinationPreviewHandle: DestinationPreviewHandle = new DestinationPreviewHandle(new L.LatLng(0, 0)); + #destinationPreviewHandleLine: L.Polyline = new L.Polyline([], { color: "#000000", weight: 3, opacity: 0.5, smoothFactor: 1, dashArray: "4, 8" }); #longPressHandled: boolean = false; #longPressTimer: number = 0; @@ -133,6 +136,7 @@ export class Map extends L.Map { this.on("click", (e: any) => this.#onClick(e)); this.on("dblclick", (e: any) => this.#onDoubleClick(e)); this.on("zoomstart", (e: any) => this.#onZoomStart(e)); + this.on("zoom", (e: any) => this.#onZoom(e)); this.on("zoomend", (e: any) => this.#onZoomEnd(e)); this.on("drag", (e: any) => this.centerOnUnit(null)); this.on("contextmenu", (e: any) => this.#onContextMenu(e)); @@ -141,6 +145,7 @@ export class Map extends L.Map { this.on('mousedown', (e: any) => this.#onMouseDown(e)); this.on('mouseup', (e: any) => this.#onMouseUp(e)); this.on('mousemove', (e: any) => this.#onMouseMove(e)); + this.on('drag', (e: any) => this.#onMouseMove(e)); this.on('keydown', (e: any) => this.#onKeyDown(e)); this.on('keyup', (e: any) => this.#onKeyUp(e)); @@ -523,36 +528,38 @@ export class Map extends L.Map { } this.hideMapContextMenu(); - if (this.#state === IDLE) { - if (this.#state == IDLE) { - this.showMapContextMenu(e.originalEvent.x, e.originalEvent.y, e.latlng); - var clickedCoalitionArea = null; + if (!this.#shiftKey) { + if (this.#state === IDLE) { + if (this.#state == IDLE) { + this.showMapContextMenu(e.originalEvent.x, e.originalEvent.y, e.latlng); + var clickedCoalitionArea = null; - /* Coalition areas are ordered in the #coalitionAreas array according to their zindex. Select the upper one */ - for (let coalitionArea of this.#coalitionAreas) { - if (coalitionArea.getBounds().contains(e.latlng)) { - if (coalitionArea.getSelected()) - clickedCoalitionArea = coalitionArea; - else - this.getMapContextMenu().setCoalitionArea(coalitionArea); + /* Coalition areas are ordered in the #coalitionAreas array according to their zindex. Select the upper one */ + for (let coalitionArea of this.#coalitionAreas) { + if (coalitionArea.getBounds().contains(e.latlng)) { + if (coalitionArea.getSelected()) + clickedCoalitionArea = coalitionArea; + else + this.getMapContextMenu().setCoalitionArea(coalitionArea); + } } + if (clickedCoalitionArea) + this.showCoalitionAreaContextMenu(e.originalEvent.x, e.originalEvent.y, e.latlng, clickedCoalitionArea); } - if (clickedCoalitionArea) - this.showCoalitionAreaContextMenu(e.originalEvent.x, e.originalEvent.y, e.latlng, clickedCoalitionArea); } - } - else if (this.#state === MOVE_UNIT) { - if (!e.originalEvent.ctrlKey) { - getApp().getUnitsManager().selectedUnitsClearDestinations(); + else if (this.#state === MOVE_UNIT) { + if (!e.originalEvent.ctrlKey) { + getApp().getUnitsManager().selectedUnitsClearDestinations(); + } + getApp().getUnitsManager().selectedUnitsAddDestination(this.#computeDestinationRotation && this.#destinationRotationCenter != null ? this.#destinationRotationCenter : e.latlng, this.#shiftKey, this.#destinationGroupRotation) + + this.#destinationGroupRotation = 0; + this.#destinationRotationCenter = null; + this.#computeDestinationRotation = false; + } + else { + this.setState(IDLE); } - getApp().getUnitsManager().selectedUnitsAddDestination(this.#computeDestinationRotation && this.#destinationRotationCenter != null ? this.#destinationRotationCenter : e.latlng, this.#shiftKey, this.#destinationGroupRotation) - - this.#destinationGroupRotation = 0; - this.#destinationRotationCenter = null; - this.#computeDestinationRotation = false; - } - else { - this.setState(IDLE); } } @@ -586,7 +593,7 @@ export class Map extends L.Map { } this.#longPressTimer = window.setTimeout(() => { - if (e.originalEvent.button != 2 || e.originalEvent.ctrlKey) + if (e.originalEvent.button != 2 || e.originalEvent.ctrlKey || e.originalEvent.shiftKey) return; this.hideMapContextMenu(); @@ -684,6 +691,11 @@ export class Map extends L.Map { this.#isZooming = true; } + #onZoom(e: any) { + if (this.#centerUnit != null) + this.#panToUnit(this.#centerUnit); + } + #onZoomEnd(e: any) { this.#isZooming = false; } @@ -726,7 +738,7 @@ export class Map extends L.Map { #showDestinationCursors() { const singleCursor = !this.#shiftKey; - const selectedUnitsCount = getApp().getUnitsManager().getSelectedUnits({ excludeHumans: false, onlyOnePerGroup: true }).length; + const selectedUnitsCount = getApp().getUnitsManager().getSelectedUnits({ excludeHumans: true, onlyOnePerGroup: true }).length; if (selectedUnitsCount > 0) { if (singleCursor && this.#destinationPreviewCursors.length != 1) { this.#hideDestinationCursors(); @@ -740,6 +752,9 @@ export class Map extends L.Map { this.#destinationPreviewCursors.splice(0, 1); } + this.#destinationPreviewHandleLine.addTo(this); + this.#destinationPreviewHandle.addTo(this); + while (this.#destinationPreviewCursors.length < selectedUnitsCount) { var cursor = new DestinationPreviewMarker(this.getMouseCoordinates(), { interactive: false }); cursor.addTo(this); @@ -761,6 +776,9 @@ export class Map extends L.Map { this.#destinationPreviewCursors[idx].setLatLng(this.#shiftKey ? latlng : this.getMouseCoordinates()); }) }; + + this.#destinationPreviewHandleLine.setLatLngs([groupLatLng, this.getMouseCoordinates()]); + this.#destinationPreviewHandle.setLatLng(this.getMouseCoordinates()); } #hideDestinationCursors() { @@ -770,22 +788,15 @@ export class Map extends L.Map { }) this.#destinationPreviewCursors = []; + this.#destinationPreviewHandleLine.removeFrom(this); + this.#destinationPreviewHandle.removeFrom(this); + /* Reset the variables used to compute the rotation of the group cursors */ this.#destinationGroupRotation = 0; this.#computeDestinationRotation = false; this.#destinationRotationCenter = null; } - #showTargetCursor() { - this.#hideTargetCursor(); - this.#targetCursor.addTo(this); - } - - #hideTargetCursor() { - this.#targetCursor.setLatLng(new L.LatLng(0, 0)); - this.removeLayer(this.#targetCursor); - } - #showDrawingCursor() { this.#hideDefaultCursor(); if (!this.hasLayer(this.#drawingCursor)) @@ -803,7 +814,6 @@ export class Map extends L.Map { if (this.#ctrlKey || this.#selecting) { /* Hide all non default cursors */ this.#hideDestinationCursors(); - this.#hideTargetCursor(); this.#hideDrawingCursor(); this.#showDefaultCursor(); @@ -811,13 +821,11 @@ export class Map extends L.Map { /* Hide all the unnecessary cursors depending on the active state */ if (this.#state !== IDLE) this.#hideDefaultCursor(); if (this.#state !== MOVE_UNIT) this.#hideDestinationCursors(); - //if (![BOMBING, CARPET_BOMBING, FIRE_AT_AREA].includes(this.#state)) this.#hideTargetCursor(); if (this.#state !== COALITIONAREA_DRAW_POLYGON) this.#hideDrawingCursor(); /* Show the active cursor depending on the active state */ if (this.#state === IDLE) this.#showDefaultCursor(); else if (this.#state === MOVE_UNIT) this.#showDestinationCursors(); - //else if ([BOMBING, CARPET_BOMBING, FIRE_AT_AREA].includes(this.#state)) this.#showTargetCursor(); else if (this.#state === COALITIONAREA_DRAW_POLYGON) this.#showDrawingCursor(); } } diff --git a/client/src/map/markers/destinationpreviewHandle.ts b/client/src/map/markers/destinationpreviewHandle.ts new file mode 100644 index 00000000..e6037edf --- /dev/null +++ b/client/src/map/markers/destinationpreviewHandle.ts @@ -0,0 +1,19 @@ +import { DivIcon, LatLng } from "leaflet"; +import { CustomMarker } from "../markers/custommarker"; + +export class DestinationPreviewHandle extends CustomMarker { + constructor(latlng: LatLng) { + super(latlng, {interactive: true, draggable: true}); + } + + createIcon() { + this.setIcon(new DivIcon({ + iconSize: [18, 18], + iconAnchor: [9, 9], + className: "leaflet-destination-preview-handle-marker", + })); + var el = document.createElement("div"); + el.classList.add("ol-destination-preview-handle-icon"); + this.getElement()?.appendChild(el); + } +} \ No newline at end of file diff --git a/client/src/mission/missionmanager.ts b/client/src/mission/missionmanager.ts index 00e19632..17b24bf4 100644 --- a/client/src/mission/missionmanager.ts +++ b/client/src/mission/missionmanager.ts @@ -36,8 +36,16 @@ export class MissionManager { } updateBullseyes(data: BullseyesData) { + const commandMode = getApp().getMissionManager().getCommandModeOptions().commandMode; for (let idx in data.bullseyes) { const bullseye = data.bullseyes[idx]; + + // Prevent Red and Blue coalitions seeing each other's bulleye(s) + if ((bullseye.coalition === "red" && commandMode === BLUE_COMMANDER) + || (bullseye.coalition === "blue" && commandMode === RED_COMMANDER)) { + continue; + } + if (!(idx in this.#bullseyes)) this.#bullseyes[idx] = new Bullseye([0, 0]).addTo(getApp().getMap()); diff --git a/client/src/olympusapp.ts b/client/src/olympusapp.ts index a4013b57..8d9120e0 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("unit-list-panel", "unit-list-panel-content")) // Popups this.getPopupsManager().add("infoPopup", new Popup("info-popup")); @@ -196,8 +198,8 @@ export class OlympusApp { if (config && config.address != undefined && config.port != undefined) { const address = config.address; const port = config.port; - if (typeof address === 'string' && typeof port == 'number') { - this.getServerManager().setAddress(address == "*" ? window.location.hostname : address, port); + if (typeof address === 'string' && typeof port == 'number') { + this.getServerManager().setAddress(address == "*" ? window.location.hostname : address, port); } } else { @@ -236,22 +238,38 @@ export class OlympusApp { }); const shortcutManager = this.getShortcutManager(); - shortcutManager.add("toggleDemo", new ShortcutKeyboard({ + shortcutManager.addKeyboardShortcut("toggleDemo", { "callback": () => { this.getServerManager().toggleDemoEnabled(); }, "code": "KeyT" - })).add("togglePause", new ShortcutKeyboard({ + }).addKeyboardShortcut("togglePause", { "altKey": false, "callback": () => { this.getServerManager().setPaused(!this.getServerManager().getPaused()); }, "code": "Space", "ctrlKey": false - })); + }).addKeyboardShortcut("deselectAll", { + "callback": (ev: KeyboardEvent) => { + this.getUnitsManager().deselectAllUnits(); + }, + "code": "Escape" + }).addKeyboardShortcut("toggleUnitLabels", { + "altKey": false, + "callback": () => { + const chk = document.querySelector(`label[title="Show unit labels"] input[type="checkbox"]`); + if (chk instanceof HTMLElement) { + chk.click(); + } + }, + "code": "KeyL", + "ctrlKey": false, + "shiftKey": false + }); ["KeyW", "KeyA", "KeyS", "KeyD", "ArrowLeft", "ArrowRight", "ArrowUp", "ArrowDown"].forEach(code => { - shortcutManager.add(`pan${code}keydown`, new ShortcutKeyboard({ + shortcutManager.addKeyboardShortcut(`pan${code}keydown`, { "altKey": false, "callback": (ev: KeyboardEvent) => { this.getMap().handleMapPanning(ev); @@ -259,30 +277,42 @@ export class OlympusApp { "code": code, "ctrlKey": false, "event": "keydown" - })); - }); + }); - ["KeyW", "KeyA", "KeyS", "KeyD", "ArrowLeft", "ArrowRight", "ArrowUp", "ArrowDown"].forEach(code => { - shortcutManager.add(`pan${code}keyup`, new ShortcutKeyboard({ + shortcutManager.addKeyboardShortcut(`pan${code}keyup`, { "callback": (ev: KeyboardEvent) => { this.getMap().handleMapPanning(ev); }, "code": code - })); + }); }); - ["Digit1", "Digit2", "Digit3", "Digit4", "Digit5", "Digit6", "Digit7", "Digit8", "Digit9"].forEach(code => { - shortcutManager.add(`hotgroup${code}`, new ShortcutKeyboard({ + const digits = ["Digit1", "Digit2", "Digit3", "Digit4", "Digit5", "Digit6", "Digit7", "Digit8", "Digit9"]; + + digits.forEach(code => { + shortcutManager.addKeyboardShortcut(`hotgroup${code}`, { + "altKey": false, "callback": (ev: KeyboardEvent) => { if (ev.ctrlKey && ev.shiftKey) this.getUnitsManager().selectedUnitsAddToHotgroup(parseInt(ev.code.substring(5))); else if (ev.ctrlKey && !ev.shiftKey) this.getUnitsManager().selectedUnitsSetHotgroup(parseInt(ev.code.substring(5))); + else if (!ev.ctrlKey && ev.shiftKey) + this.getUnitsManager().selectUnitsByHotgroup(parseInt(ev.code.substring(5)), false); else this.getUnitsManager().selectUnitsByHotgroup(parseInt(ev.code.substring(5))); }, "code": code - })); + }); + }); + + // Stop hotgroup controls sending the browser to another tab + digits.forEach(code => { + document.addEventListener("keydown", (ev: KeyboardEvent) => { + if (ev.code === code && ev.ctrlKey === true && ev.altKey === false && ev.shiftKey === false) { + ev.preventDefault(); + } + }); }); // TODO: move from here in dedicated class diff --git a/client/src/panels/connectionstatuspanel.ts b/client/src/panels/connectionstatuspanel.ts index 6ab2997e..2621b26a 100644 --- a/client/src/panels/connectionstatuspanel.ts +++ b/client/src/panels/connectionstatuspanel.ts @@ -1,12 +1,27 @@ import { Panel } from "./panel"; export class ConnectionStatusPanel extends Panel { + constructor(ID: string) { super( ID ); } - - update(connected: boolean) { - this.getElement().toggleAttribute( "data-is-connected", connected ); + + showDisconnected() { + this.getElement().toggleAttribute( "data-is-connected", false ); + this.getElement().toggleAttribute( "data-is-paused", false ); } + + + showConnected() { + this.getElement().toggleAttribute( "data-is-connected", true ); + this.getElement().toggleAttribute( "data-is-paused", false ); + } + + + showServerPaused() { + this.getElement().toggleAttribute( "data-is-connected", false ); + this.getElement().toggleAttribute( "data-is-paused", true ); + } + } \ No newline at end of file diff --git a/client/src/panels/mouseinfopanel.ts b/client/src/panels/mouseinfopanel.ts index bec57566..ad24c24f 100644 --- a/client/src/panels/mouseinfopanel.ts +++ b/client/src/panels/mouseinfopanel.ts @@ -26,6 +26,7 @@ export class MouseInfoPanel extends Panel { getApp().getMap()?.on("click", (e: any) => this.#onMapClick(e)); getApp().getMap()?.on('zoom', (e: any) => this.#onZoom(e)); getApp().getMap()?.on('mousemove', (e: any) => this.#onMouseMove(e)); + getApp().getMap()?.on('drag', (e: any) => this.#onMouseMove(e)); document.addEventListener('unitsSelection', (e: CustomEvent) => this.#update()); document.addEventListener('clearSelection', () => this.#update()); @@ -107,14 +108,15 @@ export class MouseInfoPanel extends Panel { #drawMeasureLine() { var mouseLatLng = getApp().getMap().containerPointToLatLng(getApp().getMap().getMousePosition()); + const mousePosition = getApp().getMap().getMousePosition(); if (this.#measurePoint != null) { var points = [this.#measurePoint, mouseLatLng]; this.#measureLine.setLatLngs(points); var dist = distance(this.#measurePoint.lat, this.#measurePoint.lng, mouseLatLng.lat, mouseLatLng.lng); var bear = bearing(this.#measurePoint.lat, this.#measurePoint.lng, mouseLatLng.lat, mouseLatLng.lng); var startXY = getApp().getMap().latLngToContainerPoint(this.#measurePoint); - var dx = (getApp().getMap().getMousePosition().x - startXY.x); - var dy = (getApp().getMap().getMousePosition().y - startXY.y); + var dx = mousePosition.x - startXY.x; + var dy = mousePosition.y - startXY.y; var angle = Math.atan2(dy, dx); if (angle > Math.PI / 2) @@ -133,8 +135,8 @@ export class MouseInfoPanel extends Panel { let data = [`${bng}°`, `${str} ${unit}`]; this.#measureBox.innerText = data.join(" / "); - this.#measureBox.style.left = (getApp().getMap().getMousePosition().x + startXY.x) / 2 - this.#measureBox.offsetWidth / 2 + "px"; - this.#measureBox.style.top = (getApp().getMap().getMousePosition().y + startXY.y) / 2 - this.#measureBox.offsetHeight / 2 + "px"; + this.#measureBox.style.left = (mousePosition.x + startXY.x) / 2 - this.#measureBox.offsetWidth / 2 + "px"; + this.#measureBox.style.top = (mousePosition.y + startXY.y) / 2 - this.#measureBox.offsetHeight / 2 + "px"; this.#measureBox.style.rotate = angle + "rad"; } } diff --git a/client/src/panels/unitcontrolpanel.ts b/client/src/panels/unitcontrolpanel.ts index f6953a12..1ca60845 100644 --- a/client/src/panels/unitcontrolpanel.ts +++ b/client/src/panels/unitcontrolpanel.ts @@ -295,7 +295,7 @@ export class UnitControlPanel extends Panel { const TACANCallsignInput = this.#advancedSettingsDialog.querySelector("#tacan-callsign")?.querySelector("input") as HTMLInputElement; const radioMhzInput = this.#advancedSettingsDialog.querySelector("#radio-mhz")?.querySelector("input") as HTMLInputElement; const radioCallsignNumberInput = this.#advancedSettingsDialog.querySelector("#radio-callsign-number")?.querySelector("input") as HTMLInputElement; - + const unit = units[0]; const roles = aircraftDatabase.getByName(unit.getName())?.loadouts?.map((loadout) => {return loadout.roles}) const tanker = roles != undefined && Array.prototype.concat.apply([], roles)?.includes("Refueling"); @@ -305,6 +305,7 @@ export class UnitControlPanel extends Panel { /* Activate the correct options depending on unit type */ this.#advancedSettingsDialog.toggleAttribute("data-show-settings", !tanker && !AWACS); + this.#advancedSettingsDialog.toggleAttribute("data-show-air-unit-checkboxes", ["Aircraft", "Helicopter"].includes(units[0].getCategory())); this.#advancedSettingsDialog.toggleAttribute("data-show-tasking", tanker || AWACS); this.#advancedSettingsDialog.toggleAttribute("data-show-tanker", tanker); this.#advancedSettingsDialog.toggleAttribute("data-show-AWACS", AWACS); diff --git a/client/src/panels/unitlistpanel.ts b/client/src/panels/unitlistpanel.ts new file mode 100644 index 00000000..b35c6267 --- /dev/null +++ b/client/src/panels/unitlistpanel.ts @@ -0,0 +1,191 @@ +import { getApp } from ".."; +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"; + #units: Unit[] = []; + #unitNameCache: {[key:string]: string} = {}; + #updatesInterval!: ReturnType; + + constructor(panelElement: string, contentElement: string) { + super(panelElement); + 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) { + const el = t.closest( "[data-unit-id]"); + + if (el instanceof HTMLElement) { + let id: number = Number(el.getAttribute("data-unit-id") || 0); + getApp().getUnitsManager().selectUnit(id); + } + } + }); + + new ShortcutKeyboard({ + "callback": () => { this.toggle() }, + "code": "KeyU" + }); + + this.startUpdates(); + } + + doUpdate() { + if (!this.getVisible()) + return; + + this.#contentElement.innerHTML = ""; + + this.#units = Object.values(getApp().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; + } + + const name = unit.getName(); + + if ( this.#unitNameCache.hasOwnProperty( name ) === false ) { + this.#unitNameCache[name] = unit.getDatabase()?.getByName(unit.getName())?.label || unit.getName(); + } + + this.#contentElement.innerHTML += ` +
+
${unit.getUnitName()}
+
${this.#unitNameCache[name]}
+
${unit.getCategory()}
+
${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); + }); + } + + #sortUnitsByCategory() { + this.#units.sort((unit1: Unit, unit2: Unit) => { + let str1 = unit1.getCategory(); + let str2 = unit2.getCategory(); + + let cmp = this.#sortStringsCompare(str1, str2); + + if (cmp !== 0) + return cmp; + + str1 = unit1.getUnitName().toLowerCase(); + 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(); + }, 4000); + } + + stopUpdates() { + clearInterval(this.#updatesInterval); + } + + toggle() { + if (this.getVisible()) + this.stopUpdates(); + else + this.startUpdates(); + + super.toggle(); + } +} diff --git a/client/src/server/servermanager.ts b/client/src/server/servermanager.ts index 361fa6c2..1d36200f 100644 --- a/client/src/server/servermanager.ts +++ b/client/src/server/servermanager.ts @@ -17,6 +17,8 @@ export class ServerManager { #sessionHash: string | null = null; #lastUpdateTimes: {[key: string]: number} = {} #demoEnabled = false; + #previousMissionElapsedTime:number = 0; // Track if mission elapsed time is increasing (i.e. is the server paused) + #serverIsPaused: boolean = false; constructor() { this.#lastUpdateTimes[UNITS_URI] = Date.now(); @@ -445,9 +447,25 @@ export class ServerManager { var time = getApp().getUnitsManager()?.update(buffer); return time; }, true); - (getApp().getPanelsManager().get("connectionStatus") as ConnectionStatusPanel).update(this.getConnected()); + + const elapsedMissionTime = getApp().getMissionManager().getDateAndTime().elapsedTime; + this.#serverIsPaused = ( elapsedMissionTime === this.#previousMissionElapsedTime ); + this.#previousMissionElapsedTime = elapsedMissionTime; + + const csp = (getApp().getPanelsManager().get("connectionStatus") as ConnectionStatusPanel); + + if ( this.getConnected() ) { + if ( this.getServerIsPaused() ) { + csp.showServerPaused(); + } else { + csp.showConnected(); + } + } else { + csp.showDisconnected(); + } + } - }, 5000); + }, ( this.getServerIsPaused() ? 500 : 5000 )); window.setInterval(() => { if (!this.getPaused() && getApp().getMissionManager().getCommandModeOptions().commandMode != NONE) { @@ -521,4 +539,8 @@ export class ServerManager { getPaused() { return this.#paused; } + + getServerIsPaused() { + return this.#serverIsPaused; + } } diff --git a/client/src/shortcut/shortcut.ts b/client/src/shortcut/shortcut.ts index ce9d58e4..09fbe79a 100644 --- a/client/src/shortcut/shortcut.ts +++ b/client/src/shortcut/shortcut.ts @@ -1,4 +1,4 @@ -import { KeyboardShortcutOptions, MouseShortcutOptions, ShortcutOptions } from "../interfaces"; +import { ShortcutKeyboardOptions, ShortcutMouseOptions, ShortcutOptions } from "../interfaces"; import { keyEventWasInInput } from "../other/utils"; export abstract class Shortcut { @@ -14,7 +14,7 @@ export abstract class Shortcut { } export class ShortcutKeyboard extends Shortcut { - constructor(config: KeyboardShortcutOptions) { + constructor(config: ShortcutKeyboardOptions) { config.event = config.event || "keyup"; super(config); @@ -37,7 +37,7 @@ export class ShortcutKeyboard extends Shortcut { } export class ShortcutMouse extends Shortcut { - constructor(config: MouseShortcutOptions) { + constructor(config: ShortcutMouseOptions) { super(config); } } \ No newline at end of file diff --git a/client/src/shortcut/shortcutmanager.ts b/client/src/shortcut/shortcutmanager.ts index 6f1acd21..be8b70fe 100644 --- a/client/src/shortcut/shortcutmanager.ts +++ b/client/src/shortcut/shortcutmanager.ts @@ -1,5 +1,6 @@ +import { ShortcutKeyboardOptions, ShortcutMouseOptions } from "../interfaces"; import { Manager } from "../other/manager"; -import { Shortcut } from "./shortcut"; +import { ShortcutKeyboard, ShortcutMouse } from "./shortcut"; export class ShortcutManager extends Manager { @@ -25,8 +26,18 @@ export class ShortcutManager extends Manager { } - add(name: string, shortcut: Shortcut) { - super.add(name, shortcut); + add( name: string, shortcut:any ) { + console.error( "ShortcutManager:add() cannot be used. Use addKeyboardShortcut or addMouseShortcut." ); + return this; + } + + addKeyboardShortcut( name:string, shortcutKeyboardOptions:ShortcutKeyboardOptions ) { + super.add( name, new ShortcutKeyboard( shortcutKeyboardOptions ) ); + return this; + } + + addMouseShortcut( name:string, shortcutMouseOptions:ShortcutMouseOptions ) { + super.add( name, new ShortcutMouse( shortcutMouseOptions ) ); return this; } diff --git a/client/src/unit/unitsmanager.ts b/client/src/unit/unitsmanager.ts index bdeb4ee7..0631214e 100644 --- a/client/src/unit/unitsmanager.ts +++ b/client/src/unit/unitsmanager.ts @@ -201,8 +201,12 @@ export class UnitsManager { * * @param hotgroup The hotgroup number */ - selectUnitsByHotgroup(hotgroup: number) { - this.deselectAllUnits(); + selectUnitsByHotgroup(hotgroup: number, deselectAllUnits: boolean = true ) { + + if ( deselectAllUnits ) { + this.deselectAllUnits(); + } + this.getUnitsByHotgroup(hotgroup).forEach((unit: Unit) => unit.setSelected(true)) } 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/other/dialogs.ejs b/client/views/other/dialogs.ejs index 3615be2d..3e20be92 100644 --- a/client/views/other/dialogs.ejs +++ b/client/views/other/dialogs.ejs @@ -38,28 +38,28 @@
-
+
-
+
-
+
-
+