diff --git a/frontend/react/src/constants/constants.ts b/frontend/react/src/constants/constants.ts index 268ff63f..dac06c3a 100644 --- a/frontend/react/src/constants/constants.ts +++ b/frontend/react/src/constants/constants.ts @@ -308,6 +308,7 @@ export const MAP_OPTIONS_DEFAULTS = { fillSelectedRing: false, showMinimap: false, protectDCSUnits: true, + keepRelativePositions: true } as MapOptions; export const MAP_HIDDEN_TYPES_DEFAULTS = { @@ -399,8 +400,7 @@ export enum AudioMessageType { settings, } -export const CONTEXT_ACTION_COLORS = [null, "white", "green", "purple", "blue", "red"]; -export enum ContextActionColors { +export enum ContextActionType { NO_COLOR, MOVE, OTHER, @@ -408,3 +408,6 @@ export enum ContextActionColors { ENGAGE, DELETE, } +export const CONTEXT_ACTION_COLORS = [null, "white", "green", "purple", "blue", "red"]; + + diff --git a/frontend/react/src/events.ts b/frontend/react/src/events.ts index f2b06fe4..4bfe441f 100644 --- a/frontend/react/src/events.ts +++ b/frontend/react/src/events.ts @@ -80,15 +80,17 @@ export class ServerStatusUpdatedEvent { } } -export class UnitDatabaseLoadedEvent { - static on(callback: () => void) { +export class UnitDatabaseLoadedEvent extends BaseOlympusEvent {} + +export class InfoPopupEvent { + static on(callback: (messages: string[]) => void) { document.addEventListener(this.name, (ev: CustomEventInit) => { - callback(); + callback(ev.detail.messages); }); } - static dispatch() { - document.dispatchEvent(new CustomEvent(this.name)); + static dispatch(messages: string[]) { + document.dispatchEvent(new CustomEvent(this.name, {detail: {messages}})); console.log(`Event ${this.name} dispatched`); } } @@ -223,6 +225,58 @@ export class SelectedUnitsChangedEvent { } } +export class UnitExplosionRequestEvent { + static on(callback: (units: Unit[]) => void) { + document.addEventListener(this.name, (ev: CustomEventInit) => { + callback(ev.detail.units); + }); + } + + static dispatch(units: Unit[]) { + document.dispatchEvent(new CustomEvent(this.name, {detail: {units}})); + console.log(`Event ${this.name} dispatched`); + } +} + +export class FormationCreationRequestEvent { + static on(callback: (leader: Unit, wingmen: Unit[]) => void) { + document.addEventListener(this.name, (ev: CustomEventInit) => { + callback(ev.detail.leader, ev.detail.wingmen); + }); + } + + static dispatch(leader: Unit, wingmen: Unit[]) { + document.dispatchEvent(new CustomEvent(this.name, {detail: {leader, wingmen}})); + console.log(`Event ${this.name} dispatched`); + } +} + +export class MapContextMenuRequestEvent { + static on(callback: (latlng: L.LatLng) => void) { + document.addEventListener(this.name, (ev: CustomEventInit) => { + callback(ev.detail.latlng); + }); + } + + static dispatch(latlng: L.LatLng) { + document.dispatchEvent(new CustomEvent(this.name, {detail: {latlng}})); + console.log(`Event ${this.name} dispatched`); + } +} + +export class UnitContextMenuRequestEvent { + static on(callback: (unit: Unit) => void) { + document.addEventListener(this.name, (ev: CustomEventInit) => { + callback(ev.detail.unit); + }); + } + + static dispatch(unit: Unit) { + document.dispatchEvent(new CustomEvent(this.name, {detail: {unit}})); + console.log(`Event ${this.name} dispatched`); + } +} + /************** Command mode events ***************/ export class CommandModeOptionsChangedEvent { static on(callback: (options: CommandModeOptions) => void) { diff --git a/frontend/react/src/map/map.ts b/frontend/react/src/map/map.ts index 78bd7ffc..628594f8 100644 --- a/frontend/react/src/map/map.ts +++ b/frontend/react/src/map/map.ts @@ -45,8 +45,11 @@ import { ContextActionChangedEvent, ContextActionSetChangedEvent, HiddenTypesChangedEvent, + MapContextMenuRequestEvent, MapOptionsChangedEvent, MapSourceChangedEvent, + SelectionClearedEvent, + UnitSelectedEvent, UnitUpdatedEvent, } from "../events"; import { ContextActionSet } from "../unit/contextactionset"; @@ -116,6 +119,10 @@ export class Map extends L.Map { /* Coalition areas drawing */ #coalitionAreas: (CoalitionPolygon | CoalitionCircle)[] = []; + /* Units movement */ + #destinationPreviewMarkers: { [key: number]: TemporaryUnitMarker | TargetMarker } = {}; + #destinationRotation: number = 0; + /* Unit context actions */ #contextActionSet: null | ContextActionSet = null; #contextAction: null | ContextAction = null; @@ -190,6 +197,7 @@ export class Map extends L.Map { /* Custom touch events for touchscreen support */ L.DomEvent.on(this.getContainer(), "touchstart", this.#onMouseDown, this); L.DomEvent.on(this.getContainer(), "touchend", this.#onMouseUp, this); + L.DomEvent.on(this.getContainer(), "wheel", this.#onWheel, this); /* Event listeners */ AppStateChangedEvent.on((state, subState) => this.#onStateChanged(state, subState)); @@ -204,6 +212,7 @@ export class Map extends L.Map { UnitUpdatedEvent.on((unit) => { if (this.#centeredUnit != null && unit == this.#centeredUnit) this.#panToUnit(this.#centeredUnit); + if (unit.getSelected()) this.#moveDestinationPreviewMarkers(); }); MapOptionsChangedEvent.on((options) => { @@ -255,6 +264,11 @@ export class Map extends L.Map { } }); + UnitSelectedEvent.on((unit) => this.#updateDestinationPreviewMarkers()); + SelectionClearedEvent.on(() => this.#updateDestinationPreviewMarkers()); + ContextActionChangedEvent.on((contextAction) => this.#updateDestinationPreviewMarkers()); + MapOptionsChangedEvent.on((mapOptions) => this.#moveDestinationPreviewMarkers()); + /* Pan interval */ this.#panInterval = window.setInterval(() => { if (this.#panUp || this.#panDown || this.#panRight || this.#panLeft) @@ -724,6 +738,18 @@ export class Map extends L.Map { return marker; } + addExplosionMarker(latlng: L.LatLng) { + const explosionMarker = new ExplosionMarker(latlng, 5); + explosionMarker.addTo(this); + return explosionMarker; + } + + addSmokeMarker(latlng: L.LatLng, color: string) { + const smokeMarker = new SmokeMarker(latlng, color); + smokeMarker.addTo(this); + return smokeMarker; + } + setOption(key, value) { this.#options[key] = value; MapOptionsChangedEvent.dispatch(this.#options); @@ -781,8 +807,8 @@ export class Map extends L.Map { //} } - executeContextAction(targetUnit: Unit | null, targetPosition: L.LatLng | null) { - this.#contextAction?.executeCallback(targetUnit, targetPosition); + executeContextAction(targetUnit: Unit | null, targetPosition: L.LatLng | null, originalEvent?: MouseEvent) { + this.#contextAction?.executeCallback(targetUnit, targetPosition, originalEvent); } getContextActionSet() { @@ -793,8 +819,8 @@ export class Map extends L.Map { return this.#contextAction; } - executeDefaultContextAction(targetUnit: Unit | null, targetPosition: L.LatLng | null) { - this.#contextActionSet?.getDefaultContextAction()?.executeCallback(targetUnit, targetPosition); + executeDefaultContextAction(targetUnit: Unit | null, targetPosition: L.LatLng | null, originalEvent?: MouseEvent) { + this.#contextActionSet?.getDefaultContextAction()?.executeCallback(targetUnit, targetPosition, originalEvent); } preventClicks() { @@ -831,10 +857,9 @@ export class Map extends L.Map { } else if (subState === SpawnSubState.SPAWN_EFFECT) { console.log(`Effect request table:`); console.log(this.#effectRequestTable); - if (this.#effectRequestTable?.type === 'explosion') - this.#currentEffectMarker = new ExplosionMarker(new L.LatLng(0, 0)) - else if (this.#effectRequestTable?.type === 'smoke') - this.#currentEffectMarker = new SmokeMarker(new L.LatLng(0, 0), this.#effectRequestTable.smokeColor ?? "white") + if (this.#effectRequestTable?.type === "explosion") this.#currentEffectMarker = new ExplosionMarker(new L.LatLng(0, 0)); + else if (this.#effectRequestTable?.type === "smoke") + this.#currentEffectMarker = new SmokeMarker(new L.LatLng(0, 0), this.#effectRequestTable.smokeColor ?? "white"); this.#currentEffectMarker?.addTo(this); } } else if (state === OlympusState.UNIT_CONTROL) { @@ -874,6 +899,8 @@ export class Map extends L.Map { this.#isMouseDown = false; window.clearTimeout(this.#longPressTimer); + this.scrollWheelZoom.enable(); + this.#isMouseOnCooldown = true; this.#mouseCooldownTimer = window.setTimeout(() => { this.#isMouseOnCooldown = false; @@ -887,6 +914,8 @@ export class Map extends L.Map { return; } + this.scrollWheelZoom.disable(); + this.#shortPressTimer = window.setTimeout(() => { /* If the mouse is no longer being pressed, execute the short press action */ if (!this.#isMouseDown) this.#onShortPress(e); @@ -898,6 +927,11 @@ export class Map extends L.Map { }, 350); } + #onWheel(e: any) { + //this.#destinationRotation += e.deltaY / 25; + //this.#moveDestinationPreviewMarkers(); + } + #onDoubleClick(e: any) { console.log(`Double click at ${e.latlng}`); @@ -947,14 +981,12 @@ export class Map extends L.Map { else if (this.#effectRequestTable.explosionType === "White phosphorous") getApp().getServerManager().spawnExplosion(50, "phosphorous", pressLocation); - const explosionMarker = new ExplosionMarker(pressLocation, 5); - explosionMarker.addTo(this); + this.addExplosionMarker(pressLocation); } else if (this.#effectRequestTable.type === "smoke") { getApp() .getServerManager() .spawnSmoke(this.#effectRequestTable.smokeColor ?? "white", pressLocation); - const smokeMarker = new SmokeMarker(pressLocation, this.#effectRequestTable.smokeColor ?? "white"); - smokeMarker.addTo(this); + this.addSmokeMarker(pressLocation, this.#effectRequestTable.smokeColor ?? "white"); } } } @@ -982,10 +1014,10 @@ export class Map extends L.Map { } } else if (getApp().getState() === OlympusState.UNIT_CONTROL) { if (e.type === "touchstart" || e.originalEvent.buttons === 1) { - if (this.#contextAction !== null) this.executeContextAction(null, pressLocation); + if (this.#contextAction !== null) this.executeContextAction(null, pressLocation, e.originalEvent); else getApp().setState(OlympusState.IDLE); } else if (e.originalEvent.buttons === 2) { - this.executeDefaultContextAction(null, pressLocation); + this.executeDefaultContextAction(null, pressLocation, e.originalEvent); } } else if (getApp().getState() === OlympusState.JTAC) { if (getApp().getSubState() === JTACSubState.SELECT_TARGET) { @@ -1055,6 +1087,7 @@ export class Map extends L.Map { } else if (getApp().getState() === OlympusState.UNIT_CONTROL) { if (e.originalEvent.button === 2) { getApp().setState(OlympusState.UNIT_CONTROL, UnitControlSubState.MAP_CONTEXT_MENU); + MapContextMenuRequestEvent.dispatch(pressLocation); } else { if (e.type === "touchstart") document.dispatchEvent(new CustomEvent("forceboxselect", { detail: e })); else document.dispatchEvent(new CustomEvent("forceboxselect", { detail: e.originalEvent })); @@ -1070,10 +1103,10 @@ export class Map extends L.Map { this.#lastMousePosition.y = e.originalEvent.y; this.#lastMouseCoordinates = e.latlng; - if (this.#currentSpawnMarker) - this.#currentSpawnMarker.setLatLng(e.latlng); - if (this.#currentEffectMarker) - this.#currentEffectMarker.setLatLng(e.latlng); + if (this.#currentSpawnMarker) this.#currentSpawnMarker.setLatLng(e.latlng); + if (this.#currentEffectMarker) this.#currentEffectMarker.setLatLng(e.latlng); + + this.#moveDestinationPreviewMarkers(); } #onMapMove(e: any) { @@ -1190,4 +1223,37 @@ export class Map extends L.Map { } else this.#IPToTargetLine.setLatLngs([this.#targetPoint.getLatLng(), this.#IPPoint.getLatLng()]); } } + + #updateDestinationPreviewMarkers() { + const selectedUnits = getApp() + .getUnitsManager() + .getSelectedUnits() + .filter((unit) => !unit.getHuman()); + + Object.keys(this.#destinationPreviewMarkers).forEach((ID) => { + this.#destinationPreviewMarkers[ID].removeFrom(this); + delete this.#destinationPreviewMarkers[ID]; + }); + + selectedUnits.forEach((unit) => { + if (["move", "path", "land-at-point"].includes(this.#contextAction?.getId() ?? "")) { + this.#destinationPreviewMarkers[unit.ID] = new TemporaryUnitMarker(new L.LatLng(0, 0), unit.getName(), unit.getCoalition()); + } else if (this.#contextAction?.getTarget() === "position" && this.#contextAction?.getId() !== "land") { + this.#destinationPreviewMarkers[unit.ID] = new TargetMarker(new L.LatLng(0, 0)); + } + this.#destinationPreviewMarkers[unit.ID]?.addTo(this); + }); + } + + #moveDestinationPreviewMarkers() { + if (this.#options.keepRelativePositions) { + Object.entries(getApp().getUnitsManager().computeGroupDestination(this.#lastMouseCoordinates, this.#destinationRotation)).forEach(([ID, latlng]) => { + this.#destinationPreviewMarkers[ID]?.setLatLng(latlng); + }); + } else { + Object.values(this.#destinationPreviewMarkers).forEach((marker) => { + marker.setLatLng(this.#lastMouseCoordinates); + }); + } + } } diff --git a/frontend/react/src/mission/missionmanager.ts b/frontend/react/src/mission/missionmanager.ts index 8d9754bc..e5dab791 100644 --- a/frontend/react/src/mission/missionmanager.ts +++ b/frontend/react/src/mission/missionmanager.ts @@ -7,7 +7,7 @@ import { AirbasesData, BullseyesData, CommandModeOptions, DateAndTime, MissionDa import { Coalition } from "../types/types"; import { Carrier } from "./carrier"; import { NavyUnit } from "../unit/unit"; -import { CommandModeOptionsChangedEvent } from "../events"; +import { CommandModeOptionsChangedEvent, InfoPopupEvent } from "../events"; /** The MissionManager */ export class MissionManager { @@ -94,7 +94,7 @@ export class MissionManager { if (data.mission.theatre != this.#theatre) { this.#theatre = data.mission.theatre; getApp().getMap().setTheatre(this.#theatre); - //(getApp().getPopupsManager().get("infoPopup") as Popup).setText("Map set to " + this.#theatre); + getApp().addInfoMessage("Map set to " + this.#theatre); } /* Set the date and time data */ diff --git a/frontend/react/src/olympusapp.ts b/frontend/react/src/olympusapp.ts index d2eb9f95..b98e6c37 100644 --- a/frontend/react/src/olympusapp.ts +++ b/frontend/react/src/olympusapp.ts @@ -21,7 +21,7 @@ import { ServerManager } from "./server/servermanager"; import { AudioManager } from "./audio/audiomanager"; import { NO_SUBSTATE, OlympusState, OlympusSubState } from "./constants/constants"; -import { AppStateChangedEvent, ConfigLoadedEvent, SelectedUnitsChangedEvent } from "./events"; +import { AppStateChangedEvent, ConfigLoadedEvent, InfoPopupEvent, SelectedUnitsChangedEvent } from "./events"; import { OlympusConfig } from "./interfaces"; export var VERSION = "{{OLYMPUS_VERSION_NUMBER}}"; @@ -33,6 +33,7 @@ export class OlympusApp { #config: OlympusConfig | null = null; #state: OlympusState = OlympusState.NOT_INITIALIZED; #subState: OlympusSubState = NO_SUBSTATE; + #infoMessages: string[] = []; /* Main leaflet map, extended by custom methods */ #map: Map | null = null; @@ -157,4 +158,13 @@ export class OlympusApp { getSubState() { return this.#subState; } + + addInfoMessage(message: string) { + this.#infoMessages.push(message); + InfoPopupEvent.dispatch(this.#infoMessages); + setTimeout(() => { + this.#infoMessages.shift(); + InfoPopupEvent.dispatch(this.#infoMessages); + }, 5000) + } } diff --git a/frontend/react/src/other/utils.ts b/frontend/react/src/other/utils.ts index 9739cc64..f332d030 100644 --- a/frontend/react/src/other/utils.ts +++ b/frontend/react/src/other/utils.ts @@ -227,63 +227,6 @@ export function polygonArea(polygon: Polygon) { return turf.area(poly); } -export function randomUnitBlueprint( - unitDatabase: UnitDatabase, - options: { - type?: string; - role?: string; - ranges?: string[]; - eras?: string[]; - coalition?: string; - } -) { - /* Start from all the unit blueprints in the database */ - var unitBlueprints = unitDatabase.getBlueprints(); - - /* If a specific type or role is provided, use only the blueprints of that type or role */ - if (options.type && options.role) { - console.error("Can't create random unit if both type and role are provided. Either create by type or by role."); - return null; - } - - if (options.type) { - unitBlueprints = unitDatabase.getByType(options.type); - } else if (options.role) { - unitBlueprints = unitDatabase.getByType(options.role); - } - - /* Keep only the units that have a range included in the requested values */ - if (options.ranges) { - unitBlueprints = unitBlueprints.filter((unitBlueprint: UnitBlueprint) => { - var rangeType = ""; - var range = unitBlueprint.acquisitionRange; - if (range !== undefined) { - if (range >= 0 && range < 10000) rangeType = "Short range"; - else if (range >= 10000 && range < 100000) rangeType = "Medium range"; - else if (range >= 100000 && range < 999999) rangeType = "Long range"; - } - return options.ranges?.includes(rangeType); - }); - } - - /* Keep only the units that have an era included in the requested values */ - if (options.eras) { - unitBlueprints = unitBlueprints.filter((unitBlueprint: UnitBlueprint) => { - return unitBlueprint.era ? options.eras?.includes(unitBlueprint.era) : true; - }); - } - - /* Keep only the units that have the correct coalition, if selected */ - if (options.coalition) { - unitBlueprints = unitBlueprints.filter((unitBlueprint: UnitBlueprint) => { - return unitBlueprint.coalition && unitBlueprint.coalition !== "" ? options.coalition === unitBlueprint.coalition : true; - }); - } - - var index = Math.floor(Math.random() * unitBlueprints.length); - return unitBlueprints[index]; -} - export function enumToState(state: number) { if (state < states.length) return states[state]; else return states[0]; @@ -347,9 +290,8 @@ export function convertDateAndTimeToDate(dateAndTime: DateAndTime) { export function getGroundElevation(latlng: LatLng, callback: CallableFunction) { /* Get the ground elevation from the server endpoint */ - /* TODO */ const xhr = new XMLHttpRequest(); - xhr.open("GET", `api/elevation/${latlng.lat}/${latlng.lng}`, true); + xhr.open("GET", window.location.href.split("?")[0].replace("vite/", "") + `api/elevation/${latlng.lat}/${latlng.lng}`, true); xhr.timeout = 500; // ms xhr.responseType = "json"; xhr.onload = () => { diff --git a/frontend/react/src/server/servermanager.ts b/frontend/react/src/server/servermanager.ts index 8fca1ae1..f86c2437 100644 --- a/frontend/react/src/server/servermanager.ts +++ b/frontend/react/src/server/servermanager.ts @@ -14,7 +14,7 @@ import { reactionsToThreat, } from "../constants/constants"; import { AirbasesData, BullseyesData, CommandModeOptions, GeneralSettings, MissionData, Radio, ServerRequestOptions, ServerStatus, TACAN } from "../interfaces"; -import { ServerStatusUpdatedEvent } from "../events"; +import { InfoPopupEvent, ServerStatusUpdatedEvent } from "../events"; export class ServerManager { #connected: boolean = false; @@ -350,12 +350,6 @@ export class ServerManager { this.PUT(data, callback); } - showFormationMenu(ID: number, isLeader: boolean, wingmenIDs: number[], callback: CallableFunction = () => {}) { - var command = { ID: ID, wingmenIDs: wingmenIDs, isLeader: isLeader }; - var data = { setLeader: command }; - this.PUT(data, callback); - } - setROE(ID: number, ROE: string, callback: CallableFunction = () => {}) { var command = { ID: ID, ROE: ROEs.indexOf(ROE) }; var data = { setROE: command }; @@ -660,7 +654,7 @@ export class ServerManager { setConnected(newConnected: boolean) { if (this.#connected != newConnected) { - //newConnected ? (getApp().getPopupsManager().get("infoPopup") as Popup).setText("Connected to DCS Olympus server") : (getApp().getPopupsManager().get("infoPopup") as Popup).setText("Disconnected from DCS Olympus server"); + newConnected ? getApp().addInfoMessage("Connected to DCS Olympus server") : getApp().addInfoMessage("Disconnected from DCS Olympus server"); if (newConnected) { document.getElementById("splash-screen")?.classList.add("hide"); document.getElementById("gray-out")?.classList.add("hide"); @@ -676,7 +670,7 @@ export class ServerManager { setPaused(newPaused: boolean) { this.#paused = newPaused; - //this.#paused ? (getApp().getPopupsManager().get("infoPopup") as Popup).setText("View paused") : (getApp().getPopupsManager().get("infoPopup") as Popup).setText("View unpaused"); + this.#paused ? getApp().addInfoMessage("View paused") : getApp().addInfoMessage("View unpaused"); } getPaused() { diff --git a/frontend/react/src/shortcut/shortcutmanager.ts b/frontend/react/src/shortcut/shortcutmanager.ts index 7f680a0d..16046c58 100644 --- a/frontend/react/src/shortcut/shortcutmanager.ts +++ b/frontend/react/src/shortcut/shortcutmanager.ts @@ -89,6 +89,15 @@ export class ShortcutManager { ctrlKey: false, shiftKey: false, }) + .addKeyboardShortcut("toggleRelativePositions", { + altKey: false, + callback: () => { + getApp().getMap().setOption("keepRelativePositions", !getApp().getMap().getOptions().keepRelativePositions); + }, + code: "KeyP", + ctrlKey: false, + shiftKey: false, + }) .addKeyboardShortcut("increaseCameraZoom", { altKey: true, callback: () => { diff --git a/frontend/react/src/types/types.ts b/frontend/react/src/types/types.ts index bc702044..710c23d2 100644 --- a/frontend/react/src/types/types.ts +++ b/frontend/react/src/types/types.ts @@ -21,6 +21,7 @@ export type MapOptions = { fillSelectedRing: boolean; showMinimap: boolean; protectDCSUnits: boolean; + keepRelativePositions: boolean; }; export type MapHiddenTypes = { diff --git a/frontend/react/src/ui/components/olstatebutton.tsx b/frontend/react/src/ui/components/olstatebutton.tsx index ce2f8fc2..83a11b5f 100644 --- a/frontend/react/src/ui/components/olstatebutton.tsx +++ b/frontend/react/src/ui/components/olstatebutton.tsx @@ -6,7 +6,7 @@ import { OlTooltip } from "./oltooltip"; export function OlStateButton(props: { className?: string; - borderColor?: string | null; + buttonColor?: string | null; checked: boolean; icon: IconProp; tooltip: string; @@ -21,10 +21,13 @@ export function OlStateButton(props: { ` h-[40px] w-[40px] flex-none rounded-md text-lg font-medium dark:bg-olympus-600 dark:text-gray-300 dark:hover:bg-olympus-300 - dark:data-[checked='true']:bg-blue-500 - dark:data-[checked='true']:text-white `; + let textColor = "white"; + if (props.checked && props.buttonColor == "white") { + textColor = "#243141" + } + return ( <> diff --git a/frontend/react/src/ui/contextmenus/mapcontextmenu.tsx b/frontend/react/src/ui/contextmenus/mapcontextmenu.tsx index 90ebd904..1be88d06 100644 --- a/frontend/react/src/ui/contextmenus/mapcontextmenu.tsx +++ b/frontend/react/src/ui/contextmenus/mapcontextmenu.tsx @@ -5,13 +5,21 @@ import { CONTEXT_ACTION_COLORS, NO_SUBSTATE, OlympusState, OlympusSubState, Unit import { OlDropdownItem } from "../components/oldropdown"; import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; import { LatLng } from "leaflet"; -import { AppStateChangedEvent, ContextActionChangedEvent, ContextActionSetChangedEvent, SelectionClearedEvent } from "../../events"; +import { + AppStateChangedEvent, + ContextActionChangedEvent, + ContextActionSetChangedEvent, + MapContextMenuRequestEvent, + SelectionClearedEvent, + UnitContextMenuRequestEvent, +} from "../../events"; import { ContextActionSet } from "../../unit/contextactionset"; +import { getApp } from "../../olympusapp"; export function MapContextMenu(props: {}) { const [appState, setAppState] = useState(OlympusState.NOT_INITIALIZED); const [appSubState, setAppSubState] = useState(NO_SUBSTATE as OlympusSubState); - const [contextActionSet, setContextActionsSet] = useState(null as ContextActionSet | null); + const [contextActionSet, setcontextActionSet] = useState(null as ContextActionSet | null); const [xPosition, setXPosition] = useState(0); const [yPosition, setYPosition] = useState(0); const [latLng, setLatLng] = useState(null as null | LatLng); @@ -19,14 +27,24 @@ export function MapContextMenu(props: {}) { var contentRef = useRef(null); - // TODO show at correct position - useEffect(() => { AppStateChangedEvent.on((state, subState) => { setAppState(state); setAppSubState(subState); }); - ContextActionSetChangedEvent.on((contextActionSet) => setContextActionsSet(contextActionSet)); + ContextActionSetChangedEvent.on((contextActionSet) => setcontextActionSet(contextActionSet)); + MapContextMenuRequestEvent.on((latlng) => { + setLatLng(latlng); + const containerPoint = getApp().getMap().latLngToContainerPoint(latlng); + setXPosition(getApp().getMap().getContainer().offsetLeft + containerPoint.x); + setYPosition(getApp().getMap().getContainer().offsetTop + containerPoint.y); + }); + UnitContextMenuRequestEvent.on((unit) => { + setUnit(unit); + const containerPoint = getApp().getMap().latLngToContainerPoint(unit.getPosition()); + setXPosition(getApp().getMap().getContainer().offsetLeft + containerPoint.x); + setYPosition(getApp().getMap().getContainer().offsetTop + containerPoint.y); + }); }, []); useEffect(() => { @@ -50,64 +68,63 @@ export function MapContextMenu(props: {}) { } }); - let reorderedActions: ContextAction[] = []; - CONTEXT_ACTION_COLORS.forEach((color) => { - if (contextActionSet) - Object.values(contextActionSet.getContextActions()).forEach((contextAction: ContextAction) => { - if (color === null && contextAction.getOptions().buttonColor === undefined) reorderedActions.push(contextAction); - else if (color === contextAction.getOptions().buttonColor) reorderedActions.push(contextAction); - }); - }); + let reorderedActions: ContextAction[] = contextActionSet + ? Object.values(contextActionSet.getContextActions(appSubState === UnitControlSubState.MAP_CONTEXT_MENU ? "position" : "unit")).sort( + (a: ContextAction, b: ContextAction) => (a.getOptions().type < b.getOptions().type ? -1 : 1) + ) + : []; return ( <> - {appState === OlympusState.UNIT_CONTROL && appSubState === UnitControlSubState.UNIT_CONTEXT_MENU && ( - <> -
+ {appState === OlympusState.UNIT_CONTROL && + (appSubState === UnitControlSubState.MAP_CONTEXT_MENU || appSubState === UnitControlSubState.UNIT_CONTEXT_MENU) && ( + <>
- {contextActionSet && - Object.values(contextActionSet.getContextActions(latLng ? "position" : "unit")).map((contextActionIt) => { - const colorString = contextActionIt.getOptions().buttonColor - ? ` +
+ {contextActionSet && + reorderedActions.map((contextActionIt) => { + const colorString = ` border-2 - border-${contextActionIt.getOptions().buttonColor}-500 - ` - : ""; - return ( - { - if (contextActionIt.getOptions().executeImmediately) { - contextActionIt.executeCallback(null, null); - } else { - if (latLng !== null) { - contextActionIt.executeCallback(null, latLng); - } else if (unit !== null) { - contextActionIt.executeCallback(unit, null); - } - } - - }} - > - -
{contextActionIt.getLabel()}
-
- ); - })} + border-${CONTEXT_ACTION_COLORS[contextActionIt.getOptions().type]}-500 + `; + + return ( + { + if (contextActionIt.getOptions().executeImmediately) { + contextActionIt.executeCallback(null, null); + } else { + if (appSubState === UnitControlSubState.MAP_CONTEXT_MENU ) { + contextActionIt.executeCallback(null, latLng); + } else if (unit !== null) { + contextActionIt.executeCallback(unit, null); + } + } + getApp().setState(OlympusState.UNIT_CONTROL) + }} + > + +
{contextActionIt.getLabel()}
+
+ ); + })} +
-
- - )} + + )} ); } diff --git a/frontend/react/src/ui/panels/airbasemenu.tsx b/frontend/react/src/ui/panels/airbasemenu.tsx index 6e235377..636013d3 100644 --- a/frontend/react/src/ui/panels/airbasemenu.tsx +++ b/frontend/react/src/ui/panels/airbasemenu.tsx @@ -9,8 +9,9 @@ import { OlAccordion } from "../components/olaccordion"; import { OlUnitListEntry } from "../components/olunitlistentry"; import { olButtonsVisibilityAircraft, olButtonsVisibilityHelicopter } from "../components/olicons"; import { UnitSpawnMenu } from "./unitspawnmenu"; -import { AirbaseSelectedEvent, UnitDatabaseLoadedEvent } from "../../events"; +import { AirbaseSelectedEvent, CommandModeOptionsChangedEvent, UnitDatabaseLoadedEvent } from "../../events"; import { getApp } from "../../olympusapp"; +import { BLUE_COMMANDER, COMMAND_MODE_OPTIONS_DEFAULTS, GAME_MASTER, RED_COMMANDER } from "../../constants/constants"; enum CategoryAccordion { NONE, @@ -27,6 +28,8 @@ export function AirbaseMenu(props: { open: boolean; onClose: () => void; childre const [blueprints, setBlueprints] = useState([] as UnitBlueprint[]); const [roles, setRoles] = useState({ aircraft: [] as string[], helicopter: [] as string[] }); const [openAccordion, setOpenAccordion] = useState(CategoryAccordion.NONE); + const [commandModeOptions, setCommandModeOptions] = useState(COMMAND_MODE_OPTIONS_DEFAULTS); + const [showCost, setShowCost] = useState(false); useEffect(() => { AirbaseSelectedEvent.on((airbase) => { @@ -45,6 +48,12 @@ export function AirbaseMenu(props: { open: boolean; onClose: () => void; childre .getRoles((unit) => unit.category === "helicopter"), }); }); + + CommandModeOptionsChangedEvent.on((commandModeOptions) => { + setCommandModeOptions(commandModeOptions); + setShowCost(!(commandModeOptions.commandMode === GAME_MASTER || !commandModeOptions.restrictSpawns)); + setOpenAccordion(CategoryAccordion.NONE); + }); }, []); useEffect(() => { @@ -132,112 +141,137 @@ export function AirbaseMenu(props: { open: boolean; onClose: () => void; childre -
- {blueprint && ( - setBlueprint(null)} - /> - )} - Spawn units at airbase -
- {blueprint === null && ( -
- setFilterString(value)} text={filterString} /> - { - setOpenAccordion(openAccordion === CategoryAccordion.AIRCRAFT ? CategoryAccordion.NONE : CategoryAccordion.AIRCRAFT); - setSelectedRole(null); - }} - > -
- {roles.aircraft.sort().map((role) => { - return ( -
{ - selectedRole === role ? setSelectedRole(null) : setSelectedRole(role); - }} - > - {role} -
- ); - })} + {(commandModeOptions.commandMode === GAME_MASTER || + (commandModeOptions.commandMode === BLUE_COMMANDER && airbase?.getCoalition() === "blue") || + (commandModeOptions.commandMode === RED_COMMANDER && airbase?.getCoalition() === "red")) && ( + <> +
+ {blueprint && ( + setBlueprint(null)} + /> + )} + Spawn units at airbase +
+ {blueprint === null && ( +
+ setFilterString(value)} text={filterString} /> + { + setOpenAccordion(openAccordion === CategoryAccordion.AIRCRAFT ? CategoryAccordion.NONE : CategoryAccordion.AIRCRAFT); + setSelectedRole(null); + }} + > +
+ {roles.aircraft.sort().map((role) => { + return ( +
{ + selectedRole === role ? setSelectedRole(null) : setSelectedRole(role); + }} + > + {role} +
+ ); + })} +
+
+ {filteredBlueprints + .filter((blueprint) => blueprint.category === "aircraft") + .map((blueprint) => { + return ( + setBlueprint(blueprint)} + showCost={showCost} + cost={getApp().getUnitsManager().getDatabase().getSpawnPointsByName(blueprint.name)} + /> + ); + })} +
+
+ { + setOpenAccordion(openAccordion === CategoryAccordion.HELICOPTER ? CategoryAccordion.NONE : CategoryAccordion.HELICOPTER); + setSelectedRole(null); + }} + > +
+ {roles.helicopter.sort().map((role) => { + return ( +
{ + selectedRole === role ? setSelectedRole(null) : setSelectedRole(role); + }} + > + {role} +
+ ); + })} +
+
+ {filteredBlueprints + .filter((blueprint) => blueprint.category === "helicopter") + .map((blueprint) => { + return ( + setBlueprint(blueprint)} + showCost={showCost} + cost={getApp().getUnitsManager().getDatabase().getSpawnPointsByName(blueprint.name)} + /> + ); + })} +
+
-
- {filteredBlueprints - .filter((blueprint) => blueprint.category === "aircraft") - .map((entry) => { - return setBlueprint(entry)} />; - })} -
- - { - setOpenAccordion(openAccordion === CategoryAccordion.HELICOPTER ? CategoryAccordion.NONE : CategoryAccordion.HELICOPTER); - setSelectedRole(null); - }} - > -
- {roles.helicopter.sort().map((role) => { - return ( -
{ - selectedRole === role ? setSelectedRole(null) : setSelectedRole(role); - }} - > - {role} -
- ); - })} -
-
- {filteredBlueprints - .filter((blueprint) => blueprint.category === "helicopter") - .map((entry) => { - return setBlueprint(entry)} />; - })} -
-
-
+ )} + <> + {!(blueprint === null) && ( + + )} + + )} - <> - {!(blueprint === null) && ( - - )} -
); diff --git a/frontend/react/src/ui/panels/formationmenu.tsx b/frontend/react/src/ui/panels/formationmenu.tsx index 5e87501f..ec6cadf9 100644 --- a/frontend/react/src/ui/panels/formationmenu.tsx +++ b/frontend/react/src/ui/panels/formationmenu.tsx @@ -4,19 +4,21 @@ import { OlDropdown, OlDropdownItem } from "../components/oldropdown"; import { useDrag } from "../libs/useDrag"; import { Unit } from "../../unit/unit"; import { OlRangeSlider } from "../components/olrangeslider"; +import { FormationCreationRequestEvent } from "../../events"; export function FormationMenu(props: { open: boolean; onClose: () => void; - leader: Unit | null; - wingmen: Unit[] | null; children?: JSX.Element | JSX.Element[]; }) { + const [leader, setLeader] = useState(null as Unit | null) + const [wingmen, setWingmen] = useState(null as Unit[] | null) + /* The useDrag custom hook used to handle the dragging of the units requires that the number of hooks remains unchanged. The units array is therefore initialized to 128 units maximum. */ let units = Array(128).fill(null) as (Unit | null)[]; - units[0] = props.leader; - props.wingmen?.forEach((unit, idx) => { + units[0] = leader; + wingmen?.forEach((unit, idx) => { if (idx < units.length) units[idx + 1] = unit; }); @@ -53,6 +55,13 @@ export function FormationMenu(props: { }); }); + useEffect(() => { + FormationCreationRequestEvent.on((leader, wingmen) => { + setLeader(leader); + setWingmen(wingmen); + }) + }) + /* When the formation type is changed, reset the position to the center and the position of the silhouettes depending on the aircraft */ useEffect(() => { if (scrollRef.current && containerRef.current) { diff --git a/frontend/react/src/ui/panels/infobar.tsx b/frontend/react/src/ui/panels/infobar.tsx index e41ca112..ae0fd470 100644 --- a/frontend/react/src/ui/panels/infobar.tsx +++ b/frontend/react/src/ui/panels/infobar.tsx @@ -1,132 +1,52 @@ -import React, { useEffect, useRef, useState } from "react"; -import { ContextActionSet } from "../../unit/contextactionset"; -import { OlStateButton } from "../components/olstatebutton"; -import { getApp } from "../../olympusapp"; -import { ContextAction } from "../../unit/contextaction"; -import { CONTEXT_ACTION_COLORS } from "../../constants/constants"; -import { FaInfoCircle } from "react-icons/fa"; -import { FaChevronLeft, FaChevronRight } from "react-icons/fa6"; +import React, { useEffect, useState } from "react"; +import { AppStateChangedEvent, ContextActionChangedEvent, InfoPopupEvent } from "../../events"; import { OlympusState } from "../../constants/constants"; -import { AppStateChangedEvent, ContextActionChangedEvent, ContextActionSetChangedEvent } from "../../events"; +import { ContextAction } from "../../unit/contextaction"; export function InfoBar(props: {}) { + const [messages, setMessages] = useState([] as string[]); const [appState, setAppState] = useState(OlympusState.NOT_INITIALIZED); - const [contextActionSet, setContextActionsSet] = useState(null as ContextActionSet | null); const [contextAction, setContextAction] = useState(null as ContextAction | null); - const [scrolledLeft, setScrolledLeft] = useState(true); - const [scrolledRight, setScrolledRight] = useState(false); - - /* Initialize the "scroll" position of the element */ - var scrollRef = useRef(null); - useEffect(() => { - if (scrollRef.current) onScroll(scrollRef.current); - }); useEffect(() => { + InfoPopupEvent.on((messages) => setMessages([...messages])); AppStateChangedEvent.on((state, subState) => setAppState(state)); - ContextActionSetChangedEvent.on((contextActionSet) => setContextActionsSet(contextActionSet)); ContextActionChangedEvent.on((contextAction) => setContextAction(contextAction)); }, []); - function onScroll(el) { - const sl = el.scrollLeft; - const sr = el.scrollWidth - el.scrollLeft - el.clientWidth; - - sl < 1 && !scrolledLeft && setScrolledLeft(true); - sl > 1 && scrolledLeft && setScrolledLeft(false); - - sr < 1 && !scrolledRight && setScrolledRight(true); - sr > 1 && scrolledRight && setScrolledRight(false); + let topString = ""; + if (appState === OlympusState.UNIT_CONTROL) { + if (contextAction === null) { + topString = "top-32"; + } else { + topString = "top-48"; + } + } else { + topString = "top-16"; } - let reorderedActions: ContextAction[] = []; - CONTEXT_ACTION_COLORS.forEach((color) => { - if (contextActionSet) { - Object.values(contextActionSet.getContextActions()).forEach((contextAction: ContextAction) => { - if (color === null && contextAction.getOptions().buttonColor === undefined) reorderedActions.push(contextAction); - else if (color === contextAction.getOptions().buttonColor) reorderedActions.push(contextAction); - }); - } - }); - return ( - <> - {appState === OlympusState.UNIT_CONTROL && contextActionSet && Object.keys(contextActionSet.getContextActions()).length > 0 && ( - <> +
+ {messages.map((message, idx) => { + return (
- {!scrolledLeft && ( - - )} -
onScroll(ev.target)} ref={scrollRef}> - {reorderedActions.map((contextActionIt: ContextAction) => { - return ( - { - if (contextActionIt.getOptions().executeImmediately) { - contextActionIt.executeCallback(null, null); - } else { - contextActionIt !== contextAction ? getApp().getMap().setContextAction(contextActionIt) : getApp().getMap().setContextAction(null); - } - }} - /> - ); - })} -
- {!scrolledRight && ( - - )} + {message}
- {contextAction && ( -
-
- )} - - )} - + ); + })} +
); } diff --git a/frontend/react/src/ui/panels/optionsmenu.tsx b/frontend/react/src/ui/panels/optionsmenu.tsx index 650ab8fe..a88a23b4 100644 --- a/frontend/react/src/ui/panels/optionsmenu.tsx +++ b/frontend/react/src/ui/panels/optionsmenu.tsx @@ -1,12 +1,19 @@ -import React from "react"; +import React, { useEffect, useState } from "react"; import { Menu } from "./components/menu"; import { OlCheckbox } from "../components/olcheckbox"; import { OlRangeSlider } from "../components/olrangeslider"; import { OlNumberInput } from "../components/olnumberinput"; -import { MapOptions } from "../../types/types"; import { getApp } from "../../olympusapp"; +import { MAP_OPTIONS_DEFAULTS } from "../../constants/constants"; +import { MapOptionsChangedEvent } from "../../events"; + +export function OptionsMenu(props: { open: boolean; onClose: () => void; children?: JSX.Element | JSX.Element[] }) { + const [mapOptions, setMapOptions] = useState(MAP_OPTIONS_DEFAULTS); + + useEffect(() => { + MapOptionsChangedEvent.on((mapOptions) => setMapOptions({ ...mapOptions })); + }, []); -export function OptionsMenu(props: { open: boolean; onClose: () => void; options: MapOptions; children?: JSX.Element | JSX.Element[] }) { return (
void; options dark:hover:bg-olympus-400 `} onClick={() => { - getApp().getMap().setOption("showUnitLabels", !props.options.showUnitLabels); + getApp().getMap().setOption("showUnitLabels", !mapOptions.showUnitLabels); }} > - {}}> + {}}> Show Unit Labels void; options dark:hover:bg-olympus-400 `} onClick={() => { - getApp().getMap().setOption("showUnitsEngagementRings", !props.options.showUnitsEngagementRings); + getApp().getMap().setOption("showUnitsEngagementRings", !mapOptions.showUnitsEngagementRings); }} > - {}}> + {}}> Show Threat Rings void; options dark:hover:bg-olympus-400 `} onClick={() => { - getApp().getMap().setOption("showUnitsAcquisitionRings", !props.options.showUnitsAcquisitionRings); + getApp().getMap().setOption("showUnitsAcquisitionRings", !mapOptions.showUnitsAcquisitionRings); }} > - {}}> + {}}> Show Detection rings void; options dark:hover:bg-olympus-400 `} onClick={() => { - getApp().getMap().setOption("showUnitTargets", !props.options.showUnitTargets); + getApp().getMap().setOption("showUnitTargets", !mapOptions.showUnitTargets); }} > - {}}> + {}}> Show Detection lines void; options dark:hover:bg-olympus-400 `} onClick={() => { - getApp().getMap().setOption("hideUnitsShortRangeRings", !props.options.hideUnitsShortRangeRings); + getApp().getMap().setOption("hideUnitsShortRangeRings", !mapOptions.hideUnitsShortRangeRings); }} > - {}}> + {}}> Hide Short range Rings void; options dark:hover:bg-olympus-400 `} onClick={() => { - getApp().getMap().setOption("hideGroupMembers", !props.options.hideGroupMembers); + getApp().getMap().setOption("keepRelativePositions", !mapOptions.keepRelativePositions); }} > - {}}> + {}}> + Keep units relative positions + + P + +
+
{ + getApp().getMap().setOption("hideGroupMembers", !mapOptions.hideGroupMembers); + }} + > + {}}> Hide Group members void; options dark:hover:bg-olympus-400 `} onClick={() => { - getApp().getMap().setOption("showMinimap", !props.options.showMinimap); + getApp().getMap().setOption("showMinimap", !mapOptions.showMinimap); }} > - {}}> + {}}> Show minimap void; children? blueprint={blueprint} onClick={() => setBlueprint(blueprint)} showCost={showCost} - cost={blueprint.cost ?? 10} + cost={getApp().getUnitsManager().getDatabase().getSpawnPointsByName(blueprint.name)} /> ); })} @@ -219,7 +219,7 @@ export function SpawnMenu(props: { open: boolean; onClose: () => void; children? blueprint={blueprint} onClick={() => setBlueprint(blueprint)} showCost={showCost} - cost={blueprint.cost ?? 10} + cost={getApp().getUnitsManager().getDatabase().getSpawnPointsByName(blueprint.name)} /> ); })} @@ -250,7 +250,7 @@ export function SpawnMenu(props: { open: boolean; onClose: () => void; children? blueprint={blueprint} onClick={() => setBlueprint(blueprint)} showCost={showCost} - cost={blueprint.cost ?? 10} + cost={getApp().getUnitsManager().getDatabase().getSpawnPointsByName(blueprint.name)} /> ); })} @@ -281,7 +281,7 @@ export function SpawnMenu(props: { open: boolean; onClose: () => void; children? blueprint={blueprint} onClick={() => setBlueprint(blueprint)} showCost={showCost} - cost={blueprint.cost ?? 10} + cost={getApp().getUnitsManager().getDatabase().getSpawnPointsByName(blueprint.name)} /> ); })} @@ -336,7 +336,7 @@ export function SpawnMenu(props: { open: boolean; onClose: () => void; children? blueprint={blueprint} onClick={() => setBlueprint(blueprint)} showCost={showCost} - cost={blueprint.cost ?? 10} + cost={getApp().getUnitsManager().getDatabase().getSpawnPointsByName(blueprint.name)} /> ); })} @@ -388,7 +388,7 @@ export function SpawnMenu(props: { open: boolean; onClose: () => void; children? blueprint={blueprint} onClick={() => setBlueprint(blueprint)} showCost={showCost} - cost={blueprint.cost ?? 10} + cost={getApp().getUnitsManager().getDatabase().getSpawnPointsByName(blueprint.name)} /> ); })} diff --git a/frontend/react/src/ui/panels/unitcontrolbar.tsx b/frontend/react/src/ui/panels/unitcontrolbar.tsx index 54d1257b..27d2b399 100644 --- a/frontend/react/src/ui/panels/unitcontrolbar.tsx +++ b/frontend/react/src/ui/panels/unitcontrolbar.tsx @@ -11,7 +11,7 @@ import { AppStateChangedEvent, ContextActionChangedEvent, ContextActionSetChange export function UnitControlBar(props: {}) { const [appState, setAppState] = useState(OlympusState.NOT_INITIALIZED); - const [contextActionSet, setContextActionsSet] = useState(null as ContextActionSet | null); + const [contextActionSet, setcontextActionSet] = useState(null as ContextActionSet | null); const [contextAction, setContextAction] = useState(null as ContextAction | null); const [scrolledLeft, setScrolledLeft] = useState(true); const [scrolledRight, setScrolledRight] = useState(false); @@ -24,7 +24,7 @@ export function UnitControlBar(props: {}) { useEffect(() => { AppStateChangedEvent.on((state, subState) => setAppState(state)); - ContextActionSetChangedEvent.on((contextActionSet) => setContextActionsSet(contextActionSet)); + ContextActionSetChangedEvent.on((contextActionSet) => setcontextActionSet(contextActionSet)); ContextActionChangedEvent.on((contextAction) => setContextAction(contextAction)); }, []); @@ -39,15 +39,9 @@ export function UnitControlBar(props: {}) { sr > 1 && scrolledRight && setScrolledRight(false); } - let reorderedActions: ContextAction[] = []; - CONTEXT_ACTION_COLORS.forEach((color) => { - if (contextActionSet) { - Object.values(contextActionSet.getContextActions()).forEach((contextAction: ContextAction) => { - if (color === null && contextAction.getOptions().buttonColor === undefined) reorderedActions.push(contextAction); - else if (color === contextAction.getOptions().buttonColor) reorderedActions.push(contextAction); - }); - } - }); + let reorderedActions: ContextAction[] = contextActionSet + ? Object.values(contextActionSet.getContextActions()).sort((a: ContextAction, b: ContextAction) => (a.getOptions().type < b.getOptions().type ? -1 : 1)) + : []; return ( <> @@ -77,7 +71,7 @@ export function UnitControlBar(props: {}) { checked={contextActionIt === contextAction} icon={contextActionIt.getIcon()} tooltip={contextActionIt.getLabel()} - borderColor={contextActionIt.getOptions().buttonColor} + buttonColor={CONTEXT_ACTION_COLORS[contextActionIt.getOptions().type ?? 0]} onClick={() => { if (contextActionIt.getOptions().executeImmediately) { contextActionIt.executeCallback(null, null); @@ -99,6 +93,7 @@ export function UnitControlBar(props: {}) { /> )}
+ {/*} {contextAction && (
)} + {*/} )} diff --git a/frontend/react/src/ui/panels/unitexplosionmenu.tsx b/frontend/react/src/ui/panels/unitexplosionmenu.tsx index 72be6675..615fd2e0 100644 --- a/frontend/react/src/ui/panels/unitexplosionmenu.tsx +++ b/frontend/react/src/ui/panels/unitexplosionmenu.tsx @@ -1,12 +1,18 @@ -import React, { useState } from "react"; +import React, { useEffect, useState } from "react"; import { Menu } from "./components/menu"; import { OlDropdown, OlDropdownItem } from "../components/oldropdown"; import { Unit } from "../../unit/unit"; import { getApp } from "../../olympusapp"; +import { UnitExplosionRequestEvent } from "../../events"; -export function UnitExplosionMenu(props: { open: boolean; onClose: () => void; units: Unit[] | null; children?: JSX.Element | JSX.Element[] }) { +export function UnitExplosionMenu(props: { open: boolean; onClose: () => void; children?: JSX.Element | JSX.Element[] }) { + const [units, setUnits] = useState(null as null | Unit[]) const [explosionType, setExplosionType] = useState("High explosive"); + useEffect(() => { + UnitExplosionRequestEvent.on((units) => setUnits(units)) + }, []) + return (
@@ -26,16 +32,16 @@ export function UnitExplosionMenu(props: { open: boolean; onClose: () => void; u ); })} - {props.units !== null && ( + {units !== null && (
); diff --git a/frontend/react/src/unit/contextaction.ts b/frontend/react/src/unit/contextaction.ts index 99197097..636bbd4f 100644 --- a/frontend/react/src/unit/contextaction.ts +++ b/frontend/react/src/unit/contextaction.ts @@ -1,13 +1,14 @@ import { IconDefinition } from "@fortawesome/fontawesome-svg-core"; import { Unit } from "./unit"; import { LatLng } from "leaflet"; +import { ContextActionType } from "../constants/constants"; export interface ContextActionOptions { executeImmediately?: boolean; - buttonColor?: string | null; + type: ContextActionType; } -export type ContextActionCallback = (units: Unit[], targetUnit: Unit | null, targetPosition: LatLng | null) => void; +export type ContextActionCallback = (units: Unit[], targetUnit: Unit | null, targetPosition: LatLng | null, originalEvent?: MouseEvent) => void; export class ContextAction { #id: string = ""; @@ -64,7 +65,7 @@ export class ContextAction { return this.#target; } - executeCallback(targetUnit: Unit | null, targetPosition: LatLng | null) { - if (this.#callback) this.#callback(this.#units, targetUnit, targetPosition); + executeCallback(targetUnit: Unit | null, targetPosition: LatLng | null, originalEvent?: MouseEvent) { + if (this.#callback) this.#callback(this.#units, targetUnit, targetPosition, originalEvent); } } diff --git a/frontend/react/src/unit/databases/unitdatabase.ts b/frontend/react/src/unit/databases/unitdatabase.ts index 97f2a8aa..0d218033 100644 --- a/frontend/react/src/unit/databases/unitdatabase.ts +++ b/frontend/react/src/unit/databases/unitdatabase.ts @@ -226,4 +226,61 @@ export class UnitDatabase { shortLabel: "", }; } + + +getRandomUnit( + options: { + type?: string; + role?: string; + ranges?: string[]; + eras?: string[]; + coalition?: string; + } +) { + /* Start from all the unit blueprints in the database */ + var unitBlueprints = this.getBlueprints(); + + /* If a specific type or role is provided, use only the blueprints of that type or role */ + if (options.type && options.role) { + console.error("Can't create random unit if both type and role are provided. Either create by type or by role."); + return null; + } + + if (options.type) { + unitBlueprints = this.getByType(options.type); + } else if (options.role) { + unitBlueprints = this.getByType(options.role); + } + + /* Keep only the units that have a range included in the requested values */ + if (options.ranges) { + unitBlueprints = unitBlueprints.filter((unitBlueprint: UnitBlueprint) => { + var rangeType = ""; + var range = unitBlueprint.acquisitionRange; + if (range !== undefined) { + if (range >= 0 && range < 10000) rangeType = "Short range"; + else if (range >= 10000 && range < 100000) rangeType = "Medium range"; + else if (range >= 100000 && range < 999999) rangeType = "Long range"; + } + return options.ranges?.includes(rangeType); + }); + } + + /* Keep only the units that have an era included in the requested values */ + if (options.eras) { + unitBlueprints = unitBlueprints.filter((unitBlueprint: UnitBlueprint) => { + return unitBlueprint.era ? options.eras?.includes(unitBlueprint.era) : true; + }); + } + + /* Keep only the units that have the correct coalition, if selected */ + if (options.coalition) { + unitBlueprints = unitBlueprints.filter((unitBlueprint: UnitBlueprint) => { + return unitBlueprint.coalition && unitBlueprint.coalition !== "" ? options.coalition === unitBlueprint.coalition : true; + }); + } + + var index = Math.floor(Math.random() * unitBlueprints.length); + return unitBlueprints[index]; +} } diff --git a/frontend/react/src/unit/unit.ts b/frontend/react/src/unit/unit.ts index 0c198a17..d37c51ba 100644 --- a/frontend/react/src/unit/unit.ts +++ b/frontend/react/src/unit/unit.ts @@ -18,7 +18,6 @@ import { } from "../other/utils"; import { CustomMarker } from "../map/markers/custommarker"; import { SVGInjector } from "@tanem/svg-injector"; -import { UnitDatabase } from "./databases/unitdatabase"; import { TargetMarker } from "../map/markers/targetmarker"; import { DLINK, @@ -36,11 +35,10 @@ import { GROUPING_ZOOM_TRANSITION, MAX_SHOTS_SCATTER, SHOTS_SCATTER_DEGREES, - ContextActionColors, - CONTEXT_ACTION_COLORS, OlympusState, JTACSubState, UnitControlSubState, + ContextActionType, } from "../constants/constants"; import { DataExtractor } from "../server/dataextractor"; import { Weapon } from "../weapon/weapon"; @@ -58,6 +56,7 @@ import { } from "../ui/components/olicons"; import { faExplosion, + faHand, faLocationCrosshairs, faLocationDot, faMapLocation, @@ -69,7 +68,7 @@ import { faXmarksLines, } from "@fortawesome/free-solid-svg-icons"; import { Carrier } from "../mission/carrier"; -import { ContactsUpdatedEvent, HiddenTypesChangedEvent, MapOptionsChangedEvent, UnitDeadEvent, UnitDeselectedEvent, UnitSelectedEvent, UnitUpdatedEvent } from "../events"; +import { ContactsUpdatedEvent, FormationCreationRequestEvent, HiddenTypesChangedEvent, MapOptionsChangedEvent, UnitContextMenuRequestEvent, UnitDeadEvent, UnitDeselectedEvent, UnitExplosionRequestEvent, UnitSelectedEvent, UnitUpdatedEvent } from "../events"; var pathIcon = new Icon({ iconUrl: "/vite/images/markers/marker-icon.png", @@ -828,6 +827,22 @@ export abstract class Unit extends CustomMarker { * */ appendContextActions(contextActionSet: ContextActionSet) { + contextActionSet.addContextAction( + this, + "stop", + "Stop unit", + "Stops the unit", + faHand, + null, + (units: Unit[], _1, _2) => { + getApp().getUnitsManager().clearDestinations(units); + }, + { + executeImmediately: true, + type: ContextActionType.MOVE, + } + ); + contextActionSet.addContextAction( this, "move", @@ -835,11 +850,11 @@ export abstract class Unit extends CustomMarker { "Click on the map to move the units there", faLocationDot, "position", - (units: Unit[], _, targetPosition) => { - getApp().getUnitsManager().clearDestinations(units); - if (targetPosition) getApp().getUnitsManager().addDestination(targetPosition, false, 0, units); + (units: Unit[], _, targetPosition, originalEvent) => { + if (!originalEvent?.ctrlKey) getApp().getUnitsManager().clearDestinations(units); + if (targetPosition) getApp().getUnitsManager().addDestination(targetPosition, getApp().getMap().getOptions().keepRelativePositions, 0, units); }, - { buttonColor: CONTEXT_ACTION_COLORS[ContextActionColors.MOVE] } + { type: ContextActionType.MOVE } ); contextActionSet.addContextAction( @@ -850,9 +865,9 @@ export abstract class Unit extends CustomMarker { faRoute, "position", (units: Unit[], _, targetPosition) => { - if (targetPosition) getApp().getUnitsManager().addDestination(targetPosition, false, 0, units); + if (targetPosition) getApp().getUnitsManager().addDestination(targetPosition, getApp().getMap().getOptions().keepRelativePositions, 0, units); }, - { buttonColor: CONTEXT_ACTION_COLORS[ContextActionColors.MOVE] } + { type: ContextActionType.MOVE } ); contextActionSet.addContextAction( @@ -867,7 +882,7 @@ export abstract class Unit extends CustomMarker { }, { executeImmediately: true, - buttonColor: CONTEXT_ACTION_COLORS[ContextActionColors.DELETE], + type: ContextActionType.DELETE, } ); @@ -880,16 +895,17 @@ export abstract class Unit extends CustomMarker { null, (units: Unit[], _1, _2) => { getApp().setState(OlympusState.UNIT_CONTROL, UnitControlSubState.UNIT_EXPLOSION_MENU) + UnitExplosionRequestEvent.dispatch(units) }, { executeImmediately: true, - buttonColor: CONTEXT_ACTION_COLORS[ContextActionColors.DELETE], + type: ContextActionType.DELETE, } ); - contextActionSet.addDefaultContextAction(this, "default", "Set destination", "", faRoute, null, (units: Unit[], targetUnit, targetPosition) => { + contextActionSet.addDefaultContextAction(this, "default", "Set destination", "", faRoute, null, (units: Unit[], targetUnit, targetPosition, originalEvent) => { if (targetPosition) { - getApp().getUnitsManager().clearDestinations(units); + if (!originalEvent?.ctrlKey) getApp().getUnitsManager().clearDestinations(units); getApp().getUnitsManager().addDestination(targetPosition, false, 0, units); } }); @@ -1175,6 +1191,7 @@ export abstract class Unit extends CustomMarker { clearDestinations() { if (!this.#human) this.#activePath = []; + getApp().getServerManager().addDestination(this.ID, []); } updatePathFromMarkers() { @@ -1393,11 +1410,11 @@ export abstract class Unit extends CustomMarker { #onShortPress(e: LeafletMouseEvent) { console.log(`Short press on ${this.getUnitName()}`); - if (getApp().getState() === OlympusState.IDLE || e.originalEvent.ctrlKey) { + if (getApp().getState() !== OlympusState.UNIT_CONTROL || e.originalEvent.ctrlKey) { if (!e.originalEvent.ctrlKey) getApp().getUnitsManager().deselectAllUnits(); this.setSelected(!this.getSelected()); } else if (getApp().getState() === OlympusState.UNIT_CONTROL) { - if (getApp().getMap().getContextAction()) getApp().getMap().executeContextAction(this, null); + if (getApp().getMap().getContextAction()) getApp().getMap().executeContextAction(this, null, e.originalEvent); else { getApp().getUnitsManager().deselectAllUnits(); this.setSelected(!this.getSelected()); @@ -1413,6 +1430,7 @@ export abstract class Unit extends CustomMarker { if (e.originalEvent.button === 2) { getApp().setState(OlympusState.UNIT_CONTROL, UnitControlSubState.UNIT_CONTEXT_MENU) + UnitContextMenuRequestEvent.dispatch(this); } } @@ -1837,7 +1855,7 @@ export abstract class AirUnit extends Unit { (units: Unit[]) => { getApp().getUnitsManager().refuel(units); }, - { executeImmediately: true, buttonColor: CONTEXT_ACTION_COLORS[ContextActionColors.ADMIN] } + { executeImmediately: true, type: ContextActionType.ADMIN } ); contextActionSet.addContextAction( this, @@ -1849,7 +1867,7 @@ export abstract class AirUnit extends Unit { (units: Unit[]) => { getApp().getMap().centerOnUnit(units[0]); }, - { executeImmediately: true, buttonColor: CONTEXT_ACTION_COLORS[ContextActionColors.OTHER] } + { executeImmediately: true, type: ContextActionType.OTHER } ); /* Context actions that require a target unit */ @@ -1863,7 +1881,7 @@ export abstract class AirUnit extends Unit { (units: Unit[], targetUnit: Unit | null, _) => { if (targetUnit) getApp().getUnitsManager().attackUnit(targetUnit.ID, units); }, - { buttonColor: CONTEXT_ACTION_COLORS[ContextActionColors.ENGAGE] } + { type: ContextActionType.ENGAGE } ); contextActionSet.addContextAction( @@ -1875,17 +1893,11 @@ export abstract class AirUnit extends Unit { "unit", (units: Unit[], targetUnit: Unit | null, _) => { if (targetUnit) { - document.dispatchEvent( - new CustomEvent("showFormationMenu", { - detail: { - leader: targetUnit, - wingmen: units.filter((unit) => unit !== targetUnit), - }, - }) - ); + getApp().setState(OlympusState.UNIT_CONTROL, UnitControlSubState.FORMATION); + FormationCreationRequestEvent.dispatch(targetUnit, units.filter((unit) => unit !== targetUnit)) } }, - { buttonColor: CONTEXT_ACTION_COLORS[ContextActionColors.ADMIN] } + { type: ContextActionType.ADMIN } ); if (this.canTargetPoint()) { @@ -1898,9 +1910,9 @@ export abstract class AirUnit extends Unit { faLocationCrosshairs, "position", (units: Unit[], _, targetPosition: LatLng | null) => { - if (targetPosition) getApp().getUnitsManager().bombPoint(targetPosition, units); + if (targetPosition) getApp().getUnitsManager().bombPoint(targetPosition, getApp().getMap().getOptions().keepRelativePositions, 0, units); }, - { buttonColor: CONTEXT_ACTION_COLORS[ContextActionColors.ENGAGE] } + { type: ContextActionType.ENGAGE } ); contextActionSet.addContextAction( @@ -1911,9 +1923,9 @@ export abstract class AirUnit extends Unit { faXmarksLines, "position", (units: Unit[], _, targetPosition: LatLng | null) => { - if (targetPosition) getApp().getUnitsManager().carpetBomb(targetPosition, units); + if (targetPosition) getApp().getUnitsManager().carpetBomb(targetPosition, getApp().getMap().getOptions().keepRelativePositions, 0, units); }, - { buttonColor: CONTEXT_ACTION_COLORS[ContextActionColors.ENGAGE] } + { type: ContextActionType.ENGAGE } ); } @@ -1927,7 +1939,7 @@ export abstract class AirUnit extends Unit { (units: Unit[], _, targetPosition: LatLng | null) => { if (targetPosition) getApp().getUnitsManager().landAt(targetPosition, units); }, - { buttonColor: CONTEXT_ACTION_COLORS[ContextActionColors.ADMIN] } + { type: ContextActionType.ADMIN } ); } } @@ -1973,9 +1985,9 @@ export class Helicopter extends AirUnit { olButtonsContextLandAtPoint, "position", (units: Unit[], _, targetPosition: LatLng | null) => { - if (targetPosition) getApp().getUnitsManager().landAtPoint(targetPosition, units); + if (targetPosition) getApp().getUnitsManager().landAtPoint(targetPosition, getApp().getMap().getOptions().keepRelativePositions, 0, units); }, - { buttonColor: CONTEXT_ACTION_COLORS[ContextActionColors.ADMIN] } + { type: ContextActionType.ADMIN } ); } @@ -2024,7 +2036,7 @@ export class GroundUnit extends Unit { (units: Unit[], _1, _2) => { getApp().getUnitsManager().createGroup(units); }, - { executeImmediately: true, buttonColor: CONTEXT_ACTION_COLORS[ContextActionColors.OTHER] } + { executeImmediately: true, type: ContextActionType.OTHER } ); contextActionSet.addContextAction( this, @@ -2036,7 +2048,7 @@ export class GroundUnit extends Unit { (units: Unit[]) => { getApp().getMap().centerOnUnit(units[0]); }, - { executeImmediately: true, buttonColor: CONTEXT_ACTION_COLORS[ContextActionColors.OTHER] } + { executeImmediately: true, type: ContextActionType.OTHER } ); /* Context actions that require a target unit */ @@ -2050,7 +2062,7 @@ export class GroundUnit extends Unit { (units: Unit[], targetUnit: Unit | null, _) => { if (targetUnit) getApp().getUnitsManager().attackUnit(targetUnit.ID, units); }, - { buttonColor: CONTEXT_ACTION_COLORS[ContextActionColors.ENGAGE] } + { type: ContextActionType.ENGAGE } ); /* Context actions that require a target position */ @@ -2063,9 +2075,9 @@ export class GroundUnit extends Unit { faLocationCrosshairs, "position", (units: Unit[], _, targetPosition: LatLng | null) => { - if (targetPosition) getApp().getUnitsManager().fireAtArea(targetPosition, units); + if (targetPosition) getApp().getUnitsManager().fireAtArea(targetPosition, getApp().getMap().getOptions().keepRelativePositions, 0, units); }, - { buttonColor: CONTEXT_ACTION_COLORS[ContextActionColors.ENGAGE] } + { type: ContextActionType.ENGAGE } ); contextActionSet.addContextAction( this, @@ -2075,9 +2087,9 @@ export class GroundUnit extends Unit { olButtonsContextSimulateFireFight, "position", (units: Unit[], _, targetPosition: LatLng | null) => { - if (targetPosition) getApp().getUnitsManager().simulateFireFight(targetPosition, units); + if (targetPosition) getApp().getUnitsManager().simulateFireFight(targetPosition, getApp().getMap().getOptions().keepRelativePositions, 0, units); }, - { buttonColor: CONTEXT_ACTION_COLORS[ContextActionColors.ADMIN] } + { type: ContextActionType.ADMIN } ); } } @@ -2148,7 +2160,7 @@ export class NavyUnit extends Unit { (units: Unit[], _1, _2) => { getApp().getUnitsManager().createGroup(units); }, - { executeImmediately: true, buttonColor: CONTEXT_ACTION_COLORS[ContextActionColors.OTHER] } + { executeImmediately: true, type: ContextActionType.OTHER } ); contextActionSet.addContextAction( this, @@ -2160,7 +2172,7 @@ export class NavyUnit extends Unit { (units: Unit[]) => { getApp().getMap().centerOnUnit(units[0]); }, - { executeImmediately: true, buttonColor: CONTEXT_ACTION_COLORS[ContextActionColors.OTHER] } + { executeImmediately: true, type: ContextActionType.OTHER } ); /* Context actions that require a target unit */ @@ -2174,7 +2186,7 @@ export class NavyUnit extends Unit { (units: Unit[], targetUnit: Unit | null, _) => { if (targetUnit) getApp().getUnitsManager().attackUnit(targetUnit.ID, units); }, - { buttonColor: CONTEXT_ACTION_COLORS[ContextActionColors.ENGAGE] } + { type: ContextActionType.ENGAGE } ); /* Context actions that require a target position */ @@ -2186,9 +2198,9 @@ export class NavyUnit extends Unit { faLocationCrosshairs, "position", (units: Unit[], _, targetPosition: LatLng | null) => { - if (targetPosition) getApp().getUnitsManager().fireAtArea(targetPosition, units); + if (targetPosition) getApp().getUnitsManager().fireAtArea(targetPosition, getApp().getMap().getOptions().keepRelativePositions, 0, units); }, - { buttonColor: CONTEXT_ACTION_COLORS[ContextActionColors.ENGAGE] } + { type: ContextActionType.ENGAGE } ); } diff --git a/frontend/react/src/unit/unitsmanager.ts b/frontend/react/src/unit/unitsmanager.ts index 5378b31e..52a55639 100644 --- a/frontend/react/src/unit/unitsmanager.ts +++ b/frontend/react/src/unit/unitsmanager.ts @@ -11,10 +11,6 @@ import { mToFt, mercatorToLatLng, msToKnots, - polyContains, - polygonArea, - randomPointInPoly, - randomUnitBlueprint, } from "../other/utils"; import { CoalitionPolygon } from "../map/coalitionarea/coalitionpolygon"; import { DELETE_CYCLE_TIME, DELETE_SLOW_THRESHOLD, DataIndexes, GAME_MASTER, IADSDensities, OlympusState, UnitControlSubState } from "../constants/constants"; @@ -33,6 +29,7 @@ import { ContextActionSet } from "./contextactionset"; import { CommandModeOptionsChangedEvent, ContactsUpdatedEvent, + InfoPopupEvent, SelectedUnitsChangedEvent, SelectionClearedEvent, UnitDatabaseLoadedEvent, @@ -343,9 +340,7 @@ export class UnitsManager { addDestination(latlng: L.LatLng, mantainRelativePosition: boolean, rotation: number, units: Unit[] | null = null) { if (units === null) units = this.getSelectedUnits(); - units = units.filter((unit) => { - return !unit.getHuman(); - }); + units = units.filter((unit) => !unit.getHuman()); let callback = (units) => { /* Compute the destination for each unit. If mantainRelativePosition is true, compute the destination so to hold the relative positions */ @@ -380,11 +375,9 @@ export class UnitsManager { */ clearDestinations(units: Unit[] | null = null) { if (units === null) units = this.getSelectedUnits(); - units = units.filter((unit) => { - return !unit.getHuman(); - }); + units = units.filter((unit) => !unit.getHuman()); - let callback = (units) => { + let callback = (units: Unit[]) => { for (let idx in units) { const unit = units[idx]; if (unit.getState() === "follow") { @@ -408,9 +401,7 @@ export class UnitsManager { */ landAt(latlng: LatLng, units: Unit[] | null = null) { if (units === null) units = this.getSelectedUnits(); - units = units.filter((unit) => { - return !unit.getHuman(); - }); + units = units.filter((unit) => !unit.getHuman()); let callback = (units) => { units.forEach((unit: Unit) => unit.landAt(latlng)); @@ -430,9 +421,7 @@ export class UnitsManager { */ changeSpeed(speedChange: string, units: Unit[] | null = null) { if (units === null) units = this.getSelectedUnits(); - units = units.filter((unit) => { - return !unit.getHuman(); - }); + units = units.filter((unit) => !unit.getHuman()); let callback = (units) => { units.forEach((unit: Unit) => unit.changeSpeed(speedChange)); @@ -450,9 +439,7 @@ export class UnitsManager { */ changeAltitude(altitudeChange: string, units: Unit[] | null = null) { if (units === null) units = this.getSelectedUnits(); - units = units.filter((unit) => { - return !unit.getHuman(); - }); + units = units.filter((unit) => !unit.getHuman()); let callback = (units) => { units.forEach((unit: Unit) => unit.changeAltitude(altitudeChange)); @@ -470,9 +457,7 @@ export class UnitsManager { */ setSpeed(speed: number, units: Unit[] | null = null) { if (units === null) units = this.getSelectedUnits(); - units = units.filter((unit) => { - return !unit.getHuman(); - }); + units = units.filter((unit) => !unit.getHuman()); let callback = (units) => { units.forEach((unit: Unit) => unit.setSpeed(speed)); @@ -491,9 +476,7 @@ export class UnitsManager { */ setSpeedType(speedType: string, units: Unit[] | null = null) { if (units === null) units = this.getSelectedUnits(); - units = units.filter((unit) => { - return !unit.getHuman(); - }); + units = units.filter((unit) => !unit.getHuman()); let callback = (units) => { units.forEach((unit: Unit) => unit.setSpeedType(speedType)); @@ -512,9 +495,7 @@ export class UnitsManager { */ setAltitude(altitude: number, units: Unit[] | null = null) { if (units === null) units = this.getSelectedUnits(); - units = units.filter((unit) => { - return !unit.getHuman(); - }); + units = units.filter((unit) => !unit.getHuman()); let callback = (units) => { units.forEach((unit: Unit) => unit.setAltitude(altitude)); @@ -533,9 +514,7 @@ export class UnitsManager { */ setAltitudeType(altitudeType: string, units: Unit[] | null = null) { if (units === null) units = this.getSelectedUnits(); - units = units.filter((unit) => { - return !unit.getHuman(); - }); + units = units.filter((unit) => !unit.getHuman()); let callback = (units) => { units.forEach((unit: Unit) => unit.setAltitudeType(altitudeType)); @@ -554,9 +533,7 @@ export class UnitsManager { */ setROE(ROE: string, units: Unit[] | null = null) { if (units === null) units = this.getSelectedUnits(); - units = units.filter((unit) => { - return !unit.getHuman(); - }); + units = units.filter((unit) => !unit.getHuman()); let callback = (units) => { units.forEach((unit: Unit) => unit.setROE(ROE)); @@ -575,9 +552,7 @@ export class UnitsManager { */ setReactionToThreat(reactionToThreat: string, units: Unit[] | null = null) { if (units === null) units = this.getSelectedUnits(); - units = units.filter((unit) => { - return !unit.getHuman(); - }); + units = units.filter((unit) => !unit.getHuman()); let callback = (units) => { units.forEach((unit: Unit) => unit.setReactionToThreat(reactionToThreat)); @@ -596,9 +571,7 @@ export class UnitsManager { */ setEmissionsCountermeasures(emissionCountermeasure: string, units: Unit[] | null = null) { if (units === null) units = this.getSelectedUnits(); - units = units.filter((unit) => { - return !unit.getHuman(); - }); + units = units.filter((unit) => !unit.getHuman()); let callback = (units) => { units.forEach((unit: Unit) => unit.setEmissionsCountermeasures(emissionCountermeasure)); @@ -617,9 +590,7 @@ export class UnitsManager { */ setOnOff(onOff: boolean, units: Unit[] | null = null) { if (units === null) units = this.getSelectedUnits(); - units = units.filter((unit) => { - return !unit.getHuman(); - }); + units = units.filter((unit) => !unit.getHuman()); let callback = (units) => { units.forEach((unit: Unit) => unit.setOnOff(onOff)); @@ -638,9 +609,7 @@ export class UnitsManager { */ setFollowRoads(followRoads: boolean, units: Unit[] | null = null) { if (units === null) units = this.getSelectedUnits(); - units = units.filter((unit) => { - return !unit.getHuman(); - }); + units = units.filter((unit) => !unit.getHuman()); let callback = (units) => { units.forEach((unit: Unit) => unit.setFollowRoads(followRoads)); @@ -660,9 +629,7 @@ export class UnitsManager { setOperateAs(operateAsBool: boolean, units: Unit[] | null = null) { var operateAs = operateAsBool ? "blue" : "red"; if (units === null) units = this.getSelectedUnits(); - units = units.filter((unit) => { - return !unit.getHuman(); - }); + units = units.filter((unit) => !unit.getHuman()); let callback = (units) => { units.forEach((unit: Unit) => unit.setOperateAs(operateAs)); @@ -681,9 +648,7 @@ export class UnitsManager { */ attackUnit(ID: number, units: Unit[] | null = null) { if (units === null) units = this.getSelectedUnits(); - units = units.filter((unit) => { - return !unit.getHuman(); - }); + units = units.filter((unit) => !unit.getHuman()); let callback = (units) => { units.forEach((unit: Unit) => unit.attackUnit(ID)); @@ -701,9 +666,7 @@ export class UnitsManager { refuel(units: Unit[] | null = null) { if (units === null) units = this.getSelectedUnits(); - units = units.filter((unit) => { - return !unit.getHuman(); - }); + units = units.filter((unit) => !unit.getHuman()); let callback = (units) => { units.forEach((unit: Unit) => unit.refuel()); @@ -724,9 +687,7 @@ export class UnitsManager { */ followUnit(ID: number, offset?: { x: number; y: number; z: number }, formation?: string, units: Unit[] | null = null) { if (units === null) units = this.getSelectedUnits(); - units = units.filter((unit) => { - return !unit.getHuman(); - }); + units = units.filter((unit) => !unit.getHuman()); let callback = (units) => { if (offset == undefined) { @@ -759,11 +720,10 @@ export class UnitsManager { } else offset = undefined; } - if (getApp().getMap().getOptions().protectDCSUnits && !units.every((unit) => unit.isControlledByOlympus())){ + if (getApp().getMap().getOptions().protectDCSUnits && !units.every((unit) => unit.isControlledByOlympus())) { getApp().setState(OlympusState.UNIT_CONTROL, UnitControlSubState.PROTECTION); this.#protectionCallback = callback; - } - else callback(units); + } else callback(units); }; var count = 1; var xr = 0; @@ -813,14 +773,19 @@ export class UnitsManager { * @param latlng Location to bomb * @param units (Optional) Array of units to apply the control to. If not provided, the operation will be completed on all selected units. */ - bombPoint(latlng: LatLng, units: Unit[] | null = null) { + bombPoint(latlng: LatLng, mantainRelativePosition: boolean, rotation: number = 0, units: Unit[] | null = null) { if (units === null) units = this.getSelectedUnits(); - units = units.filter((unit) => { - return !unit.getHuman(); - }); + units = units.filter((unit) => !unit.getHuman()); let callback = (units) => { - units.forEach((unit: Unit) => unit.bombPoint(latlng)); + /* Compute the target for each unit. If mantainRelativePosition is true, compute the target so to hold the relative positions */ + var unitTargets: { [key: number]: LatLng } = {}; + if (mantainRelativePosition) unitTargets = this.computeGroupDestination(latlng, rotation); + else + units.forEach((unit: Unit) => { + unitTargets[unit.ID] = latlng; + }); + units.forEach((unit: Unit) => unit.bombPoint(unitTargets[unit.ID])); this.#showActionMessage(units, `unit bombing point`); }; @@ -834,14 +799,19 @@ export class UnitsManager { * @param latlng Location to bomb * @param units (Optional) Array of units to apply the control to. If not provided, the operation will be completed on all selected units. */ - carpetBomb(latlng: LatLng, units: Unit[] | null = null) { + carpetBomb(latlng: LatLng, mantainRelativePosition: boolean, rotation: number = 0, units: Unit[] | null = null) { if (units === null) units = this.getSelectedUnits(); - units = units.filter((unit) => { - return !unit.getHuman(); - }); + units = units.filter((unit) => !unit.getHuman()); let callback = (units) => { - units.forEach((unit: Unit) => unit.carpetBomb(latlng)); + /* Compute the target for each unit. If mantainRelativePosition is true, compute the target so to hold the relative positions */ + var unitTargets: { [key: number]: LatLng } = {}; + if (mantainRelativePosition) unitTargets = this.computeGroupDestination(latlng, rotation); + else + units.forEach((unit: Unit) => { + unitTargets[unit.ID] = latlng; + }); + units.forEach((unit: Unit) => unit.carpetBomb(unitTargets[unit.ID])); this.#showActionMessage(units, `unit carpet bombing point`); }; @@ -855,14 +825,19 @@ export class UnitsManager { * @param latlng Location to fire at * @param units (Optional) Array of units to apply the control to. If not provided, the operation will be completed on all selected units. */ - fireAtArea(latlng: LatLng, units: Unit[] | null = null) { + fireAtArea(latlng: LatLng, mantainRelativePosition: boolean, rotation: number = 0, units: Unit[] | null = null) { if (units === null) units = this.getSelectedUnits(); - units = units.filter((unit) => { - return !unit.getHuman(); - }); + units = units.filter((unit) => !unit.getHuman()); let callback = (units) => { - units.forEach((unit: Unit) => unit.fireAtArea(latlng)); + /* Compute the target for each unit. If mantainRelativePosition is true, compute the target so to hold the relative positions */ + var unitTargets: { [key: number]: LatLng } = {}; + if (mantainRelativePosition) unitTargets = this.computeGroupDestination(latlng, rotation); + else + units.forEach((unit: Unit) => { + unitTargets[unit.ID] = latlng; + }); + units.forEach((unit: Unit) => unit.fireAtArea(unitTargets[unit.ID])); this.#showActionMessage(units, `unit firing at area`); }; @@ -876,26 +851,34 @@ export class UnitsManager { * @param latlng Location to fire at * @param units (Optional) Array of units to apply the control to. If not provided, the operation will be completed on all selected units. */ - simulateFireFight(latlng: LatLng, units: Unit[] | null = null) { - // TODO - // if (units === null) - // units = this.getSelectedUnits(); - // units = units.filter((unit => {!unit.getHuman()})); - // - // let callback = (units) => { - // - // getGroundElevation(latlng, (response: string) => { - // var groundElevation: number | null = null; - // try { - // groundElevation = parseFloat(response); - // } catch { - // console.warn("Simulate fire fight: could not retrieve ground elevation"); - // } - // - //if (getApp().getMap().getOptions().protectDCSUnits && !units.every(unit => unit.isControlledByOlympus())) - // this.showProtectedUnitsPopup(units.filter(unit => unit.isControlledByDCS()).length, callback);} units?.forEach((unit: Unit) => unit.simulateFireFight(latlng, groundElevation)); - // }); - // this.#showActionMessage(units, `unit simulating fire fight`); + simulateFireFight(latlng: LatLng, mantainRelativePosition: boolean, rotation: number = 0, units: Unit[] | null = null) { + if (units === null) units = this.getSelectedUnits(); + units = units.filter((unit) => !unit.getHuman()); + + let callback = (units) => { + getGroundElevation(latlng, (response: string) => { + var groundElevation: number | null = null; + try { + groundElevation = parseFloat(response); + /* Compute the target for each unit. If mantainRelativePosition is true, compute the target so to hold the relative positions */ + var unitTargets: { [key: number]: LatLng } = {}; + if (mantainRelativePosition) unitTargets = this.computeGroupDestination(latlng, rotation); + else + units.forEach((unit: Unit) => { + unitTargets[unit.ID] = latlng; + }); + units.forEach((unit: Unit) => unit.simulateFireFight(unitTargets[unit.ID], groundElevation)); + this.#showActionMessage(units, `simulating fire fight`); + } catch { + console.warn("Simulate fire fight: could not retrieve ground elevation"); + } + }); + }; + + if (getApp().getMap().getOptions().protectDCSUnits && !units.every((unit) => unit.isControlledByOlympus())) { + getApp().setState(OlympusState.UNIT_CONTROL, UnitControlSubState.PROTECTION); + this.#protectionCallback = callback; + } else callback(units); } /** Instruct units to enter into scenic AAA mode. Units will shoot in the air without aiming @@ -903,9 +886,7 @@ export class UnitsManager { */ scenicAAA(units: Unit[] | null = null) { if (units === null) units = this.getSelectedUnits(); - units = units.filter((unit) => { - return !unit.getHuman(); - }); + units = units.filter((unit) => !unit.getHuman()); let callback = (units) => { units.forEach((unit: Unit) => unit.scenicAAA()); @@ -922,9 +903,7 @@ export class UnitsManager { */ missOnPurpose(units: Unit[] | null = null) { if (units === null) units = this.getSelectedUnits(); - units = units.filter((unit) => { - return !unit.getHuman(); - }); + units = units.filter((unit) => !unit.getHuman()); let callback = (units) => { units.forEach((unit: Unit) => unit.missOnPurpose()); @@ -941,14 +920,19 @@ export class UnitsManager { * @param latlng Point where to land * @param units (Optional) Array of units to apply the control to. If not provided, the operation will be completed on all selected units. */ - landAtPoint(latlng: LatLng, units: Unit[] | null = null) { + landAtPoint(latlng: LatLng, mantainRelativePosition: boolean, rotation: number = 0, units: Unit[] | null = null) { if (units === null) units = this.getSelectedUnits(); - units = units.filter((unit) => { - return !unit.getHuman(); - }); + units = units.filter((unit) => !unit.getHuman()); let callback = (units) => { - units.forEach((unit: Unit) => unit.landAtPoint(latlng)); + /* Compute the target for each unit. If mantainRelativePosition is true, compute the target so to hold the relative positions */ + var unitTargets: { [key: number]: LatLng } = {}; + if (mantainRelativePosition) unitTargets = this.computeGroupDestination(latlng, rotation); + else + units.forEach((unit: Unit) => { + unitTargets[unit.ID] = latlng; + }); + units.forEach((unit: Unit) => unit.landAtPoint(unitTargets[unit.ID])); this.#showActionMessage(units, `unit landing at point`); }; @@ -964,9 +948,7 @@ export class UnitsManager { */ setShotsScatter(shotsScatter: number, units: Unit[] | null = null) { if (units === null) units = this.getSelectedUnits(); - units = units.filter((unit) => { - return !unit.getHuman(); - }); + units = units.filter((unit) => !unit.getHuman()); let callback = (units) => { units.forEach((unit: Unit) => unit.setShotsScatter(shotsScatter)); @@ -985,9 +967,7 @@ export class UnitsManager { */ setShotsIntensity(shotsIntensity: number, units: Unit[] | null = null) { if (units === null) units = this.getSelectedUnits(); - units = units.filter((unit) => { - return !unit.getHuman(); - }); + units = units.filter((unit) => !unit.getHuman()); let callback = (units) => { units.forEach((unit: Unit) => unit.setShotsIntensity(shotsIntensity)); @@ -1022,9 +1002,7 @@ export class UnitsManager { */ createGroup(units: Unit[] | null = null) { if (units === null) units = this.getSelectedUnits(); - units = units.filter((unit) => { - return !unit.getHuman(); - }); + units = units.filter((unit) => !unit.getHuman()); let callback = (units) => { if (this.getUnitsCategories(units).length == 1) { @@ -1033,14 +1011,14 @@ export class UnitsManager { getApp().getServerManager().cloneUnits(unitsData, true, 0 /* No spawn points, we delete the original units */); this.#showActionMessage(units, `created a group`); } else { - //(getApp().getPopupsManager().get("infoPopup") as Popup).setText(`Groups can only be created from units of the same category`); + getApp().addInfoMessage(`Groups can only be created from units of the same category`); } - - if (getApp().getMap().getOptions().protectDCSUnits && !units.every((unit) => unit.isControlledByOlympus())) { - getApp().setState(OlympusState.UNIT_CONTROL, UnitControlSubState.PROTECTION); - this.#protectionCallback = callback; - } else callback(units); }; + + if (getApp().getMap().getOptions().protectDCSUnits && !units.every((unit) => unit.isControlledByOlympus())) { + getApp().setState(OlympusState.UNIT_CONTROL, UnitControlSubState.PROTECTION); + this.#protectionCallback = callback; + } else callback(units); } /** Set the hotgroup for the selected units. It will be the only hotgroup of the unit @@ -1080,11 +1058,10 @@ export class UnitsManager { this.#showActionMessage(units as Unit[], `deleted`); }; - if ((getApp().getMap().getOptions().protectDCSUnits && !units.every((unit) => unit.isControlledByOlympus())) || units.find((unit) => unit.getHuman())){ + if ((getApp().getMap().getOptions().protectDCSUnits && !units.every((unit) => unit.isControlledByOlympus())) || units.find((unit) => unit.getHuman())) { getApp().setState(OlympusState.UNIT_CONTROL, UnitControlSubState.PROTECTION); this.#protectionCallback = callback; - } - else callback(units); + } else callback(units); } /** Compute the destinations of every unit in the selected units. This function preserves the relative positions of the units, and rotates the whole formation by rotation. @@ -1098,9 +1075,7 @@ export class UnitsManager { // TODO handle protected units if (units === null) units = this.getSelectedUnits(); - units = units.filter((unit) => { - return !unit.getHuman(); - }); + units = units.filter((unit) => !unit.getHuman()); if (units.length === 0) return {}; @@ -1159,7 +1134,7 @@ export class UnitsManager { }) ) ); /* Can be applied to humans too */ - //(getApp().getPopupsManager().get("infoPopup") as Popup).setText(`${this.#copiedUnits.length} units copied`); + getApp().addInfoMessage(`${this.#copiedUnits.length} units copied`); } /*********************** Unit manipulation functions ************************/ @@ -1173,17 +1148,17 @@ export class UnitsManager { /* If spawns are restricted, check that the user has the necessary spawn points */ if (getApp().getMissionManager().getCommandModeOptions().commandMode != GAME_MASTER) { if (getApp().getMissionManager().getCommandModeOptions().restrictSpawns && getApp().getMissionManager().getRemainingSetupTime() < 0) { - //(getApp().getPopupsManager().get("infoPopup") as Popup).setText(`Units can be pasted only during SETUP phase`); + getApp().addInfoMessage(`Units can be pasted only during SETUP phase`); return false; } this.#copiedUnits.forEach((unit: UnitData) => { - let unitSpawnPoints = getUnitDatabaseByCategory(unit.category)?.getSpawnPointsByName(unit.name); + let unitSpawnPoints = this.#unitDatabase.getSpawnPointsByName(unit.name); if (unitSpawnPoints !== undefined) spawnPoints += unitSpawnPoints; }); if (spawnPoints > getApp().getMissionManager().getAvailableSpawnPoints()) { - //(getApp().getPopupsManager().get("infoPopup") as Popup).setText("Not enough spawn points available!"); + getApp().addInfoMessage("Not enough spawn points available!"); return false; } } @@ -1229,9 +1204,9 @@ export class UnitsManager { } }); } - //(getApp().getPopupsManager().get("infoPopup") as Popup).setText(`${this.#copiedUnits.length} units pasted`); + getApp().addInfoMessage(`${this.#copiedUnits.length} units pasted`); } else { - //(getApp().getPopupsManager().get("infoPopup") as Popup).setText("No units copied!"); + getApp().addInfoMessage("No units copied!"); } } @@ -1284,14 +1259,14 @@ export class UnitsManager { /* Get a random blueprint depending on the selected parameters and spawn the unit */ let unitBlueprint: UnitBlueprint | null; if (forceCoalition) - unitBlueprint = randomUnitBlueprint(groundUnitDatabase, { + unitBlueprint = this.#unitDatabase.getRandomUnit({ type: type, eras: activeEras, ranges: activeRanges, coalition: coalitionArea.getCoalition(), }); else - unitBlueprint = randomUnitBlueprint(groundUnitDatabase, { + unitBlueprint = this.#unitDatabase.getRandomUnit({ type: type, eras: activeEras, ranges: activeRanges, @@ -1337,14 +1312,14 @@ export class UnitsManager { /* Get a random blueprint depending on the selected parameters and spawn the unit */ let unitBlueprint: UnitBlueprint | null; if (forceCoalition) - unitBlueprint = randomUnitBlueprint(groundUnitDatabase, { + unitBlueprint = this.#unitDatabase.getRandomUnit({ type: type, eras: activeEras, ranges: activeRanges, coalition: coalitionArea.getCoalition(), }); else - unitBlueprint = randomUnitBlueprint(groundUnitDatabase, { + unitBlueprint = this.#unitDatabase.getRandomUnit({ type: type, eras: activeEras, ranges: activeRanges, @@ -1418,38 +1393,38 @@ export class UnitsManager { if (category === "aircraft") { if (airbase == "" && spawnsRestricted) { - //(getApp().getPopupsManager().get("infoPopup") as Popup).setText("Aircrafts can be air spawned during the SETUP phase only"); + getApp().addInfoMessage("Aircrafts can be air spawned during the SETUP phase only"); return false; } spawnPoints = units.reduce((points: number, unit: UnitSpawnTable) => { - return points + this.getDatabase().getSpawnPointsByName(unit.unitType) + return points + this.getDatabase().getSpawnPointsByName(unit.unitType); }, 0); spawnFunction = () => getApp().getServerManager().spawnAircrafts(units, coalition, airbase, country, immediate, spawnPoints, callback); } else if (category === "helicopter") { if (airbase == "" && spawnsRestricted) { - //(getApp().getPopupsManager().get("infoPopup") as Popup).setText("Helicopters can be air spawned during the SETUP phase only"); + getApp().addInfoMessage("Helicopters can be air spawned during the SETUP phase only"); return false; } spawnPoints = units.reduce((points: number, unit: UnitSpawnTable) => { - return points + this.getDatabase().getSpawnPointsByName(unit.unitType) + return points + this.getDatabase().getSpawnPointsByName(unit.unitType); }, 0); spawnFunction = () => getApp().getServerManager().spawnHelicopters(units, coalition, airbase, country, immediate, spawnPoints, callback); } else if (category === "groundunit") { if (spawnsRestricted) { - //(getApp().getPopupsManager().get("infoPopup") as Popup).setText("Ground units can be spawned during the SETUP phase only"); + getApp().addInfoMessage("Ground units can be spawned during the SETUP phase only"); return false; } spawnPoints = units.reduce((points: number, unit: UnitSpawnTable) => { - return points + this.getDatabase().getSpawnPointsByName(unit.unitType) + return points + this.getDatabase().getSpawnPointsByName(unit.unitType); }, 0); spawnFunction = () => getApp().getServerManager().spawnGroundUnits(units, coalition, country, immediate, spawnPoints, callback); } else if (category === "navyunit") { if (spawnsRestricted) { - //(getApp().getPopupsManager().get("infoPopup") as Popup).setText("Navy units can be spawned during the SETUP phase only"); + getApp().addInfoMessage("Navy units can be spawned during the SETUP phase only"); return false; } spawnPoints = units.reduce((points: number, unit: UnitSpawnTable) => { - return points + this.getDatabase().getSpawnPointsByName(unit.unitType) + return points + this.getDatabase().getSpawnPointsByName(unit.unitType); }, 0); spawnFunction = () => getApp().getServerManager().spawnNavyUnits(units, coalition, country, immediate, spawnPoints, callback); } @@ -1459,7 +1434,7 @@ export class UnitsManager { spawnFunction(); return true; } else { - //(getApp().getPopupsManager().get("infoPopup") as Popup).setText("Not enough spawn points available!"); + getApp().addInfoMessage("Not enough spawn points available!"); return false; } } @@ -1527,10 +1502,8 @@ export class UnitsManager { } #showActionMessage(units: Unit[], message: string) { - //if (units.length == 1) - //(getApp().getPopupsManager().get("infoPopup") as Popup).setText(`${units[0].getUnitName()} ${message}`); - //else if (units.length > 1) - //(getApp().getPopupsManager().get("infoPopup") as Popup).setText(`${units[0].getUnitName()} and ${units.length - 1} other units ${message}`); + if (units.length == 1) getApp().addInfoMessage(`${units[0].getUnitName()} ${message}`); + else if (units.length > 1) getApp().addInfoMessage(`${units[0].getUnitName()} and ${units.length - 1} other units ${message}`); } #showSlowDeleteDialog(units: Unit[]) { @@ -1562,15 +1535,15 @@ export class UnitsManager { } #showNumberOfSelectedProtectedUnits() { - const map = getApp().getMap(); const units = this.getSelectedUnits(); const numSelectedUnits = units.length; - //const numProtectedUnits = units.filter((unit: Unit) => map.getIsUnitProtected(unit)).length; + const numProtectedUnits = units.filter((unit: Unit) => !unit.isControlledByOlympus() && !unit.getHuman()).length; + const numHumanUnits = units.filter((unit: Unit) => unit.getHuman()).length; - //if (numProtectedUnits === 1 && numSelectedUnits === numProtectedUnits) - //(getApp().getPopupsManager().get("infoPopup") as Popup).setText(`Notice: unit is protected`); - - //if (numProtectedUnits > 1) - //(getApp().getPopupsManager().get("infoPopup") as Popup).setText(`Notice: selection contains ${numProtectedUnits} protected units.`); + if (getApp().getMap().getOptions().protectDCSUnits && numProtectedUnits === 1 && numSelectedUnits === numProtectedUnits) + getApp().addInfoMessage(`Notice: unit is protected`); + if (getApp().getMap().getOptions().protectDCSUnits && numProtectedUnits > 1) + getApp().addInfoMessage(`Notice: selection contains ${numProtectedUnits} protected units.`); + if (numHumanUnits) getApp().addInfoMessage(`Notice: selection contains ${numHumanUnits} human units.`); } }