diff --git a/frontend/react/src/constants/constants.ts b/frontend/react/src/constants/constants.ts index 1fdda607..851ca79a 100644 --- a/frontend/react/src/constants/constants.ts +++ b/frontend/react/src/constants/constants.ts @@ -178,6 +178,7 @@ export const defaultMapLayers = { export const IDLE = "Idle"; export const MOVE_UNIT = "Move unit"; export const SPAWN_UNIT = "Spawn unit"; +export const CONTEXT_ACTION = "Context action"; export const COALITIONAREA_DRAW_POLYGON = "Draw Coalition Area"; export const IADSTypes = ["AAA", "SAM Site", "Radar (EWR)"]; diff --git a/frontend/react/src/map/map.ts b/frontend/react/src/map/map.ts index a6043272..1fd8e940 100644 --- a/frontend/react/src/map/map.ts +++ b/frontend/react/src/map/map.ts @@ -11,7 +11,7 @@ import { bearing, /*createCheckboxOption, createSliderInputOption, createTextInp import { DestinationPreviewMarker } from "./markers/destinationpreviewmarker"; import { TemporaryUnitMarker } from "./markers/temporaryunitmarker"; import { ClickableMiniMap } from "./clickableminimap"; -import { mapMirrors, defaultMapLayers, 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,*/ DCS_LINK_PORT, DCS_LINK_RATIO, MAP_OPTIONS_DEFAULTS, MAP_HIDDEN_TYPES_DEFAULTS, SPAWN_UNIT } from "../constants/constants"; +import { mapMirrors, defaultMapLayers, 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,*/ DCS_LINK_PORT, DCS_LINK_RATIO, MAP_OPTIONS_DEFAULTS, MAP_HIDDEN_TYPES_DEFAULTS, SPAWN_UNIT, CONTEXT_ACTION } from "../constants/constants"; import { CoalitionArea } from "./coalitionarea/coalitionarea"; //import { CoalitionAreaContextMenu } from "../contextmenus/coalitionareacontextmenu"; import { DrawingCursor } from "./coalitionarea/drawingcursor"; @@ -30,6 +30,7 @@ import './markers/stylesheets/units.css' import './theme.css' import { Coalition, MapHiddenTypes, MapOptions } from "../types/types"; import { SpawnRequestTable, UnitBlueprint, UnitSpawnTable } from "../interfaces"; +import { ContextAction } from "../unit/contextaction"; var hasTouchScreen = false; //if ("maxTouchPoints" in navigator) @@ -109,6 +110,8 @@ export class Map extends L.Map { #bradcastPositionXmlHttp: XMLHttpRequest | null = null; #cameraZoomRatio: number = 1.0; + #contextAction: null | ContextAction = null; + /** * * @param ID - the ID of the HTML element which will contain the context menu @@ -305,7 +308,7 @@ export class Map extends L.Map { } /* State machine */ - setState(state: string, options?: { spawnRequestTable: SpawnRequestTable }) { + setState(state: string, options?: { spawnRequestTable?: SpawnRequestTable, contextAction?: ContextAction }) { this.#state = state; /* Operations to perform if you are NOT in a state */ @@ -319,6 +322,9 @@ export class Map extends L.Map { this.#spawnCursor?.removeFrom(this); this.#spawnCursor = new TemporaryUnitMarker(new L.LatLng(0, 0), this.#spawnRequestTable?.unit.unitType ?? "unknown", this.#spawnRequestTable?.coalition ?? 'blue'); } + else if (this.#state === CONTEXT_ACTION ) { + this.#contextAction = options?.contextAction ?? null; + } else if (this.#state === COALITIONAREA_DRAW_POLYGON) { this.#coalitionAreas.push(new CoalitionArea([])); this.#coalitionAreas[this.#coalitionAreas.length - 1].addTo(this); @@ -440,10 +446,10 @@ export class Map extends L.Map { return this.#lastMouseCoordinates; } - centerOnUnit(ID: number | null) { - if (ID != null) { + centerOnUnit(unit: Unit | null) { + if (unit !== null) { this.options.scrollWheelZoom = 'center'; - this.#centerUnit = getApp().getUnitsManager().getUnitByID(ID); + this.#centerUnit = unit; } else { this.options.scrollWheelZoom = undefined; @@ -707,6 +713,10 @@ export class Map extends L.Map { this.#computeDestinationRotation = false; } } + else if (this.#state === CONTEXT_ACTION) { + if (this.#contextAction) + this.#contextAction.executeCallback(null, e.latlng); + } else { this.setState(IDLE); } diff --git a/frontend/react/src/ui/components/oldropdown.tsx b/frontend/react/src/ui/components/oldropdown.tsx index c1593024..3416d866 100644 --- a/frontend/react/src/ui/components/oldropdown.tsx +++ b/frontend/react/src/ui/components/oldropdown.tsx @@ -49,7 +49,7 @@ export function OlDropdown(props: { if (cxr > window.innerWidth) offsetX -= (cxr - window.innerWidth) if (cyb > window.innerHeight) - offsetY = -ch - 8; + offsetY -= bh + ch + 16; /* Apply the offset */ content.style.left = `${offsetX}px` diff --git a/frontend/react/src/ui/components/olstatebutton.tsx b/frontend/react/src/ui/components/olstatebutton.tsx index 80994944..c9014d99 100644 --- a/frontend/react/src/ui/components/olstatebutton.tsx +++ b/frontend/react/src/ui/components/olstatebutton.tsx @@ -1,42 +1,61 @@ import { IconProp } from "@fortawesome/fontawesome-svg-core"; import { faExternalLink, faLock, faLockOpen, faUnlock, faUnlockAlt } from "@fortawesome/free-solid-svg-icons"; import { FontAwesomeIcon } from "@fortawesome/react-fontawesome" -import React from "react" +import React, { useRef, useState } from "react" +import { OlTooltip } from "./oltooltip"; export function OlStateButton(props: { className?: string, checked: boolean, icon: IconProp, + tooltip: string, onClick: () => void }) { + var [hover, setHover] = useState(false); + var buttonRef = useRef(null); + const className = (props.className ?? '') + ` h-[40px] w-[40px] flex-none font-medium rounded-md text-lg dark:bg-olympus-600 dark:hover:bg-olympus-300 dark:data-[checked='true']:bg-blue-500 dark:data-[checked='true']:text-white dark:text-gray-300 dark:border-gray-600 `; - return + {hover && } + } export function OlRoundStateButton(props: { className?: string, checked: boolean, icon: IconProp, + tooltip: string, onClick: () => void }) { + var [hover, setHover] = useState(false); + var buttonRef = useRef(null); + const className = (props.className ?? '') + ` h-8 w-8 flex-none m-auto border-2 border-gray-900 font-medium rounded-full text-sm dark:bg-[transparent] dark:data-[checked='true']:bg-white dark:text-gray-400 dark:data-[checked='true']:text-gray-900 dark:data-[checked='true']:border-white dark:border-gray-400 dark:data-[checked='true']:hover:bg-gray-200 dark:data-[checked='true']:hover:border-gray-200 dark:hover:bg-gray-800`; - return + { hover && } + } export function OlLockStateButton(props: { className?: string, checked: boolean, + tooltip: string, onClick: () => void }) { + var [hover, setHover] = useState(false); + var buttonRef = useRef(null); + const className = (props.className ?? '') + ` h-8 w-8 flex-none m-auto border-gray-900 font-medium rounded-full text-sm dark:bg-red-500 dark:data-[checked='true']:bg-green-500 dark:text-olympus-900 dark:data-[checked='true']:text-green-900 dark:data-[checked='true']:hover:bg-green-400 dark:hover:bg-red-400`; - return + { hover && } + } \ No newline at end of file diff --git a/frontend/react/src/ui/components/oltooltip.tsx b/frontend/react/src/ui/components/oltooltip.tsx new file mode 100644 index 00000000..180dc24f --- /dev/null +++ b/frontend/react/src/ui/components/oltooltip.tsx @@ -0,0 +1,63 @@ +import React, { useEffect, useRef, useState } from "react"; + +export function OlTooltip(props: { + content: string, + buttonRef: React.MutableRefObject +}) { + var contentRef = useRef(null); + + function setPosition(content: HTMLDivElement, button: HTMLButtonElement) { + /* Reset the position of the content */ + content.style.left = "0px"; + content.style.top = "0px"; + content.style.height = ""; + + /* Get the position and size of the button and the content elements */ + var [cxl, cyt, cxr, cyb, cw, ch] = [content.getBoundingClientRect().x, content.getBoundingClientRect().y, content.getBoundingClientRect().x + content.clientWidth, content.getBoundingClientRect().y + content.clientHeight, content.clientWidth, content.clientHeight]; + var [bxl, byt, bxr, byb, bbw, bh] = [button.getBoundingClientRect().x, button.getBoundingClientRect().y, button.getBoundingClientRect().x + button.clientWidth, button.getBoundingClientRect().y + button.clientHeight, button.clientWidth, button.clientHeight]; + + /* Limit the maximum height */ + if (ch > 400) { + ch = 400; + content.style.height = `${ch}px`; + } + + /* Compute the horizontal position of the center of the button and the content */ + var cxc = (cxl + cxr) / 2; + var bxc = (bxl + bxr) / 2; + + /* Compute the x and y offsets needed to align the button and element horizontally, and to put the content below the button */ + var offsetX = bxc - cxc; + var offsetY = byb - cyt + 8; + + /* Compute the new position of the left and right margins of the content */ + cxl += offsetX; + cxr += offsetX; + cyb += offsetY; + + /* Try and move the content so it is inside the screen */ + if (cxl < 0) + offsetX -= cxl; + if (cxr > window.innerWidth) + offsetX -= (cxr - window.innerWidth) + if (cyb > window.innerHeight) + offsetY -= bh + ch + 16; + + /* Apply the offset */ + content.style.left = `${offsetX}px` + content.style.top = `${offsetY}px` + } + + useEffect(() => { + if (contentRef.current && props.buttonRef.current) { + const content = contentRef.current as HTMLDivElement; + const button = props.buttonRef.current as HTMLButtonElement; + + setPosition(content, button); + } + }) + + return props.content !== "" &&
+ { props.content } +
+} \ No newline at end of file diff --git a/frontend/react/src/ui/panels/header.tsx b/frontend/react/src/ui/panels/header.tsx index de84266a..b9bbae73 100644 --- a/frontend/react/src/ui/panels/header.tsx +++ b/frontend/react/src/ui/panels/header.tsx @@ -24,7 +24,7 @@ export function Header() {
- {}}/> + {}} tooltip="Lock/unlock protected units (from scripted mission)"/>
{ @@ -36,7 +36,8 @@ export function Header() { getApp().getMap().setHiddenType(entry[0], !appState.mapHiddenTypes[entry[0]]); }} checked={!appState.mapHiddenTypes[entry[0]]} - icon={entry[1]} /> + icon={entry[1]} + tooltip={"Hide/show " + entry[0] + " units" } /> }) }
@@ -52,7 +53,8 @@ export function Header() { getApp().getMap().setHiddenType(entry[0], !appState.mapHiddenTypes[entry[0]]); }} checked={!appState.mapHiddenTypes[entry[0]]} - icon={entry[1]} /> + icon={entry[1]} + tooltip={"Hide/show " + entry[0] + " units" } /> }) } @@ -61,18 +63,21 @@ export function Header() { getApp().getMap().setHiddenType( 'blue', !appState.mapHiddenTypes['blue'] )} checked={!appState.mapHiddenTypes['blue']} - icon={faFlag} className={"!text-blue-500"} /> + icon={faFlag} className={"!text-blue-500"} + tooltip={"Hide/show blue units" } /> getApp().getMap().setHiddenType('red', !appState.mapHiddenTypes['red'] )} checked={!appState.mapHiddenTypes['red']} - icon={faFlag} className={"!text-red-500"} /> + icon={faFlag} className={"!text-red-500"} + tooltip={"Hide/show red units" } /> getApp().getMap().setHiddenType('neutral', !appState.mapHiddenTypes['neutral'] )} checked={!appState.mapHiddenTypes['neutral']} - icon={faFlag} className={"!text-gray-500"} /> + icon={faFlag} className={"!text-gray-500"} + tooltip={"Hide/show neutral units" } /> {}}> - {}} /> + {}} tooltip="Activate/deactivate camera plugin" /> DCS Sat DCS Alt diff --git a/frontend/react/src/ui/panels/sidebar.tsx b/frontend/react/src/ui/panels/sidebar.tsx index 8f501b05..1c60d4fc 100644 --- a/frontend/react/src/ui/panels/sidebar.tsx +++ b/frontend/react/src/ui/panels/sidebar.tsx @@ -12,17 +12,17 @@ export function SideBar() { diff --git a/frontend/react/src/ui/panels/unitmousecontrolbar.tsx b/frontend/react/src/ui/panels/unitmousecontrolbar.tsx index 7812044e..6d34bfa3 100644 --- a/frontend/react/src/ui/panels/unitmousecontrolbar.tsx +++ b/frontend/react/src/ui/panels/unitmousecontrolbar.tsx @@ -5,15 +5,19 @@ import { OlStateButton } from '../components/olstatebutton'; import { faAccessibleIcon } from '@fortawesome/free-brands-svg-icons'; import { faCamera } from '@fortawesome/free-solid-svg-icons'; import { getApp } from '../../olympusapp'; +import { ContextAction } from '../../unit/contextaction'; +import { CONTEXT_ACTION } from '../../constants/constants'; +import { FaInfoCircle, FaQuestionCircle } from 'react-icons/fa'; export function UnitMouseControlBar(props: { }) { - var [open, setOpen] = useState(false); + var [open, setOpen] = useState(false); var [selectedUnits, setSelectedUnits] = useState([] as Unit[]); var [contextActionsSet, setContextActionsSet] = useState(new ContextActionSet()); + var [activeContextAction, setActiveContextAction] = useState(null as null | ContextAction); - /* When a unit is selected, open the menu */ + /* When a unit is selected, open the menu */ document.addEventListener("unitsSelection", (ev: CustomEventInit) => { setOpen(true); setSelectedUnits(ev.detail as Unit[]); @@ -34,22 +38,43 @@ export function UnitMouseControlBar(props: { updateData(); }) - /* Update the current values of the shown data */ + /* Update the current values of the shown data */ function updateData() { var newContextActionSet = new ContextActionSet(); - getApp().getUnitsManager().getSelectedUnits().forEach((unit: Unit) => { - unit.appendContextActions(newContextActionSet); - }) + getApp().getUnitsManager().getSelectedUnits().forEach((unit: Unit) => { + unit.appendContextActions(newContextActionSet); + }) setContextActionsSet(newContextActionSet); - } + setActiveContextAction(null); + } - return
- { - Object.values(contextActionsSet.getContextActions()).map((contextAction) => { - return {}} /> - }) + return <> { + open && <> +
+ { + Object.values(contextActionsSet.getContextActions()).map((contextAction) => { + return { + if (contextAction.getOptions().executeImmediately) { + setActiveContextAction(null); + contextAction.executeCallback(null, null); + } else { + setActiveContextAction(contextAction); + getApp().getMap().setState(CONTEXT_ACTION, { contextAction: contextAction }); + } + }} /> + }) + } +
+ { activeContextAction &&
+ +
+ {activeContextAction.getDescription()} +
+
} -
+ + } + } \ No newline at end of file diff --git a/frontend/react/src/unit/contextaction.ts b/frontend/react/src/unit/contextaction.ts index a6087d9c..61bb4337 100644 --- a/frontend/react/src/unit/contextaction.ts +++ b/frontend/react/src/unit/contextaction.ts @@ -1,29 +1,30 @@ import { IconDefinition } from "@fortawesome/fontawesome-svg-core"; import { Unit } from "./unit"; +import { LatLng } from "leaflet"; export interface ContextActionOptions { - isScenic?: boolean + executeImmediately?: boolean } +export type ContextActionCallback = (units: Unit[], targetUnit: Unit | null, targetPosition: LatLng | null) => void; + export class ContextAction { #id: string = ""; #label: string = ""; #description: string = ""; - #callback: CallableFunction | null = null; + #callback: ContextActionCallback | null = null; #units: Unit[] = []; - #hideContextAfterExecution: boolean = true #icon: IconDefinition; #options: ContextActionOptions; - constructor(id: string, label: string, description: string, icon: IconDefinition, callback: CallableFunction, hideContextAfterExecution: boolean = true, options: ContextActionOptions) { + constructor(id: string, label: string, description: string, icon: IconDefinition, callback: ContextActionCallback, options: ContextActionOptions) { this.#id = id; this.#label = label; this.#description = description; this.#callback = callback; this.#icon = icon; - this.#hideContextAfterExecution = hideContextAfterExecution; this.#options = { - "isScenic": false, + executeImmediately: false, ...options } } @@ -56,12 +57,8 @@ export class ContextAction { return this.#icon; } - executeCallback() { + executeCallback(targetUnit: Unit | null, targetPosition: LatLng | null) { if (this.#callback) - this.#callback(this.#units); - } - - getHideContextAfterExecution() { - return this.#hideContextAfterExecution; + this.#callback(this.#units, targetUnit, targetPosition); } } diff --git a/frontend/react/src/unit/contextactionset.ts b/frontend/react/src/unit/contextactionset.ts index 833ec3a3..65e7da59 100644 --- a/frontend/react/src/unit/contextactionset.ts +++ b/frontend/react/src/unit/contextactionset.ts @@ -1,16 +1,15 @@ -import { LatLng } from "leaflet"; -import { ContextAction, ContextActionOptions } from "./contextaction"; +import { ContextAction, ContextActionCallback, ContextActionOptions } from "./contextaction"; import { Unit } from "./unit"; import { IconDefinition } from "@fortawesome/fontawesome-svg-core"; export class ContextActionSet { - #contextActions: {[key: string]: ContextAction} = {}; + #contextActions: { [key: string]: ContextAction } = {}; - addContextAction(unit: Unit, id: string, label: string, description: string, icon: IconDefinition, callback: (units: Unit[], targetUnit: Unit, targetPosition: LatLng) => void, hideContextAfterExecution: boolean = true, options?:ContextActionOptions) { + addContextAction(unit: Unit, id: string, label: string, description: string, icon: IconDefinition, callback: ContextActionCallback, options?: ContextActionOptions) { options = options || {}; if (!(id in this.#contextActions)) { - this.#contextActions[id] = new ContextAction(id, label, description, icon, callback, hideContextAfterExecution, options); + this.#contextActions[id] = new ContextAction(id, label, description, icon, callback, options); } this.#contextActions[id].addUnit(unit); } diff --git a/frontend/react/src/unit/unit.ts b/frontend/react/src/unit/unit.ts index 2113e6da..7baa955b 100644 --- a/frontend/react/src/unit/unit.ts +++ b/frontend/react/src/unit/unit.ts @@ -1000,6 +1000,7 @@ export abstract class Unit extends CustomMarker { showFollowOptions(units: Unit[]) { var contextActionSet = new ContextActionSet(); + // TODO FIX contextActionSet.addContextAction(this, 'trail', "Trail", "Follow unit in trail formation", olIconsTrail, () => this.applyFollowOptions('trail', units)); contextActionSet.addContextAction(this, 'echelon-lh', "Echelon (LH)", "Follow unit in echelon left formation", olIconsEchelonLh, () => this.applyFollowOptions('echelon-lh', units)); contextActionSet.addContextAction(this, 'echelon-rh', "Echelon (RH)", "Follow unit in echelon right formation", olIconsEchelonRh, () => this.applyFollowOptions('echelon-rh', units)); @@ -1111,11 +1112,11 @@ export abstract class Unit extends CustomMarker { if (this.#miniMapMarker == null) { this.#miniMapMarker = new CircleMarker(new LatLng(this.#position.lat, this.#position.lng), { radius: 0.5 }); if (this.#coalition == "neutral") - this.#miniMapMarker.setStyle({ color: "#CFD9E8" }); + this.#miniMapMarker.setStyle({ color: "#CFD9E8", radius: 2 }); else if (this.#coalition == "red") - this.#miniMapMarker.setStyle({ color: "#ff5858" }); + this.#miniMapMarker.setStyle({ color: "#ff5858", radius: 2 }); else - this.#miniMapMarker.setStyle({ color: "#247be2" }); + this.#miniMapMarker.setStyle({ color: "#247be2", radius: 2 }); this.#miniMapMarker.addTo(getApp().getMap().getMiniMapLayerGroup()); this.#miniMapMarker.bringToBack(); } @@ -1498,12 +1499,33 @@ export abstract class AirUnit extends Unit { } appendContextActions(contextActionSet: ContextActionSet) { - contextActionSet.addContextAction(this, "attack", "Attack unit", "Attack the unit using A/A or A/G weapons", olStatesAttack, (units: Unit[], targetUnit: Unit, targetPosition: LatLng) => { getApp().getUnitsManager().attackUnit(targetUnit.ID, units) }); - contextActionSet.addContextAction(this, "follow", "Follow unit", "Follow this unit in formation", olIconsFollow, (units: Unit[], targetUnit: Unit, targetPosition: LatLng) => { targetUnit.showFollowOptions(units); }, false); // Don't hide the context menu after the execution (to show the follow options) - contextActionSet.addContextAction(this, "refuel", "Refuel", "Refuel units at the nearest AAR Tanker. If no tanker is available the unit will RTB", olStatesRefuel, (units: Unit[]) => { getApp().getUnitsManager().refuel(units) }); - contextActionSet.addContextAction(this, "center-map", "Center map", "Center the map on the unit and follow it", faQuestionCircle, () => { getApp().getMap().centerOnUnit(this.ID); }); - contextActionSet.addContextAction(this, "bomb", "Precision bombing", "Precision bombing of a specific point", faQuestionCircle, (units: Unit[], targetUnit: Unit, targetPosition: LatLng) => { getApp().getUnitsManager().bombPoint(targetPosition, units) }); - contextActionSet.addContextAction(this, "carpet-bomb", "Carpet bombing", "Carpet bombing close to a point", faQuestionCircle, (units: Unit[], targetUnit: Unit, targetPosition: LatLng) => { getApp().getUnitsManager().carpetBomb(targetPosition, units) }); + /* Context actions to be executed immediately */ + contextActionSet.addContextAction(this, "refuel", "Refuel", "Refuel units at the nearest AAR Tanker. If no tanker is available the unit will RTB", olStatesRefuel, (units: Unit[]) => { + getApp().getUnitsManager().refuel(units) + }, { executeImmediately: true }); + contextActionSet.addContextAction(this, "center-map", "Center map", "Center the map on the unit and follow it", faQuestionCircle, (units: Unit[]) => { + getApp().getMap().centerOnUnit(units[0]); + }, { executeImmediately: true }); + + /* Context actions with a target unit */ + contextActionSet.addContextAction(this, "attack", "Attack unit", "Click on a unit to attack it using A/A or A/G weapons", olStatesAttack, (units: Unit[], targetUnit: Unit | null, targetPosition: LatLng | null) => { + if (targetUnit) + getApp().getUnitsManager().attackUnit(targetUnit.ID, units) + }); + contextActionSet.addContextAction(this, "follow", "Follow unit", "Click on a unit to follow it in formation", olIconsFollow, (units: Unit[], targetUnit: Unit | null, targetPosition: LatLng | null) => { + if (targetUnit) + targetUnit.showFollowOptions(units); + }); + + /* Context actions with a target position */ + contextActionSet.addContextAction(this, "bomb", "Precision bombing", "Click on a point to execute a precision bombing attack", faQuestionCircle, (units: Unit[], targetUnit: Unit | null, targetPosition: LatLng | null) => { + if (targetPosition) + getApp().getUnitsManager().bombPoint(targetPosition , units) + }); + contextActionSet.addContextAction(this, "carpet-bomb", "Carpet bombing", "Click on a point to execute a carpet bombing attack", faQuestionCircle, (units: Unit[], targetUnit: Unit | null, targetPosition: LatLng | null) => { + if (targetPosition) + getApp().getUnitsManager().carpetBomb(targetPosition , units) + }); } } @@ -1540,7 +1562,10 @@ export class Helicopter extends AirUnit { appendContextActions(contextActionSet: ContextActionSet) { super.appendContextActions(contextActionSet); - contextActionSet.addContextAction(this, "land-at-point", "Land here", "Land at this precise location", olIconsLandAtPoint, (units: Unit[], targetUnit: Unit, targetPosition: LatLng) => { getApp().getUnitsManager().landAtPoint(targetPosition, units) }); + contextActionSet.addContextAction(this, "land-at-point", "Land here", "Click on a point to land there", olIconsLandAtPoint, (units: Unit[], targetUnit: Unit | null, targetPosition: LatLng | null) => { + if (targetPosition) + getApp().getUnitsManager().landAtPoint(targetPosition , units) + }); } getMarkerCategory() { @@ -1575,21 +1600,38 @@ export class GroundUnit extends Unit { } appendContextActions(contextActionSet: ContextActionSet) { - contextActionSet.addContextAction(this, "group-ground", "Group ground units", "Create a group of ground units", olIconsGroupGround, (units: Unit[], targetUnit: Unit, targetPosition: LatLng) => { getApp().getUnitsManager().createGroup(units) }); - contextActionSet.addContextAction(this, "attack", "Attack unit", "Attack the unit using A/A or A/G weapons", faQuestionCircle, (units: Unit[], targetUnit: Unit, targetPosition: LatLng) => { getApp().getUnitsManager().attackUnit(targetUnit.ID, units) }); - contextActionSet.addContextAction(this, "center-map", "Center map", "Center the map on the unit and follow it", faQuestionCircle, () => { getApp().getMap().centerOnUnit(this.ID); }); - - if (this.canTargetPoint()) { - contextActionSet.addContextAction(this, "fire-at-area", "Fire at area", "Fire at a specific area on the ground", faQuestionCircle, (units: Unit[], targetUnit: Unit, targetPosition: LatLng) => { getApp().getUnitsManager().fireAtArea(targetPosition, units) }); - contextActionSet.addContextAction(this, "simulate-fire-fight", "Simulate fire fight", "Simulate a fire fight by shooting randomly in a certain large area.\nWARNING: works correctly only on neutral units, blue or red units will aim", faQuestionCircle, (units: Unit[], targetUnit: Unit, targetPosition: LatLng) => { getApp().getUnitsManager().simulateFireFight(targetPosition, units) }); - } + /* Context actions to be executed immediately */ + contextActionSet.addContextAction(this, "group-ground", "Group ground units", "Create a group of ground units", olIconsGroupGround, (units: Unit[], targetUnit: Unit | null, targetPosition: LatLng | null) => { + getApp().getUnitsManager().createGroup(units) + }, { executeImmediately: true }); + contextActionSet.addContextAction(this, "center-map", "Center map", "Center the map on the unit and follow it", faQuestionCircle, (units: Unit[]) => { + getApp().getMap().centerOnUnit(units[0]); + }, { executeImmediately: true }); if (this.canAAA()) { - contextActionSet.addContextAction(this, "scenic-aaa", "Scenic AAA", "Shoot AAA in the air without aiming at any target, when an enemy unit gets close enough.\nWARNING: works correctly only on neutral units, blue or red units will aim", faQuestionCircle, (units: Unit[]) => { getApp().getUnitsManager().scenicAAA(units) }, undefined, { - "isScenic": true + contextActionSet.addContextAction(this, "scenic-aaa", "Scenic AAA", "Shoot AAA in the air without aiming at any target, when an enemy unit gets close enough. WARNING: works correctly only on neutral units, blue or red units will aim", faQuestionCircle, (units: Unit[]) => { + getApp().getUnitsManager().scenicAAA(units) + }, { executeImmediately: true }); + contextActionSet.addContextAction(this, "miss-aaa", "Dynamic accuracy AAA", "Shoot AAA towards the closest enemy unit, but don't aim precisely. WARNING: works correctly only on neutral units, blue or red units will aim", faQuestionCircle, (units: Unit[]) => { + getApp().getUnitsManager().missOnPurpose(units) + }, { executeImmediately: true }); + } + + /* Context actions that require a target unit */ + contextActionSet.addContextAction(this, "attack", "Attack unit", "Click on a unit to attack it", faQuestionCircle, (units: Unit[], targetUnit: Unit | null, targetPosition: LatLng | null) => { + if (targetUnit) + getApp().getUnitsManager().attackUnit(targetUnit.ID, units) + }); + + /* Context actions that require a target position */ + if (this.canTargetPoint()) { + contextActionSet.addContextAction(this, "fire-at-area", "Fire at area", "Click on a point to precisely fire at it (if possible)", faQuestionCircle, (units: Unit[], targetUnit: Unit | null, targetPosition: LatLng | null) => { + if (targetPosition) + getApp().getUnitsManager().fireAtArea(targetPosition , units) }); - contextActionSet.addContextAction(this, "miss-aaa", "Dynamic accuracy AAA", "Shoot AAA towards the closest enemy unit, but don't aim precisely.\nWARNING: works correctly only on neutral units, blue or red units will aim", faQuestionCircle, (units: Unit[]) => { getApp().getUnitsManager().missOnPurpose(units) }, undefined, { - "isScenic": true + contextActionSet.addContextAction(this, "simulate-fire-fight", "Simulate fire fight", "Simulate a fire fight by shooting randomly in a certain large area. WARNING: works correctly only on neutral units, blue or red units will aim", faQuestionCircle, (units: Unit[], targetUnit: Unit | null, targetPosition: LatLng | null) => { + if (targetPosition) + getApp().getUnitsManager().simulateFireFight(targetPosition , units) }); } } @@ -1663,10 +1705,25 @@ export class NavyUnit extends Unit { } appendContextActions(contextActionSet: ContextActionSet) { - contextActionSet.addContextAction(this, "group-navy", "Group navy units", "Create a group of navy units", faQuestionCircle, (units: Unit[], targetUnit: Unit, targetPosition: LatLng) => { getApp().getUnitsManager().createGroup(units) }); - contextActionSet.addContextAction(this, "attack", "Attack unit", "Attack the unit using A/A or A/G weapons", faQuestionCircle, (units: Unit[], targetUnit: Unit, targetPosition: LatLng) => { getApp().getUnitsManager().attackUnit(targetUnit.ID, units) }); - contextActionSet.addContextAction(this, "center-map", "Center map", "Center the map on the unit and follow it", faQuestionCircle, () => { getApp().getMap().centerOnUnit(this.ID); }); - contextActionSet.addContextAction(this, "fire-at-area", "Fire at area", "Fire at a specific area on the ground", faQuestionCircle, (units: Unit[], targetUnit: Unit, targetPosition: LatLng) => { getApp().getUnitsManager().fireAtArea(targetPosition, units) }); + /* Context actions to be executed immediately */ + contextActionSet.addContextAction(this, "group-navy", "Group navy units", "Create a group of navy units", faQuestionCircle, (units: Unit[], targetUnit: Unit | null, targetPosition: LatLng | null) => { + getApp().getUnitsManager().createGroup(units) + }, { executeImmediately: true }); + contextActionSet.addContextAction(this, "center-map", "Center map", "Center the map on the unit and follow it", faQuestionCircle, (units: Unit[]) => { + getApp().getMap().centerOnUnit(units[0]); + }, { executeImmediately: true }); + + /* Context actions that require a target unit */ + contextActionSet.addContextAction(this, "attack", "Attack unit", "Click on a unit to attack it", faQuestionCircle, (units: Unit[], targetUnit: Unit | null, targetPosition: LatLng | null) => { + if (targetUnit) + getApp().getUnitsManager().attackUnit(targetUnit.ID, units) + }); + + /* Context actions that require a target position */ + contextActionSet.addContextAction(this, "fire-at-area", "Fire at area", "Click on a point to precisely fire at it (if possible)", faQuestionCircle, (units: Unit[], targetUnit: Unit | null, targetPosition: LatLng | null) => { + if (targetPosition) + getApp().getUnitsManager().fireAtArea(targetPosition , units) + }); } getCategory() { diff --git a/frontend/server/demo.js b/frontend/server/demo.js index 4a51bc1f..73778165 100644 --- a/frontend/server/demo.js +++ b/frontend/server/demo.js @@ -462,7 +462,7 @@ module.exports = function (configLocation) { }; mission(req, res){ - var ret = {mission: {theatre: "PersianGulf"}}; + var ret = {mission: {theatre: "Nevada"}}; ret.time = Date.now(); ret.mission.dateAndTime = {