diff --git a/client/public/stylesheets/other/contextmenus.css b/client/public/stylesheets/other/contextmenus.css index 172afdac..fd816bb7 100644 --- a/client/public/stylesheets/other/contextmenus.css +++ b/client/public/stylesheets/other/contextmenus.css @@ -8,24 +8,20 @@ z-index: 9999; } -#map-contextmenu>div:nth-child(2) { - align-items: center; + + +/* #map-contextmenu>div:nth-child(n+4)>div { + width: 100%; +} */ + +#map-contextmenu .spawn-mode { display: flex; - flex-direction: row; + flex-direction: column; justify-content: space-between; - padding-right: 0px; + row-gap: 5px; } -#map-contextmenu>div:nth-child(3) { - align-items: center; - display: flex; - flex-direction: row; - justify-content: space-between; - padding-right: 0px; -} - -#map-contextmenu>div:nth-child(n+4) { - align-items: center; +.ol-context-menu-panel { display: flex; flex-direction: column; justify-content: space-between; @@ -33,10 +29,6 @@ padding: 20px; } -#map-contextmenu>div:nth-child(n+4)>div { - width: 100%; -} - .contextmenu-advanced-options, .contextmenu-metadata { align-items: center; @@ -143,20 +135,6 @@ padding: 2px 5px; } -/* -.ol-tag-CA { - background-color: #FF000022; -} - -.ol-tag-Radar { - background-color: #00FF0022; -} - -.ol-tag-IR { - background-color: #0000FF22; -} -*/ - .unit-loadout-list { min-width: 0; } @@ -187,7 +165,65 @@ content: " (" attr(data-points) " points)"; } -.upper-bar svg>* { +#spawn-mode-tabs { + align-items: center; + column-gap: 6px; + display: flex; + position: absolute; + right: 0; + top:0; + translate: -6px -100%; + z-index: 9998; +} + +#spawn-mode-tabs button { + align-items: center; + border-bottom:2px solid transparent; + border-bottom-left-radius: 0; + border-bottom-right-radius: 0; + border-top-left-radius: var(--border-radius-sm); + border-top-right-radius: var(--border-radius-sm); + display: flex; + height:32px; + justify-content: center; + margin:0; + width:38px; +} + +#spawn-mode-tabs button:hover { + background-color: var(--background-steel); +} + +[data-coalition="blue"] + #spawn-mode-tabs button { + border-bottom-color: var(--primary-blue); +} + + +[data-coalition="red"] + #spawn-mode-tabs button { + border-bottom-color: var(--primary-red); +} + + +[data-coalition="neutral"] + #spawn-mode-tabs button { + border-bottom-color: var(--primary-neutral); +} + +#spawn-mode-tabs button svg { + height:24px; + margin:6px; + width:24px; +} + +.upper-bar { + align-items: center; + display: flex; + flex-direction: row; + justify-content: space-between; + padding-right: 0px; +} + +.upper-bar svg>*, +#spawn-mode-tabs button svg * { fill: white; } @@ -200,24 +236,78 @@ margin-left: auto; } +#spawn-history-menu { + align-items: center; + flex-direction: column; + max-height: 300px; + row-gap: 6px; +} + +#spawn-history-menu button { + align-items: center; + column-gap: 6px; + display:flex; + height:32px; + text-align: left; + padding:0; + width:100%; +} + +#spawn-history-menu button span { + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; +} + +#spawn-history-menu button svg { + border-radius: var(--border-radius-sm); + height:24px; + padding:4px; + width:24px; +} + +#spawn-history-menu button:hover { + background-color: transparent; + text-decoration: underline; +} + +#spawn-history-menu button:hover svg * { + fill:white !important; +} + +#spawn-history-menu button[data-spawned-coalition="blue"] svg { + background-color: var(--primary-blue); +} + +#spawn-history-menu button[data-spawned-coalition="red"] svg { + background-color: var(--primary-red); +} + +#spawn-history-menu button[data-spawned-coalition="neutral"] svg { + background-color: var(--primary-neutral); +} + [data-coalition="blue"]#active-coalition-label, [data-coalition="blue"].deploy-unit-button, [data-coalition="blue"]#spawn-airbase-aircraft-button, -[data-coalition="blue"].create-iads-button { +[data-coalition="blue"].create-iads-button, +[data-coalition="blue"] + #spawn-mode-tabs button.selected { background-color: var(--primary-blue) } [data-coalition="red"]#active-coalition-label, [data-coalition="red"].deploy-unit-button, [data-coalition="red"]#spawn-airbase-aircraft-button, -[data-coalition="red"].create-iads-button { +[data-coalition="red"].create-iads-button, +[data-coalition="red"] + #spawn-mode-tabs button.selected { background-color: var(--primary-red) } [data-coalition="neutral"]#active-coalition-label, [data-coalition="neutral"].deploy-unit-button, [data-coalition="neutral"]#spawn-airbase-aircraft-button, -[data-coalition="neutral"].create-iads-button { +[data-coalition="neutral"].create-iads-button, +[data-coalition="neutral"] + #spawn-mode-tabs button.selected { background-color: var(--primary-neutral) } diff --git a/client/public/themes/olympus/images/buttons/other/arrow-down-solid.svg b/client/public/themes/olympus/images/buttons/other/arrow-down-solid.svg new file mode 100644 index 00000000..a31ed4b6 --- /dev/null +++ b/client/public/themes/olympus/images/buttons/other/arrow-down-solid.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/client/public/themes/olympus/images/buttons/other/clock-rotate-left-solid.svg b/client/public/themes/olympus/images/buttons/other/clock-rotate-left-solid.svg new file mode 100644 index 00000000..50966792 --- /dev/null +++ b/client/public/themes/olympus/images/buttons/other/clock-rotate-left-solid.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/client/src/constants/constants.ts b/client/src/constants/constants.ts index de8cfb55..7ada419b 100644 --- a/client/src/constants/constants.ts +++ b/client/src/constants/constants.ts @@ -221,16 +221,19 @@ export const MAP_MARKER_CONTROLS: MapMarkerVisibilityControl[] = [{ "toggles": ["dcs"], "tooltip": "Toggle DCS-controlled units' visibility" }, { + "category": "Aircraft", "image": "visibility/aircraft.svg", "name": "Aircraft", "toggles": ["aircraft"], "tooltip": "Toggle aircraft's visibility" }, { + "category": "Helicopter", "image": "visibility/helicopter.svg", "name": "Helicopter", "toggles": ["helicopter"], "tooltip": "Toggle helicopters' visibility" }, { + "category": "AirDefence", "image": "visibility/groundunit-sam.svg", "name": "Air defence", "toggles": ["groundunit-sam"], @@ -241,6 +244,7 @@ export const MAP_MARKER_CONTROLS: MapMarkerVisibilityControl[] = [{ "toggles": ["groundunit"], "tooltip": "Toggle ground units' visibility" }, { + "category": "GroundUnit", "image": "visibility/navyunit.svg", "name": "Naval", "toggles": ["navyunit"], diff --git a/client/src/contextmenus/mapcontextmenu.ts b/client/src/contextmenus/mapcontextmenu.ts index d7619c99..bff15bc3 100644 --- a/client/src/contextmenus/mapcontextmenu.ts +++ b/client/src/contextmenus/mapcontextmenu.ts @@ -5,8 +5,10 @@ import { Switch } from "../controls/switch"; import { GAME_MASTER } from "../constants/constants"; import { CoalitionArea } from "../map/coalitionarea/coalitionarea"; import { AirDefenceUnitSpawnMenu, AircraftSpawnMenu, GroundUnitSpawnMenu, HelicopterSpawnMenu, NavyUnitSpawnMenu } from "../controls/unitspawnmenu"; -import { Airbase } from "../mission/airbase"; import { SmokeMarker } from "../map/markers/smokemarker"; +import { UnitSpawnTable } from "../interfaces"; +import { getCategoryBlueprintIconSVG, getUnitDatabaseByCategory } from "../other/utils"; +import { SVGInjector } from "@tanem/svg-injector"; /** The MapContextMenu is the main contextmenu shown to the user whenever it rightclicks on the map. It is the primary interaction method for the user. * It allows to spawn units, create explosions and smoke, and edit CoalitionAreas. @@ -96,6 +98,8 @@ export class MapContextMenu extends ContextMenu { this.getContainer()?.addEventListener("hide", () => this.#airDefenceUnitSpawnMenu.clearCirclesPreviews()); this.getContainer()?.addEventListener("hide", () => this.#groundUnitSpawnMenu.clearCirclesPreviews()); this.getContainer()?.addEventListener("hide", () => this.#navyUnitSpawnMenu.clearCirclesPreviews()); + + this.#setupHistory(); } /** Show the contextmenu on top of the map, usually at the location where the user has clicked on it. @@ -257,4 +261,91 @@ export class MapContextMenu extends ContextMenu { this.#groundUnitSpawnMenu.setCountries(); this.#navyUnitSpawnMenu.setCountries(); } + + /** Handles all of the logic for historal logging. + * + */ + #setupHistory() { + /* Set up the tab clicks */ + const spawnModes = this.getContainer()?.querySelectorAll(".spawn-mode"); + const activeCoalitionLabel = document.getElementById("active-coalition-label"); + const tabs = this.getContainer()?.querySelectorAll(".spawn-mode-tab"); + + // Default selected tab to the "spawn now" option + if (tabs) tabs[tabs.length-1].classList.add("selected"); + + tabs?.forEach((btn:Element) => { + btn.addEventListener("click", (ev:MouseEventInit) => { + // Highlight tab + tabs.forEach(tab => tab.classList.remove("selected")); + btn.classList.add("selected"); + + // Hide/reset + spawnModes?.forEach(div => div.classList.add("hide")); + + const prevSiblings = []; + let prev = btn.previousElementSibling; + + /* Tabs and content windows are assumed to be in the same order */ + // Count previous + while ( prev ) { + prevSiblings.push(prev); + prev = prev.previousElementSibling; + } + + // Show content + if (spawnModes && spawnModes[prevSiblings.length]) { + spawnModes[prevSiblings.length].classList.remove("hide"); + } + + // We don't want to see the "Spawn [coalition] unit" label + if (activeCoalitionLabel) activeCoalitionLabel.classList.toggle("hide", !btn.hasAttribute("data-show-label")); + }); + }); + + const history = document.getElementById("spawn-history-menu"); + const maxEntries = 20; + + /** Listen for unit spawned **/ + document.addEventListener( "unitSpawned", (ev:CustomEventInit) => { + const buttons = history.querySelectorAll("button"); + const detail:any = ev.detail; + if (buttons.length === 0) history.innerHTML = ""; // Take out any "no data" messages + const button = document.createElement("button"); + button.title = "Click to spawn"; + button.setAttribute("data-spawned-coalition", detail.coalition); + button.setAttribute("data-unit-type", detail.unitSpawnTable[0].unitType); + button.setAttribute("data-unit-qty", detail.unitSpawnTable.length); + + const db = getUnitDatabaseByCategory(detail.category); + button.innerHTML = `${db?.getByName(detail.unitSpawnTable[0].unitType)?.label} (${detail.unitSpawnTable.length})`; + + // Remove a previous instance to save clogging up the list + const previous:any = [].slice.call(buttons).find( (button:Element) => ( + detail.coalition === button.getAttribute("data-spawned-coalition") && + detail.unitSpawnTable[0].unitType === button.getAttribute("data-unit-type") && + detail.unitSpawnTable.length === parseInt(button.getAttribute("data-unit-qty") || "-1"))); + + if (previous instanceof HTMLElement) previous.remove(); + + /* Click to do the spawn */ + button.addEventListener("click", (ev:MouseEventInit) => { + detail.unitSpawnTable.forEach((table:UnitSpawnTable, i:number) => { + table.location = this.getLatLng(); // Set to new menu location + table.location.lat += 0.00015 * i; + }); + getApp().getUnitsManager().spawnUnits(detail.category, detail.unitSpawnTable, detail.coalition, detail.immediate, detail.airbase, detail.country); + this.hide(); + }); + + /* Insert into DOM */ + history.prepend(button); + SVGInjector(button.querySelectorAll("img")); + + /* Trim down to max number of entries */ + while (history.querySelectorAll("button").length > maxEntries) { + history.childNodes[maxEntries].remove(); + } + }); + } } \ No newline at end of file diff --git a/client/src/map/map.ts b/client/src/map/map.ts index be47a116..6218214a 100644 --- a/client/src/map/map.ts +++ b/client/src/map/map.ts @@ -38,6 +38,7 @@ require("../../public/javascripts/leaflet.nauticscale.js") require("../../public/javascripts/L.Path.Drag.js") export type MapMarkerVisibilityControl = { + "category"?: string; "image": string; "isProtected"?: boolean, "name": string, diff --git a/client/src/other/utils.ts b/client/src/other/utils.ts index 74866674..0476a610 100644 --- a/client/src/other/utils.ts +++ b/client/src/other/utils.ts @@ -5,7 +5,7 @@ import { aircraftDatabase } from "../unit/databases/aircraftdatabase"; import { helicopterDatabase } from "../unit/databases/helicopterdatabase"; import { groundUnitDatabase } from "../unit/databases/groundunitdatabase"; import { Buffer } from "buffer"; -import { ROEs, emissionsCountermeasures, reactionsToThreat, states } from "../constants/constants"; +import { GROUND_UNIT_AIR_DEFENCE_REGEX, ROEs, emissionsCountermeasures, reactionsToThreat, states } from "../constants/constants"; import { Dropdown } from "../controls/dropdown"; import { navyUnitDatabase } from "../unit/databases/navyunitdatabase"; import { DateAndTime, UnitBlueprint } from "../interfaces"; @@ -379,6 +379,20 @@ export function getUnitDatabaseByCategory(category: string) { return null; } +export function getCategoryBlueprintIconSVG(category:string, unitName:string) { + + const path = "/resources/theme/images/buttons/visibility/"; + + // We can just send these back okay + if (["Aircraft", "Helicopter", "NavyUnit"].includes(category)) return `${path}${category.toLowerCase()}.svg`; + + // Return if not a ground units as it's therefore something we don't recognise + if (category !== "GroundUnit") return false; + + /** We need to get the unit detail for ground units so we can work out if it's an air defence unit or not **/ + return GROUND_UNIT_AIR_DEFENCE_REGEX.test(unitName) ? `${path}groundunit-sam.svg` : `${path}groundunit.svg`; +} + export function base64ToBytes(base64: string) { return Buffer.from(base64, 'base64').buffer; } diff --git a/client/src/unit/unitsmanager.ts b/client/src/unit/unitsmanager.ts index da455dc1..28556b46 100644 --- a/client/src/unit/unitsmanager.ts +++ b/client/src/unit/unitsmanager.ts @@ -1427,6 +1427,16 @@ export class UnitsManager { if (spawnPoints <= getApp().getMissionManager().getAvailableSpawnPoints()) { getApp().getMissionManager().setSpentSpawnPoints(spawnPoints); spawnFunction(); + document.dispatchEvent( new CustomEvent( "unitSpawned", { + "detail": { + "airbase": airbase, + "category": category, + "coalition": coalition, + "country": country, + "immediate": immediate, + "unitSpawnTable": units + } + })); return true; } else { (getApp().getPopupsManager().get("infoPopup") as Popup).setText("Not enough spawn points available!"); diff --git a/client/views/contextmenus/map.ejs b/client/views/contextmenus/map.ejs index a3b87b49..4f375786 100644 --- a/client/views/contextmenus/map.ejs +++ b/client/views/contextmenus/map.ejs @@ -1,58 +1,71 @@
-
-
- - - - - - +
+ +
-
- - - - +
+

Spawn history

+
+

You do not have any units to show.

-
-
-
- -
-
- -
-
- -
- -
- - - - - -
-
- - - - - - +
+
+
+ + + + + + + +
+
+ + + + +
+
+ +
+
+ +
+
+ +
+
+ +
+ +
+ + + + + +
+
+ + + + + + +
\ No newline at end of file