diff --git a/client/public/stylesheets/other/contextmenus.css b/client/public/stylesheets/other/contextmenus.css index e621d13d..57fc1341 100644 --- a/client/public/stylesheets/other/contextmenus.css +++ b/client/public/stylesheets/other/contextmenus.css @@ -4,7 +4,7 @@ height: fit-content; position: absolute; row-gap: 5px; - width: 230px; + width: 280px; z-index: 9999; } @@ -109,6 +109,11 @@ background-size: 48px; } +#explosion-spawn-button { + background-image: url("/resources/theme/images/buttons/spawn/explosion.svg"); + background-size: 48px; +} + .unit-spawn-button { border: none; border-radius: 0px; @@ -208,6 +213,7 @@ text-align: center; } +#explosion-menu>button, #smoke-spawn-menu>button { align-items: center; column-gap: 10px; diff --git a/client/public/themes/olympus/images/buttons/spawn/explosion.svg b/client/public/themes/olympus/images/buttons/spawn/explosion.svg new file mode 100644 index 00000000..192784b0 --- /dev/null +++ b/client/public/themes/olympus/images/buttons/spawn/explosion.svg @@ -0,0 +1,42 @@ + + diff --git a/client/src/controls/mapcontextmenu.ts b/client/src/controls/mapcontextmenu.ts index 38cbba2c..863238a2 100644 --- a/client/src/controls/mapcontextmenu.ts +++ b/client/src/controls/mapcontextmenu.ts @@ -1,6 +1,6 @@ import { LatLng } from "leaflet"; import { getActiveCoalition, getMap, setActiveCoalition } from ".."; -import { spawnAircraft, spawnGroundUnit, spawnSmoke } from "../server/server"; +import { spawnAircraft, spawnExplosion, spawnGroundUnit, spawnSmoke } from "../server/server"; import { aircraftDatabase } from "../units/aircraftdatabase"; import { groundUnitsDatabase } from "../units/groundunitsdatabase"; import { ContextMenu } from "./contextmenu"; @@ -38,6 +38,9 @@ export class MapContextMenu extends ContextMenu { this.#aircraftTypeDropdown = new Dropdown("aircraft-type-options", (type: string) => this.#setAircraftType(type)); this.#aircraftLoadoutDropdown = new Dropdown("loadout-options", (loadout: string) => this.#setAircraftLoadout(loadout)); this.#aircrafSpawnAltitudeSlider = new Slider("aircraft-spawn-altitude-slider", 0, 50000, "ft", (value: number) => {this.#spawnOptions.altitude = value;}); + this.#aircrafSpawnAltitudeSlider.setIncrement(500); + this.#aircrafSpawnAltitudeSlider.setValue(20000); + this.#aircrafSpawnAltitudeSlider.setActive(true); this.#groundUnitRoleDropdown = new Dropdown("ground-unit-role-options", (role: string) => this.#setGroundUnitRole(role)); this.#groundUnitTypeDropdown = new Dropdown("ground-unit-type-options", (type: string) => this.#setGroundUnitType(type)); @@ -68,9 +71,11 @@ export class MapContextMenu extends ContextMenu { spawnSmoke(e.detail.color, this.getLatLng()); }); - this.#aircrafSpawnAltitudeSlider.setIncrement(500); - this.#aircrafSpawnAltitudeSlider.setValue(20000); - this.#aircrafSpawnAltitudeSlider.setActive(true); + document.addEventListener("contextMenuExplosion", (e: any) => { + this.hide(); + spawnExplosion(e.detail.strength, this.getLatLng()); + }); + this.hide(); } @@ -89,6 +94,8 @@ export class MapContextMenu extends ContextMenu { this.getContainer()?.querySelector("#ground-unit-spawn-button")?.classList.toggle("is-open", type === "ground-unit"); this.getContainer()?.querySelector("#smoke-spawn-menu")?.classList.toggle("hide", type !== "smoke"); this.getContainer()?.querySelector("#smoke-spawn-button")?.classList.toggle("is-open", type === "smoke"); + this.getContainer()?.querySelector("#explosion-menu")?.classList.toggle("hide", type !== "explosion"); + this.getContainer()?.querySelector("#explosion-spawn-button")?.classList.toggle("is-open", type === "explosion"); this.#resetAircraftRole(); this.#resetAircraftType(); diff --git a/client/src/map/map.ts b/client/src/map/map.ts index 6a75186b..eaf0951d 100644 --- a/client/src/map/map.ts +++ b/client/src/map/map.ts @@ -21,7 +21,7 @@ require("../../public/javascripts/leaflet.nauticscale.js") /* Map constants */ export const IDLE = "IDLE"; -export const MOVE_UNIT = "MOVE_UNIT"; +export const UNIT_SELECTED = "MOVE_UNIT"; export const visibilityControls: string[] = ["human", "dcs", "aircraft", "groundunit-sam", "groundunit-other", "navyunit", "airbase"]; export const visibilityControlsTootlips: string[] = ["Toggle human players visibility", "Toggle DCS controlled units visibility", "Toggle aircrafts visibility", "Toggle SAM units visibility", "Toggle ground units (not SAM) visibility", "Toggle navy units visibility", "Toggle airbases visibility"]; @@ -159,7 +159,7 @@ export class Map extends L.Map { this.#computeDestinationRotation = false; this.#destinationRotationCenter = null; } - else if (this.#state === MOVE_UNIT) { + else if (this.#state === UNIT_SELECTED) { /* Remove all the exising destination preview markers */ this.#destinationPreviewMarkers.forEach((marker: L.Marker) => { this.removeLayer(marker); @@ -363,7 +363,7 @@ export class Map extends L.Map { if (this.#state === IDLE) { } - else if (this.#state === MOVE_UNIT) { + else if (this.#state === UNIT_SELECTED) { this.setState(IDLE); getUnitsManager().deselectAllUnits(); } @@ -381,17 +381,52 @@ export class Map extends L.Map { this.showMapContextMenu(e); } } - else if (this.#state === MOVE_UNIT) { - if (!e.originalEvent.ctrlKey) { - getUnitsManager().selectedUnitsClearDestinations(); + else if (this.#state === UNIT_SELECTED) { + if (e.originalEvent.shiftKey) { + var options: {[key: string]: {text: string, tooltip: string}} = {}; + var selectedUnitTypes = getUnitsManager().getSelectedUnitsTypes(); + + if (selectedUnitTypes.length === 1 && ["Aircraft"].includes(selectedUnitTypes[0])) + { + options["bomb"] = {text: "Bomb here", tooltip: "Precision bombing of this specific point"}; + options["carpet-bomb"] = {text: "Carpet bomb", tooltip: "Carpet bombing around this point"}; + options["building-bomb"] = {text: "Bomb building", tooltip: "Precision bombing of the building closest to this point"}; + } + + if (selectedUnitTypes.length === 1 && ["GroundUnit"].includes(selectedUnitTypes[0])) + options["fire-at-area"] = {text: "Fire at area", tooltip: "Fire at this point"}; + + if (Object.keys(options).length > 0) { + this.showUnitContextMenu(e); + this.getUnitContextMenu().setOptions(options, (option: string) => { + this.hideUnitContextMenu(); + this.#executeAction(e, option); + }); + } + + } else { + if (!e.originalEvent.ctrlKey) { + getUnitsManager().selectedUnitsClearDestinations(); + } + getUnitsManager().selectedUnitsAddDestination(this.#computeDestinationRotation && this.#destinationRotationCenter != null ? this.#destinationRotationCenter : e.latlng, e.originalEvent.shiftKey, this.#destinationGroupRotation) + this.#destinationGroupRotation = 0; + this.#destinationRotationCenter = null; + this.#computeDestinationRotation = false; } - getUnitsManager().selectedUnitsAddDestination(this.#computeDestinationRotation && this.#destinationRotationCenter != null ? this.#destinationRotationCenter : e.latlng, e.originalEvent.shiftKey, this.#destinationGroupRotation) - this.#destinationGroupRotation = 0; - this.#destinationRotationCenter = null; - this.#computeDestinationRotation = false; } } + #executeAction(e: any, action: string) { + if (action === "bomb") + getUnitsManager().selectedUnitsBombPoint(this.getMouseCoordinates()); + else if (action === "carpet-bomb") + getUnitsManager().selectedUnitsCarpetBomb(this.getMouseCoordinates()); + else if (action === "building-bomb") + getUnitsManager().selectedUnitsBombBuilding(this.getMouseCoordinates()); + else if (action === "fire-at-area") + getUnitsManager().selectedUnitsFireAtArea(this.getMouseCoordinates()); + } + #onSelectionEnd(e: any) { clearTimeout(this.#leftClickTimer); this.#preventLeftClick = true; @@ -404,7 +439,7 @@ export class Map extends L.Map { #onMouseDown(e: any) { this.hideAllContextMenus(); - if (this.#state == MOVE_UNIT) { + if (this.#state == UNIT_SELECTED) { this.#destinationGroupRotation = 0; this.#destinationRotationCenter = null; this.#computeDestinationRotation = false; diff --git a/client/src/server/server.ts b/client/src/server/server.ts index 7ee189b1..f4a0000f 100644 --- a/client/src/server/server.ts +++ b/client/src/server/server.ts @@ -1,4 +1,4 @@ -import * as L from 'leaflet' +import { LatLng } from 'leaflet'; import { getConnectionStatusPanel, getInfoPopup, getMissionData, getUnitDataTable, getUnitsManager, setConnectionStatus } from '..'; import { SpawnOptions } from '../controls/mapcontextmenu'; @@ -121,12 +121,18 @@ export function addDestination(ID: number, path: any) { POST(data, () => { }); } -export function spawnSmoke(color: string, latlng: L.LatLng) { +export function spawnSmoke(color: string, latlng: LatLng) { var command = { "color": color, "location": latlng }; var data = { "smoke": command } POST(data, () => { }); } +export function spawnExplosion(strength: number, latlng: LatLng) { + var command = { "strength": strength, "location": latlng }; + var data = { "explosion": command } + POST(data, () => { }); +} + export function spawnGroundUnit(spawnOptions: SpawnOptions) { var command = { "type": spawnOptions.type, "location": spawnOptions.latlng, "coalition": spawnOptions.coalition }; var data = { "spawnGround": command } @@ -155,7 +161,7 @@ export function followUnit(ID: number, targetID: number, offset: { "x": number, POST(data, () => { }); } -export function cloneUnit(ID: number, latlng: L.LatLng) { +export function cloneUnit(ID: number, latlng: LatLng) { var command = { "ID": ID, "location": latlng }; var data = { "cloneUnit": command } POST(data, () => { }); @@ -167,7 +173,7 @@ export function deleteUnit(ID: number, explosion: boolean) { POST(data, () => { }); } -export function landAt(ID: number, latlng: L.LatLng) { +export function landAt(ID: number, latlng: LatLng) { var command = { "ID": ID, "location": latlng }; var data = { "landAt": command } POST(data, () => { }); @@ -251,6 +257,29 @@ export function refuel(ID: number) { POST(data, () => { }); } +export function bombPoint(ID: number, latlng: LatLng) { + var command = { "ID": ID, "location": latlng } + var data = { "bombPoint": command } + POST(data, () => { }); +} + +export function carpetBomb(ID: number, latlng: LatLng) { + var command = { "ID": ID, "location": latlng } + var data = { "carpetBomb": command } + POST(data, () => { }); +} + +export function bombBuilding(ID: number, latlng: LatLng) { + var command = { "ID": ID, "location": latlng } + var data = { "bombBuilding": command } + POST(data, () => { }); +} + +export function fireAtArea(ID: number, latlng: LatLng) { + var command = { "ID": ID, "location": latlng } + var data = { "fireAtArea": command } + POST(data, () => { }); +} export function setAdvacedOptions(ID: number, isTanker: boolean, isAWACS: boolean, TACAN: TACAN, radio: Radio, generalSettings: GeneralSettings) { var command = { "ID": ID, diff --git a/client/src/units/unit.ts b/client/src/units/unit.ts index 2e8bf910..5393c1ad 100644 --- a/client/src/units/unit.ts +++ b/client/src/units/unit.ts @@ -1,7 +1,7 @@ import { Marker, LatLng, Polyline, Icon, DivIcon, CircleMarker, Map } from 'leaflet'; import { getMap, getUnitsManager } from '..'; import { rad2deg } from '../other/utils'; -import { addDestination, attackUnit, changeAltitude, changeSpeed, createFormation as setLeader, deleteUnit, getUnits, landAt, setAltitude, setReactionToThreat, setROE, setSpeed, refuel, setAdvacedOptions, followUnit, setEmissionsCountermeasures, setSpeedType, setAltitudeType, setOnOff, setFollowRoads } from '../server/server'; +import { addDestination, attackUnit, changeAltitude, changeSpeed, createFormation as setLeader, deleteUnit, getUnits, landAt, setAltitude, setReactionToThreat, setROE, setSpeed, refuel, setAdvacedOptions, followUnit, setEmissionsCountermeasures, setSpeedType, setAltitudeType, setOnOff, setFollowRoads, bombPoint, carpetBomb, bombBuilding, fireAtArea } from '../server/server'; import { aircraftDatabase } from './aircraftdatabase'; import { groundUnitsDatabase } from './groundunitsdatabase'; import { CustomMarker } from '../map/custommarker'; @@ -517,6 +517,22 @@ export class Unit extends CustomMarker { setAdvacedOptions(this.ID, isTanker, isAWACS, TACAN, radio, generalSettings); } + bombPoint(latlng: LatLng) { + bombPoint(this.ID, latlng); + } + + carpetBomb(latlng: LatLng) { + carpetBomb(this.ID, latlng); + } + + bombBuilding(latlng: LatLng) { + bombBuilding(this.ID, latlng); + } + + fireAtArea(latlng: LatLng) { + fireAtArea(this.ID, latlng); + } + /***********************************************/ onAdd(map: Map): this { super.onAdd(map); @@ -557,7 +573,7 @@ export class Unit extends CustomMarker { } else if ((getUnitsManager().getSelectedUnits().length > 0 && (getUnitsManager().getSelectedUnits().includes(this))) || getUnitsManager().getSelectedUnits().length == 0) { if (this.getBaseData().category == "Aircraft") { - options["refuel"] = {text: "AAR Refuel", tooltip: "Refuel unit at the nearest AAR Tanker. If no tanker is available the unit will RTB."}; // TODO Add some way of knowing which aircraft can AAR + options["refuel"] = {text: "Air to air refuel", tooltip: "Refuel unit at the nearest AAR Tanker. If no tanker is available the unit will RTB."}; // TODO Add some way of knowing which aircraft can AAR } } diff --git a/client/src/units/unitsmanager.ts b/client/src/units/unitsmanager.ts index 540ea7ef..82512468 100644 --- a/client/src/units/unitsmanager.ts +++ b/client/src/units/unitsmanager.ts @@ -2,7 +2,7 @@ import { LatLng, LatLngBounds } from "leaflet"; import { getHotgroupPanel, getInfoPopup, getMap, getUnitDataTable } from ".."; import { Unit } from "./unit"; import { cloneUnit } from "../server/server"; -import { IDLE, MOVE_UNIT } from "../map/map"; +import { IDLE, UNIT_SELECTED } from "../map/map"; import { deg2rad, keyEventWasInInput, latLngToMercator, mercatorToLatLng } from "../other/utils"; export class UnitsManager { @@ -305,7 +305,7 @@ export class UnitsManager { for (let idx in selectedUnits) { selectedUnits[idx].setOnOff(onOff); } - this.#showActionMessage(selectedUnits, `unit acitve set to ${onOff}`); + this.#showActionMessage(selectedUnits, `unit active set to ${onOff}`); } selectedUnitsSetFollowRoads(followRoads: boolean) { @@ -437,6 +437,38 @@ export class UnitsManager { return unitDestinations; } + selectedUnitsBombPoint(mouseCoordinates: LatLng) { + var selectedUnits = this.getSelectedUnits({ excludeHumans: true }); + for (let idx in selectedUnits) { + selectedUnits[idx].bombPoint(mouseCoordinates); + } + this.#showActionMessage(selectedUnits, `unit bombing point`); + } + + selectedUnitsCarpetBomb(mouseCoordinates: LatLng) { + var selectedUnits = this.getSelectedUnits({ excludeHumans: true }); + for (let idx in selectedUnits) { + selectedUnits[idx].carpetBomb(mouseCoordinates); + } + this.#showActionMessage(selectedUnits, `unit bombing point`); + } + + selectedUnitsBombBuilding(mouseCoordinates: LatLng) { + var selectedUnits = this.getSelectedUnits({ excludeHumans: true }); + for (let idx in selectedUnits) { + selectedUnits[idx].bombBuilding(mouseCoordinates); + } + this.#showActionMessage(selectedUnits, `unit bombing point`); + } + + selectedUnitsFireAtArea(mouseCoordinates: LatLng) { + var selectedUnits = this.getSelectedUnits({ excludeHumans: true }); + for (let idx in selectedUnits) { + selectedUnits[idx].fireAtArea(mouseCoordinates); + } + this.#showActionMessage(selectedUnits, `unit bombing point`); + } + /***********************************************/ copyUnits() { this.#copiedUnits = this.getSelectedUnits(); /* Can be applied to humans too */ @@ -459,7 +491,7 @@ export class UnitsManager { /***********************************************/ #onUnitSelection(unit: Unit) { if (this.getSelectedUnits().length > 0) { - getMap().setState(MOVE_UNIT); + getMap().setState(UNIT_SELECTED); /* Disable the firing of the selection event for a certain amount of time. This avoids firing many events if many units are selected */ if (!this.#selectionEventDisabled) { window.setTimeout(() => { diff --git a/client/views/other/contextmenus.ejs b/client/views/other/contextmenus.ejs index 422177f4..77a6006b 100644 --- a/client/views/other/contextmenus.ejs +++ b/client/views/other/contextmenus.ejs @@ -8,6 +8,8 @@ data-on-click-params='{ "type": "ground-unit" }' class="unit-spawn-button"> +