diff --git a/client/demo.js b/client/demo.js index 9e560bee..b23e5b92 100644 --- a/client/demo.js +++ b/client/demo.js @@ -54,7 +54,7 @@ class DemoDataGenerator { /* - UNCOMMENT TO TEST ALL UNITS + ***************** UNCOMMENT TO TEST ALL UNITS **************** var databases = Object.assign({}, aircraftDatabase, helicopterDatabase, groundUnitDatabase, navyUnitDatabase); var t = Object.keys(databases).length; @@ -114,6 +114,39 @@ class DemoDataGenerator { DEMO_UNIT_DATA[idx].position.lat += idx / 100; DEMO_UNIT_DATA[idx].category = "GroundUnit"; DEMO_UNIT_DATA[idx].isLeader = false; + + idx += 1; + DEMO_UNIT_DATA[idx] = JSON.parse(JSON.stringify(baseData)); + DEMO_UNIT_DATA[idx].name = "F-14B"; + DEMO_UNIT_DATA[idx].groupName = `Group-1`; + DEMO_UNIT_DATA[idx].position.lat += idx / 100; + DEMO_UNIT_DATA[idx].category = "Aircraft"; + DEMO_UNIT_DATA[idx].isLeader = false; + + idx += 1; + DEMO_UNIT_DATA[idx] = JSON.parse(JSON.stringify(baseData)); + DEMO_UNIT_DATA[idx].name = "Infantry AK"; + DEMO_UNIT_DATA[idx].groupName = `Group-2`; + DEMO_UNIT_DATA[idx].position.lat += idx / 100; + DEMO_UNIT_DATA[idx].category = "GroundUnit"; + DEMO_UNIT_DATA[idx].isLeader = true; + + idx += 1; + DEMO_UNIT_DATA[idx] = JSON.parse(JSON.stringify(baseData)); + DEMO_UNIT_DATA[idx].name = "Infantry AK"; + DEMO_UNIT_DATA[idx].groupName = `Group-3`; + DEMO_UNIT_DATA[idx].position.lat += idx / 100; + DEMO_UNIT_DATA[idx].category = "GroundUnit"; + DEMO_UNIT_DATA[idx].isLeader = true; + + idx += 1; + DEMO_UNIT_DATA[idx] = JSON.parse(JSON.stringify(baseData)); + DEMO_UNIT_DATA[idx].name = "F-14B"; + DEMO_UNIT_DATA[idx].groupName = `Group-4`; + DEMO_UNIT_DATA[idx].position.lat += idx / 100; + DEMO_UNIT_DATA[idx].category = "Aircraft"; + DEMO_UNIT_DATA[idx].isLeader = true; + this.startTime = Date.now(); } diff --git a/client/src/contextmenus/airbasecontextmenu.ts b/client/src/contextmenus/airbasecontextmenu.ts index b6b753ac..85738197 100644 --- a/client/src/contextmenus/airbasecontextmenu.ts +++ b/client/src/contextmenus/airbasecontextmenu.ts @@ -24,7 +24,7 @@ export class AirbaseContextMenu extends ContextMenu { document.addEventListener("contextMenuLandAirbase", (e: any) => { if (this.#airbase) - getApp().getUnitsManager().selectedUnitsLandAt(this.#airbase.getLatLng()); + getApp().getUnitsManager().landAt(this.#airbase.getLatLng()); this.hide(); }) } @@ -111,7 +111,7 @@ export class AirbaseContextMenu extends ContextMenu { #showSpawnMenu() { if (this.#airbase != null) { getApp().setActiveCoalition(this.#airbase.getCoalition()); - getApp().getMap().showAirbaseSpawnMenu(this.getX(), this.getY(), this.getLatLng(), this.#airbase); + getApp().getMap().showAirbaseSpawnMenu(this.#airbase, this.getX(), this.getY(), this.getLatLng()); } } diff --git a/client/src/contextmenus/airbasespawnmenu.ts b/client/src/contextmenus/airbasespawnmenu.ts index a5c40e3c..ff753c6e 100644 --- a/client/src/contextmenus/airbasespawnmenu.ts +++ b/client/src/contextmenus/airbasespawnmenu.ts @@ -46,7 +46,7 @@ export class AirbaseSpawnContextMenu extends ContextMenu { * @param x X screen coordinate of the top left corner of the context menu * @param y Y screen coordinate of the top left corner of the context menu */ - show(x: number, y: number) { + show(x: number | undefined, y: number | undefined) { super.show(x, y, new LatLng(0, 0)); this.#aircraftSpawnMenu.setAirbase(undefined); diff --git a/client/src/contextmenus/contextmenu.ts b/client/src/contextmenus/contextmenu.ts index fb923e10..78d91713 100644 --- a/client/src/contextmenus/contextmenu.ts +++ b/client/src/contextmenus/contextmenu.ts @@ -19,15 +19,15 @@ export class ContextMenu { /** Show the contextmenu on top of the map, usually at the location where the user has clicked on it. * - * @param x X screen coordinate of the top left corner of the context menu - * @param y Y screen coordinate of the top left corner of the context menu - * @param latlng Leaflet latlng object of the mouse click + * @param x X screen coordinate of the top left corner of the context menu. If undefined, use the old value + * @param y Y screen coordinate of the top left corner of the context menu. If undefined, use the old value + * @param latlng Leaflet latlng object of the mouse click. If undefined, use the old value */ - show(x: number, y: number, latlng: LatLng) { - this.#latlng = latlng; + show(x: number | undefined = undefined, y: number | undefined = undefined, latlng: LatLng | undefined = undefined) { + this.#latlng = latlng ?? this.#latlng; this.#container?.classList.toggle("hide", false); - this.#x = x; - this.#y = y; + this.#x = x ?? this.#x; + this.#y = y ?? this.#y; this.clip(); this.getContainer()?.dispatchEvent(new Event("show")); } diff --git a/client/src/contextmenus/unitcontextmenu.ts b/client/src/contextmenus/unitcontextmenu.ts index fe1ee735..57502abb 100644 --- a/client/src/contextmenus/unitcontextmenu.ts +++ b/client/src/contextmenus/unitcontextmenu.ts @@ -1,4 +1,4 @@ -import { deg2rad, ftToM } from "../other/utils"; +import { ContextActionSet } from "../unit/contextactionset"; import { ContextMenu } from "./contextmenu"; /** The UnitContextMenu is shown when the user rightclicks on a unit. It dynamically presents the user with possible actions to perform on the unit. */ @@ -16,15 +16,19 @@ export class UnitContextMenu extends ContextMenu { * @param options Dictionary element containing the text and tooltip of the options shown in the menu * @param callback Callback that will be called when the user clicks on one of the options */ - setOptions(options: { [key: string]: {text: string, tooltip: string }}, callback: CallableFunction) { - this.getContainer()?.replaceChildren(...Object.keys(options).map((key: string, idx: number) => { - const option = options[key]; + setContextActions(contextActionSet: ContextActionSet) { + this.getContainer()?.replaceChildren(...Object.keys(contextActionSet.getContextActions()).map((key: string, idx: number) => { + const contextAction = contextActionSet.getContextActions()[key]; var button = document.createElement("button"); var el = document.createElement("div"); - el.title = option.tooltip; - el.innerText = option.text; + el.title = contextAction.getDescription(); + el.innerText = contextAction.getLabel(); el.id = key; - button.addEventListener("click", () => callback(key)); + button.addEventListener("click", () => { + contextAction.executeCallback(); + if (contextAction.getHideContextAfterExecution()) + this.hide(); + }); button.appendChild(el); return (button); })); diff --git a/client/src/map/map.ts b/client/src/map/map.ts index b176e137..f4df7ecd 100644 --- a/client/src/map/map.ts +++ b/client/src/map/map.ts @@ -12,16 +12,16 @@ import { DestinationPreviewMarker } from "./markers/destinationpreviewmarker"; import { TemporaryUnitMarker } from "./markers/temporaryunitmarker"; import { ClickableMiniMap } from "./clickableminimap"; import { SVGInjector } from '@tanem/svg-injector' -import { mapLayers, mapBounds, minimapBoundaries, IDLE, COALITIONAREA_DRAW_POLYGON, visibilityControls, visibilityControlsTooltips, MOVE_UNIT, SHOW_UNIT_CONTACTS, HIDE_GROUP_MEMBERS, SHOW_UNIT_PATHS, SHOW_UNIT_TARGETS, visibilityControlsTypes, SHOW_UNIT_LABELS, SHOW_UNITS_ENGAGEMENT_RINGS, SHOW_UNITS_ACQUISITION_RINGS, HIDE_UNITS_SHORT_RANGE_RINGS, FILL_SELECTED_RING, MAP_MARKER_CONTROLS } from "../constants/constants"; +import { mapLayers, mapBounds, minimapBoundaries, IDLE, COALITIONAREA_DRAW_POLYGON, MOVE_UNIT, SHOW_UNIT_CONTACTS, HIDE_GROUP_MEMBERS, SHOW_UNIT_PATHS, SHOW_UNIT_TARGETS, SHOW_UNIT_LABELS, SHOW_UNITS_ENGAGEMENT_RINGS, SHOW_UNITS_ACQUISITION_RINGS, HIDE_UNITS_SHORT_RANGE_RINGS, FILL_SELECTED_RING, MAP_MARKER_CONTROLS } from "../constants/constants"; import { TargetMarker } from "./markers/targetmarker"; import { CoalitionArea } from "./coalitionarea/coalitionarea"; import { CoalitionAreaContextMenu } from "../contextmenus/coalitionareacontextmenu"; import { DrawingCursor } from "./coalitionarea/drawingcursor"; 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"; +import { ContextActionSet } from "../unit/contextactionset"; var hasTouchScreen = false; //if ("maxTouchPoints" in navigator) @@ -322,7 +322,7 @@ export class Map extends L.Map { return this.#mapContextMenu; } - showUnitContextMenu(x: number, y: number, latlng: L.LatLng) { + showUnitContextMenu(x: number | undefined = undefined, y: number | undefined = undefined, latlng: L.LatLng | undefined = undefined) { this.hideAllContextMenus(); this.#unitContextMenu.show(x, y, latlng); } @@ -335,7 +335,7 @@ export class Map extends L.Map { this.#unitContextMenu.hide(); } - showAirbaseContextMenu(x: number, y: number, latlng: L.LatLng, airbase: Airbase) { + showAirbaseContextMenu(airbase: Airbase, x: number | undefined = undefined, y: number | undefined = undefined, latlng: L.LatLng | undefined = undefined) { this.hideAllContextMenus(); this.#airbaseContextMenu.show(x, y, latlng); this.#airbaseContextMenu.setAirbase(airbase); @@ -349,7 +349,7 @@ export class Map extends L.Map { this.#airbaseContextMenu.hide(); } - showAirbaseSpawnMenu(x: number, y: number, latlng: L.LatLng, airbase: Airbase) { + showAirbaseSpawnMenu(airbase: Airbase, x: number | undefined = undefined, y: number | undefined = undefined, latlng: L.LatLng | undefined = undefined) { this.hideAllContextMenus(); this.#airbaseSpawnMenu.show(x, y); this.#airbaseSpawnMenu.setAirbase(airbase); @@ -561,9 +561,9 @@ export class Map extends L.Map { } else if (this.#state === MOVE_UNIT) { if (!e.originalEvent.ctrlKey) { - getApp().getUnitsManager().selectedUnitsClearDestinations(); + getApp().getUnitsManager().clearDestinations(); } - getApp().getUnitsManager().selectedUnitsAddDestination(this.#computeDestinationRotation && this.#destinationRotationCenter != null ? this.#destinationRotationCenter : e.latlng, this.#shiftKey, this.#destinationGroupRotation) + getApp().getUnitsManager().addDestination(this.#computeDestinationRotation && this.#destinationRotationCenter != null ? this.#destinationRotationCenter : e.latlng, this.#shiftKey, this.#destinationGroupRotation) this.#destinationGroupRotation = 0; this.#destinationRotationCenter = null; @@ -611,59 +611,15 @@ export class Map extends L.Map { if (e.originalEvent.button != 2 || e.originalEvent.ctrlKey || e.originalEvent.shiftKey) return; - var options: { [key: string]: { text: string, tooltip: string } } = {}; - const selectedUnits = getApp().getUnitsManager().getSelectedUnits(); - const selectedUnitTypes = getApp().getUnitsManager().getSelectedUnitsCategories(); - - if (selectedUnitTypes.length === 1 && ["Aircraft", "Helicopter"].includes(selectedUnitTypes[0])) { - if (selectedUnits.every((unit: Unit) => { return unit.canLandAtPoint()})) - options["land-at-point"] = { text: "Land here", tooltip: "Land at this precise location" }; - - if (selectedUnits.every((unit: Unit) => { return unit.canTargetPoint()})) { - options["bomb"] = { text: "Precision bombing", tooltip: "Precision bombing of a specific point" }; - options["carpet-bomb"] = { text: "Carpet bombing", tooltip: "Carpet bombing close to a point" }; - } - - if (Object.keys(options).length === 0) - (getApp().getPopupsManager().get("infoPopup") as Popup).setText(`Selected units can not perform point actions.`); - } - else if (selectedUnitTypes.length === 1 && ["GroundUnit", "NavyUnit"].includes(selectedUnitTypes[0])) { - if (selectedUnits.every((unit: Unit) => { return unit.canTargetPoint() })) { - options["fire-at-area"] = { text: "Fire at area", tooltip: "Fire at a large area" }; - options["simulate-fire-fight"] = { text: "Simulate fire fight", tooltip: "Simulate a fire fight by shooting randomly in a certain large area" }; - } - else - (getApp().getPopupsManager().get("infoPopup") as Popup).setText(`Selected units can not perform point actions.`); - } - else if(selectedUnitTypes.length > 1) { - (getApp().getPopupsManager().get("infoPopup") as Popup).setText(`Multiple unit types selected, no common actions available.`); - } - - if (Object.keys(options).length > 0) { - this.showUnitContextMenu(e.originalEvent.x, e.originalEvent.y, e.latlng); - this.getUnitContextMenu().setOptions(options, (option: string) => { - this.hideUnitContextMenu(); - if (option === "bomb") { - getApp().getUnitsManager().getSelectedUnits().length > 0 ? this.setState(MOVE_UNIT) : this.setState(IDLE); - getApp().getUnitsManager().selectedUnitsBombPoint(this.getMouseCoordinates()); - } - else if (option === "carpet-bomb") { - getApp().getUnitsManager().getSelectedUnits().length > 0 ? this.setState(MOVE_UNIT) : this.setState(IDLE); - getApp().getUnitsManager().selectedUnitsCarpetBomb(this.getMouseCoordinates()); - } - else if (option === "fire-at-area") { - getApp().getUnitsManager().getSelectedUnits().length > 0 ? this.setState(MOVE_UNIT) : this.setState(IDLE); - getApp().getUnitsManager().selectedUnitsFireAtArea(this.getMouseCoordinates()); - } - else if (option === "simulate-fire-fight") { - getApp().getUnitsManager().getSelectedUnits().length > 0 ? this.setState(MOVE_UNIT) : this.setState(IDLE); - getApp().getUnitsManager().selectedUnitsSimulateFireFight(this.getMouseCoordinates()); - } - else if (option === "land-at-point") { - getApp().getUnitsManager().getSelectedUnits().length > 0 ? this.setState(MOVE_UNIT) : this.setState(IDLE); - getApp().getUnitsManager().selectedUnitsLandAtPoint(this.getMouseCoordinates()); - } - }); + var contextActionSet = new ContextActionSet(); + var units = getApp().getUnitsManager().getSelectedUnits(); + units.forEach((unit: Unit) => { + unit.appendContextActions(contextActionSet, null, e.latlng); + }) + + if (Object.keys(contextActionSet.getContextActions()).length > 0) { + getApp().getMap().showUnitContextMenu(e.originalEvent.x, e.originalEvent.y, e.latlng); + getApp().getMap().getUnitContextMenu().setContextActions(contextActionSet); } }, 150); this.#longPressHandled = false; @@ -742,6 +698,8 @@ export class Map extends L.Map { const toggles = `["${control.toggles.join('","')}"]`; const div = document.createElement("div"); div.className = control.protectable === true ? "protectable" : ""; + + // TODO: for consistency let's avoid using innerHTML. Let's create elements. div.innerHTML = `