diff --git a/frontend/react/.vscode/launch.json b/frontend/react/.vscode/launch.json index 1f07e0f0..a461c2f9 100644 --- a/frontend/react/.vscode/launch.json +++ b/frontend/react/.vscode/launch.json @@ -10,7 +10,11 @@ "name": "Launch Chrome against localhost", "url": "http://localhost:3000/vite/", "webRoot": "${workspaceFolder}", - "preLaunchTask": "npm: dev" - } + "preLaunchTask": "npm: dev", + "resolveSourceMapLocations": [ + "${workspaceFolder}/**", + "!**/node_modules/**" + ] + }, ] } \ No newline at end of file diff --git a/frontend/react/src/map/map.ts b/frontend/react/src/map/map.ts index 3b7f01ec..993683be 100644 --- a/frontend/react/src/map/map.ts +++ b/frontend/react/src/map/map.ts @@ -3,7 +3,7 @@ import { getApp } from "../olympusapp"; import { BoxSelect } from "./boxselect"; import { Airbase } from "../mission/airbase"; import { Unit } from "../unit/unit"; -import { areaContains, deg2rad, getFunctionArguments, getGroundElevation } from "../other/utils"; +import { areaContains, deg2rad, getGroundElevation } from "../other/utils"; import { TemporaryUnitMarker } from "./markers/temporaryunitmarker"; import { ClickableMiniMap } from "./clickableminimap"; import { @@ -24,7 +24,7 @@ import { import { CoalitionPolygon } from "./coalitionarea/coalitionpolygon"; import { MapHiddenTypes, MapOptions } from "../types/types"; import { SpawnRequestTable } from "../interfaces"; -import { ContextAction, ContextActionCallback } from "../unit/contextaction"; +import { ContextAction } from "../unit/contextaction"; /* Stylesheets */ import "./markers/stylesheets/airbase.css"; @@ -109,6 +109,7 @@ export class Map extends L.Map { /* Unit spawning */ #spawnRequestTable: SpawnRequestTable | null = null; #temporaryMarkers: TemporaryUnitMarker[] = []; + #currentSpawnMarker: TemporaryUnitMarker | null = null; /** * @@ -349,6 +350,8 @@ export class Map extends L.Map { /* Operations to perform when leaving a state */ if (this.#state === COALITIONAREA_DRAW_POLYGON || this.#state === COALITIONAREA_DRAW_CIRCLE) this.getSelectedCoalitionArea()?.setEditing(false); + this.#currentSpawnMarker?.removeFrom(this); + this.#currentSpawnMarker = null; this.#state = state; @@ -361,6 +364,8 @@ export class Map extends L.Map { this.#spawnRequestTable = options?.spawnRequestTable ?? null; console.log(`Spawn request table:`); console.log(this.#spawnRequestTable); + this.#currentSpawnMarker = new TemporaryUnitMarker(new L.LatLng(0, 0), this.#spawnRequestTable?.unit.unitType ?? "", this.#spawnRequestTable?.coalition ?? "neutral") + this.#currentSpawnMarker.addTo(this); } else if (this.#state === CONTEXT_ACTION) { this.deselectAllCoalitionAreas(); this.#contextAction = options?.contextAction ?? null; @@ -840,7 +845,7 @@ export class Map extends L.Map { /* Execute the short click action */ if (this.#state === IDLE) { } else if (this.#state === SPAWN_UNIT) { - if (this.#spawnRequestTable !== null) { + if (e.originalEvent.button != 2 && this.#spawnRequestTable !== null) { this.#spawnRequestTable.unit.location = pressLocation; getApp() .getUnitsManager() @@ -920,6 +925,10 @@ export class Map extends L.Map { this.#lastMousePosition.x = e.originalEvent.x; this.#lastMousePosition.y = e.originalEvent.y; this.#lastMouseCoordinates = e.latlng; + + if (this.#currentSpawnMarker) { + this.#currentSpawnMarker.setLatLng(e.latlng); + } } #onMapMove(e: any) { diff --git a/frontend/react/src/other/utils.ts b/frontend/react/src/other/utils.ts index 9359b6e2..b60ce768 100644 --- a/frontend/react/src/other/utils.ts +++ b/frontend/react/src/other/utils.ts @@ -503,7 +503,7 @@ export function filterBlueprintsByLabel(blueprints: { [key: string]: UnitBluepri var filteredBlueprints: { [key: string]: UnitBlueprint } = {}; if (blueprints) { Object.entries(blueprints).forEach(([key, value]) => { - if (value.enabled && (filterString === "" || value.label.includes(filterString))) filteredBlueprints[key] = value; + if (value.enabled && (filterString === "" || value.label.toLowerCase().includes(filterString.toLowerCase()))) filteredBlueprints[key] = value; }); } return filteredBlueprints; diff --git a/frontend/react/src/server/servermanager.ts b/frontend/react/src/server/servermanager.ts index 108a52ea..c4198ceb 100644 --- a/frontend/react/src/server/servermanager.ts +++ b/frontend/react/src/server/servermanager.ts @@ -59,7 +59,7 @@ export class ServerManager { /* If we are forcing the request we don't care if one already exists, just send it. CAREFUL: this makes sense only for low frequency requests, like refreshes, when we are reasonably confident any previous request will be done before we make a new one on the same URI. */ if (uri in this.#requests && this.#requests[uri].readyState !== 4 && !force) { - console.warn(`GET request on ${uri} URI still pending, skipping...`); + //console.warn(`GET request on ${uri} URI still pending, skipping...`); return; } diff --git a/frontend/react/src/ui/libs/useDrag.ts b/frontend/react/src/ui/libs/useDrag.ts new file mode 100644 index 00000000..ccb343f3 --- /dev/null +++ b/frontend/react/src/ui/libs/useDrag.ts @@ -0,0 +1,68 @@ +import { useCallback, useEffect, useState } from "react"; + +export const useDrag = (props: { ref, initialPosition, count}) => { + const [finalPosition, setFinalPosition] = useState({ x: props.initialPosition.x, y: props.initialPosition.y }); + const [isDragging, setIsDragging] = useState(false); + const [count, setCount] = useState(0) + + if (count !== props.count) { + setCount(props.count) + setFinalPosition({ x: props.initialPosition.x, y: props.initialPosition.y }) + } + + const handleMouseUp = (evt) => { + evt.preventDefault(); + + setIsDragging(false); + }; + + const handleMouseDown = (evt) => { + evt.preventDefault(); + + const { current: draggableElement } = props.ref; + + if (!draggableElement) { + return; + } + + setIsDragging(true); + }; + + const handleMouseMove = useCallback( + (evt) => { + const { current: draggableElement } = props.ref; + + if (!isDragging || !draggableElement) return; + + evt.preventDefault(); + + const parentRect = draggableElement.parentElement.getBoundingClientRect(); + const rect = draggableElement.getBoundingClientRect(); + + const [width, height] = [rect.width, rect.height]; + const [mouseX, mouseY] = [evt.clientX, evt.clientY]; + const [parentTop, parentLeft, parentWidth, parentHeight] = [parentRect.top, parentRect.left, parentRect.width, parentRect.height]; + + setFinalPosition({ + x: Math.max(width / 2, Math.min(mouseX - parentLeft, parentWidth - width / 2)), + y: Math.max(height / 2, Math.min(mouseY - parentTop, parentHeight - height / 2)), + }); + }, + [isDragging, props.ref] + ); + + useEffect(() => { + document.addEventListener("mousemove", handleMouseMove); + document.addEventListener("mouseup", handleMouseUp); + + return () => { + document.removeEventListener("mousemove", handleMouseMove); + document.removeEventListener("mouseup", handleMouseUp); + }; + }, [handleMouseMove]); + + return { + position: finalPosition, + handleMouseDown + }; +}; diff --git a/frontend/react/src/ui/panels/airbasemenu.tsx b/frontend/react/src/ui/panels/airbasemenu.tsx index 05224ebf..91fa1b1f 100644 --- a/frontend/react/src/ui/panels/airbasemenu.tsx +++ b/frontend/react/src/ui/panels/airbasemenu.tsx @@ -1,15 +1,10 @@ import React, { useState } from "react"; import { Menu } from "./components/menu"; -import { OlCheckbox } from "../components/olcheckbox"; -import { OlRangeSlider } from "../components/olrangeslider"; -import { OlNumberInput } from "../components/olnumberinput"; -import { Coalition, MapOptions } from "../../types/types"; -import { getApp } from "../../olympusapp"; +import { Coalition } from "../../types/types"; import { Airbase } from "../../mission/airbase"; import { FaArrowLeft, FaCompass } from "react-icons/fa6"; import { getUnitsByLabel } from "../../other/utils"; import { UnitBlueprint } from "../../interfaces"; -import { IDLE } from "../../constants/constants"; import { OlSearchBar } from "../components/olsearchbar"; import { OlAccordion } from "../components/olaccordion"; import { OlUnitEntryList } from "../components/olunitlistentry"; @@ -20,7 +15,7 @@ export function AirbaseMenu(props: { open: boolean; onClose: () => void; airbase const [blueprint, setBlueprint] = useState(null as null | UnitBlueprint); const [filterString, setFilterString] = useState(""); - const [filteredAircraft, filteredHelicopters, filteredAirDefense, filteredGroundUnits, filteredNavyUnits] = getUnitsByLabel(filterString); + const [filteredAircraft, filteredHelicopters, _1, _2, _3] = getUnitsByLabel(filterString); return ( diff --git a/frontend/react/src/ui/panels/components/menu.tsx b/frontend/react/src/ui/panels/components/menu.tsx index aa8f5456..e5697267 100644 --- a/frontend/react/src/ui/panels/components/menu.tsx +++ b/frontend/react/src/ui/panels/components/menu.tsx @@ -29,13 +29,13 @@ export function Menu(props: { >
diff --git a/frontend/react/src/ui/panels/controlspanel.tsx b/frontend/react/src/ui/panels/controlspanel.tsx index 58989076..a9d1a41f 100644 --- a/frontend/react/src/ui/panels/controlspanel.tsx +++ b/frontend/react/src/ui/panels/controlspanel.tsx @@ -2,17 +2,18 @@ import React, { useEffect, useState } from "react"; import { getApp } from "../../olympusapp"; import { IconDefinition } from "@fortawesome/free-solid-svg-icons"; import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; + export function ControlsPanel(props: {}) { const [controls, setControls] = useState( - [] as { + null as { actions: (string | number | IconDefinition)[]; target: IconDefinition; text: string; - }[] + }[] | null ); useEffect(() => { - if (getApp() && controls.length === 0) { + if (getApp() && controls === null) { setControls(getApp().getMap().getCurrentControls()); } }); @@ -30,7 +31,7 @@ export function ControlsPanel(props: {}) { justify-between gap-1 p-3 text-sm `} > - {controls.map((control) => { + {controls?.map((control) => { return (
void; + leader: Unit | null; + wingmen: Unit[] | null; + children?: JSX.Element | JSX.Element[]; +}) { + const [formationType, setFormationType] = useState("echelon-lh"); + const [horizontalStep, setHorizontalStep] = useState(50); + const [verticalStep, setVerticalStep] = useState(15); + const [count, setCount] = useState(0); + + let units = Array(128).fill(null) as (Unit | null)[]; + units[0] = props.leader; + props.wingmen?.forEach((unit, idx) => (units[idx + 1] = unit)); + + const containerRef = useRef(null); + const silhouetteReferences = units.map((unit) => useRef(null)); + const silhouetteHandles = units.map((unit, idx) => { + let offset = computeFormationOffset(formationType, idx); + let center = { x: 0, y: 0 }; + if (containerRef.current) { + center.x = (containerRef.current as HTMLDivElement).getBoundingClientRect().width / 2; + center.y = 150; + } + return useDrag({ + ref: silhouetteReferences[idx], + initialPosition: { x: offset.z + center.x, y: -offset.x + center.y }, + count: count + }); + }); + + let formationTypes = { + "echelon-lh": "Echelon left", + "echelon-rh": "Echelon right", + "line-abreast-rh": "Line abreast right", + "line-abreast-lh": "Line abreast left", + trail: "Trail", + front: "Front", + diamond: "Diamond", + }; + + return ( + +
+ Formation type presets + + {Object.keys(formationTypes).map((optionFormationType) => { + return ( + { + setCount(count + 1); + setFormationType(optionFormationType); + }} + > + {formationTypes[optionFormationType]} + + ); + })} + + +
+ <> + {units.map((unit, idx) => { + return ( +
+ +
+ ); + })} + +
+
+
+ ); +} + +function computeFormationOffset(formation, idx) { + let offset = { x: 0, y: 0, z: 0 }; + if (formation === "trail") { + offset.x = -50 * idx; + offset.y = -30 * idx; + offset.z = 0; + } else if (formation === "echelon-lh") { + offset.x = -50 * idx; + offset.y = -10 * idx; + offset.z = -50 * idx; + } else if (formation === "echelon-rh") { + offset.x = -50 * idx; + offset.y = -10 * idx; + offset.z = 50 * idx; + } else if (formation === "line-abreast-lh") { + offset.x = 0; + offset.y = 0; + offset.z = -50 * idx; + } else if (formation === "line-abreast-rh") { + offset.x = 0; + offset.y = 0; + offset.z = 50 * idx; + } else if (formation === "front") { + offset.x = 100 * idx; + offset.y = 0; + offset.z = 0; + } else if (formation === "diamond") { + var xr = 0; + var yr = 1; + var zr = -1; + var layer = 1; + + for (let i = 0; i < idx; i++) { + var xl = xr * Math.cos(Math.PI / 4) - yr * Math.sin(Math.PI / 4); + var yl = xr * Math.sin(Math.PI / 4) + yr * Math.cos(Math.PI / 4); + offset = { x: -yl * 50, y: zr * 10, z: xl * 50 }; + if (yr == 0) { + layer++; + xr = 0; + yr = layer; + zr = -layer; + } else { + if (xr < layer) { + xr++; + zr--; + } else { + yr--; + zr++; + } + } + } + } + return offset; +} diff --git a/frontend/react/src/ui/panels/unitcontrolmenu.tsx b/frontend/react/src/ui/panels/unitcontrolmenu.tsx index 2a89a0fb..10818e1c 100644 --- a/frontend/react/src/ui/panels/unitcontrolmenu.tsx +++ b/frontend/react/src/ui/panels/unitcontrolmenu.tsx @@ -485,7 +485,7 @@ export function UnitControlMenu(props: { open: boolean; onClose: () => void }) { return Object.keys(unitOccurences[coalition]).map((name, idx) => { return (
void }) { {/* ============== Payload section START ============== */} {!selectedUnits[0].isTanker() && !selectedUnits[0].isAWACS() && - selectedUnits[0].getAmmo().map((ammo) => { + selectedUnits[0].getAmmo().map((ammo, idx) => { return ( -
+
{ document.addEventListener("hiddenTypesChanged", (ev) => { @@ -91,6 +96,12 @@ export function UI() { setAirbase((ev as CustomEvent).detail); setAirbaseMenuVisible(true); }); + + document.addEventListener("createFormation", (ev) => { + setFormationMenuVisible(true); + setFormationLeader((ev as CustomEvent).detail.leader); + setFormationWingmen((ev as CustomEvent).detail.wingmen); + }); }, []); function hideAllMenus() { @@ -103,6 +114,7 @@ export function UI() { setAirbaseMenuVisible(false); setRadioMenuVisible(false); setAudioMenuVisible(false); + setFormationMenuVisible(false); } function checkPassword(password: string) { @@ -243,6 +255,7 @@ export function UI() { setAirbaseMenuVisible(false)} airbase={airbase}/> setRadioMenuVisible(false)} /> setAudioMenuVisible(false)} /> + setFormationMenuVisible(false)} /> diff --git a/frontend/react/src/unit/unit.ts b/frontend/react/src/unit/unit.ts index 9f97c328..1ec7bf35 100644 --- a/frontend/react/src/unit/unit.ts +++ b/frontend/react/src/unit/unit.ts @@ -849,7 +849,7 @@ export abstract class Unit extends CustomMarker { if (targetPosition) getApp().getUnitsManager().addDestination(targetPosition, false, 0, units); } ); - + contextActionSet.addDefaultContextAction(this, "default", "Set destination", "", faRoute, null, (units: Unit[], targetUnit, targetPosition) => { if (targetPosition) { getApp().getUnitsManager().clearDestinations(units); @@ -1294,48 +1294,6 @@ export abstract class Unit extends CustomMarker { this.#redrawMarker(); } - showFollowOptions(units: Unit[]) { - var contextActionSet = new ContextActionSet(); - - // TODO FIX - contextActionSet.addContextAction(this, "trail", "Trail", "Follow unit in trail formation", olButtonsContextTrail, null, () => - this.applyFollowOptions("trail", units) - ); - contextActionSet.addContextAction(this, "echelon-lh", "Echelon (LH)", "Follow unit in echelon left formation", olButtonsContextEchelonLh, null, () => - this.applyFollowOptions("echelon-lh", units) - ); - contextActionSet.addContextAction(this, "echelon-rh", "Echelon (RH)", "Follow unit in echelon right formation", olButtonsContextEchelonRh, null, () => - this.applyFollowOptions("echelon-rh", units) - ); - contextActionSet.addContextAction( - this, - "line-abreast-lh", - "Line abreast (LH)", - "Follow unit in line abreast left formation", - olButtonsContextLineAbreast, - null, - () => this.applyFollowOptions("line-abreast-lh", units) - ); - contextActionSet.addContextAction( - this, - "line-abreast-rh", - "Line abreast (RH)", - "Follow unit in line abreast right formation", - olButtonsContextLineAbreast, - null, - () => this.applyFollowOptions("line-abreast-rh", units) - ); - contextActionSet.addContextAction(this, "front", "Front", "Fly in front of unit", olButtonsContextFront, null, () => - this.applyFollowOptions("front", units) - ); - contextActionSet.addContextAction(this, "diamond", "Diamond", "Follow unit in diamond formation", olButtonsContextDiamond, null, () => - this.applyFollowOptions("diamond", units) - ); - contextActionSet.addContextAction(this, "custom", "Custom", "Set a custom formation position", faExclamation, null, () => - this.applyFollowOptions("custom", units) - ); - } - applyFollowOptions(formation: string, units: Unit[]) { if (formation === "custom") { document.getElementById("custom-formation-dialog")?.classList.remove("hide"); @@ -1887,7 +1845,16 @@ export abstract class AirUnit extends Unit { olButtonsContextFollow, "unit", (units: Unit[], targetUnit: Unit | null, _) => { - if (targetUnit) targetUnit.showFollowOptions(units); + if (targetUnit) { + document.dispatchEvent( + new CustomEvent("createFormation", { + detail: { + leader: targetUnit, + wingmen: units.filter((unit) => unit !== targetUnit), + }, + }) + ); + } } ); @@ -2243,7 +2210,7 @@ export class NavyUnit extends Unit { this.#carrier.setLatLng(this.getPosition()); this.#carrier.setHeading(this.getHeading()); this.#carrier.updateSize(); - } + } } onAdd(map: Map): this { @@ -2258,8 +2225,7 @@ export class NavyUnit extends Unit { onRemove(map: Map): this { super.onRemove(map); - if (this.#carrier) - this.#carrier.removeFrom(getApp().getMap()) + if (this.#carrier) this.#carrier.removeFrom(getApp().getMap()); return this; } } diff --git a/frontend/react/src/unit/unitsmanager.ts b/frontend/react/src/unit/unitsmanager.ts index f401e96c..9cd6e7d7 100644 --- a/frontend/react/src/unit/unitsmanager.ts +++ b/frontend/react/src/unit/unitsmanager.ts @@ -1463,9 +1463,9 @@ export class UnitsManager { getApp() .getServerManager() .cloneUnits(units, false, spawnPoints, (res: any) => { - if (res.commandHash !== undefined) { + if (res !== undefined) { markers.forEach((marker: TemporaryUnitMarker) => { - marker.setCommandHash(res.commandHash); + marker.setCommandHash(res); }); } }); diff --git a/frontend/server/Example.ogg b/frontend/server/Example.ogg deleted file mode 100644 index 39f957bf..00000000 Binary files a/frontend/server/Example.ogg and /dev/null differ diff --git a/frontend/server/Richard_Wagner_-_The_Valkyrie_-_Ride_of_the_Valkyries.ogg b/frontend/server/Richard_Wagner_-_The_Valkyrie_-_Ride_of_the_Valkyries.ogg deleted file mode 100644 index fb13b008..00000000 Binary files a/frontend/server/Richard_Wagner_-_The_Valkyrie_-_Ride_of_the_Valkyries.ogg and /dev/null differ diff --git a/frontend/server/sample1.WAV b/frontend/server/sample1.WAV deleted file mode 100644 index 941c804f..00000000 Binary files a/frontend/server/sample1.WAV and /dev/null differ diff --git a/frontend/server/sample3.WAV b/frontend/server/sample3.WAV deleted file mode 100644 index 084bad48..00000000 Binary files a/frontend/server/sample3.WAV and /dev/null differ