From 9d225bfc1ad965119daa287cb3f4446b311b33a5 Mon Sep 17 00:00:00 2001 From: Pax1601 Date: Tue, 12 Nov 2024 16:35:01 +0100 Subject: [PATCH] Added starred spawns --- frontend/react/src/constants/constants.ts | 3 +- frontend/react/src/events.ts | 28 +++- frontend/react/src/interfaces.ts | 1 + frontend/react/src/map/map.ts | 47 ++++-- frontend/react/src/olympusapp.ts | 10 +- frontend/react/src/other/utils.ts | 15 ++ .../react/src/ui/components/olstringinput.tsx | 2 +- .../react/src/ui/components/oltooltip.tsx | 4 +- .../contextmenus/starredspawncontextmenu.tsx | 108 +++++++++++++ frontend/react/src/ui/modals/keybindmodal.tsx | 10 +- frontend/react/src/ui/panels/airbasemenu.tsx | 22 ++- frontend/react/src/ui/panels/header.tsx | 2 +- frontend/react/src/ui/panels/sidebar.tsx | 22 +-- frontend/react/src/ui/panels/spawnmenu.tsx | 8 +- .../react/src/ui/panels/unitspawnmenu.tsx | 153 +++++++++++------- frontend/react/src/ui/ui.tsx | 28 ++-- 16 files changed, 339 insertions(+), 124 deletions(-) create mode 100644 frontend/react/src/ui/contextmenus/starredspawncontextmenu.tsx diff --git a/frontend/react/src/constants/constants.ts b/frontend/react/src/constants/constants.ts index f09399ca..e7c0e8da 100644 --- a/frontend/react/src/constants/constants.ts +++ b/frontend/react/src/constants/constants.ts @@ -271,6 +271,7 @@ export enum OlympusState { MAIN_MENU = "Main menu", UNIT_CONTROL = "Unit control", SPAWN = "Spawn", + STARRED_SPAWN = "Starred spawn", DRAW = "Draw", JTAC = "JTAC", OPTIONS = "Options", @@ -307,7 +308,7 @@ export enum JTACSubState { export enum SpawnSubState { NO_SUBSTATE = "No substate", SPAWN_UNIT = "Unit", - SPAWN_EFFECT = "Effect", + SPAWN_EFFECT = "Effect" } export enum OptionsSubstate { diff --git a/frontend/react/src/events.ts b/frontend/react/src/events.ts index 379f34fa..55cf6f5d 100644 --- a/frontend/react/src/events.ts +++ b/frontend/react/src/events.ts @@ -1,7 +1,7 @@ import { AudioSink } from "./audio/audiosink"; import { AudioSource } from "./audio/audiosource"; import { OlympusState, OlympusSubState } from "./constants/constants"; -import { CommandModeOptions, OlympusConfig, ServerStatus } from "./interfaces"; +import { CommandModeOptions, OlympusConfig, ServerStatus, SpawnRequestTable } from "./interfaces"; import { CoalitionCircle } from "./map/coalitionarea/coalitioncircle"; import { CoalitionPolygon } from "./map/coalitionarea/coalitionpolygon"; import { Airbase } from "./mission/airbase"; @@ -330,6 +330,19 @@ export class UnitContextMenuRequestEvent { } } +export class StarredSpawnContextMenuRequestEvent { + 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 HotgroupsChangedEvent { static on(callback: (hotgroups: {[key: number]: number}) => void) { document.addEventListener(this.name, (ev: CustomEventInit) => { @@ -343,6 +356,19 @@ export class HotgroupsChangedEvent { } } +export class StarredSpawnsChangedEvent { + static on(callback: (starredSpawns: {[key: number]: SpawnRequestTable}) => void) { + document.addEventListener(this.name, (ev: CustomEventInit) => { + callback(ev.detail.starredSpawns); + }); + } + + static dispatch(starredSpawns: {[key: number]: SpawnRequestTable}) { + document.dispatchEvent(new CustomEvent(this.name, {detail: {starredSpawns}})); + console.log(`Event ${this.name} dispatched`); + } +} + /************** Command mode events ***************/ export class CommandModeOptionsChangedEvent { static on(callback: (options: CommandModeOptions) => void) { diff --git a/frontend/react/src/interfaces.ts b/frontend/react/src/interfaces.ts index ef3ee240..2121b235 100644 --- a/frontend/react/src/interfaces.ts +++ b/frontend/react/src/interfaces.ts @@ -99,6 +99,7 @@ export interface SpawnRequestTable { category: string; coalition: string; unit: UnitSpawnTable; + quickAccessName?: string } export interface EffectRequestTable { diff --git a/frontend/react/src/map/map.ts b/frontend/react/src/map/map.ts index 8958449b..26ddad32 100644 --- a/frontend/react/src/map/map.ts +++ b/frontend/react/src/map/map.ts @@ -52,6 +52,8 @@ import { MapOptionsChangedEvent, MapSourceChangedEvent, SelectionClearedEvent, + StarredSpawnContextMenuRequestEvent, + StarredSpawnsChangedEvent, UnitDeselectedEvent, UnitSelectedEvent, UnitUpdatedEvent, @@ -132,6 +134,7 @@ export class Map extends L.Map { /* Unit spawning */ #spawnRequestTable: SpawnRequestTable | null = null; + #starredSpawnRequestTables: { [key: string]: SpawnRequestTable } = {}; #effectRequestTable: EffectRequestTable | null = null; #temporaryMarkers: TemporaryUnitMarker[] = []; #currentSpawnMarker: TemporaryUnitMarker | null = null; @@ -369,31 +372,32 @@ export class Map extends L.Map { .getShortcutManager() .addShortcut(`panUp`, { label: "Pan map up", - keyUpCallback: (ev: KeyboardEvent) => this.#panUp = false, - keyDownCallback: (ev: KeyboardEvent) => this.#panUp = true, + keyUpCallback: (ev: KeyboardEvent) => (this.#panUp = false), + keyDownCallback: (ev: KeyboardEvent) => (this.#panUp = true), code: "KeyW", }) .addShortcut(`panDown`, { label: "Pan map down", - keyUpCallback: (ev: KeyboardEvent) => this.#panDown = false, - keyDownCallback: (ev: KeyboardEvent) => this.#panDown = true, + keyUpCallback: (ev: KeyboardEvent) => (this.#panDown = false), + keyDownCallback: (ev: KeyboardEvent) => (this.#panDown = true), code: "KeyS", }) .addShortcut(`panLeft`, { label: "Pan map left", - keyUpCallback: (ev: KeyboardEvent) => this.#panLeft = false, - keyDownCallback: (ev: KeyboardEvent) => this.#panLeft = true, + keyUpCallback: (ev: KeyboardEvent) => (this.#panLeft = false), + keyDownCallback: (ev: KeyboardEvent) => (this.#panLeft = true), code: "KeyA", }) .addShortcut(`panRight`, { label: "Pan map right", - keyUpCallback: (ev: KeyboardEvent) => this.#panRight = false, - keyDownCallback: (ev: KeyboardEvent) => this.#panRight = true, + keyUpCallback: (ev: KeyboardEvent) => (this.#panRight = false), + keyDownCallback: (ev: KeyboardEvent) => (this.#panRight = true), code: "KeyD", - }).addShortcut(`panFast`, { + }) + .addShortcut(`panFast`, { label: "Pan map fast", - keyUpCallback: (ev: KeyboardEvent) => this.#panFast = false, - keyDownCallback: (ev: KeyboardEvent) => this.#panFast = true, + keyUpCallback: (ev: KeyboardEvent) => (this.#panFast = false), + keyDownCallback: (ev: KeyboardEvent) => (this.#panFast = true), code: "ShiftLeft", }); @@ -481,6 +485,16 @@ export class Map extends L.Map { this.#spawnRequestTable = spawnRequestTable; } + addStarredSpawnRequestTable(key, spawnRequestTable: SpawnRequestTable) { + this.#starredSpawnRequestTables[key] = spawnRequestTable; + StarredSpawnsChangedEvent.dispatch(this.#starredSpawnRequestTables); + } + + removeStarredSpawnRequestTable(key) { + if (key in this.#starredSpawnRequestTables) delete this.#starredSpawnRequestTables[key]; + StarredSpawnsChangedEvent.dispatch(this.#starredSpawnRequestTables); + } + setEffectRequestTable(effectRequestTable: EffectRequestTable) { this.#effectRequestTable = effectRequestTable; if (getApp().getState() === OlympusState.SPAWN && getApp().getSubState() === SpawnSubState.SPAWN_EFFECT) { @@ -1005,10 +1019,15 @@ export class Map extends L.Map { window.clearTimeout(this.#shortPressTimer); window.clearTimeout(this.#longPressTimer); - if (getApp().getSubState() !== NO_SUBSTATE) { - getApp().setState(getApp().getState(), NO_SUBSTATE); + if (getApp().getState() === OlympusState.IDLE) { + StarredSpawnContextMenuRequestEvent.dispatch(e.latlng); + getApp().setState(OlympusState.STARRED_SPAWN); } else { - getApp().setState(OlympusState.IDLE); + if (getApp().getSubState() !== NO_SUBSTATE) { + getApp().setState(getApp().getState(), NO_SUBSTATE); + } else { + getApp().setState(OlympusState.IDLE); + } } } diff --git a/frontend/react/src/olympusapp.ts b/frontend/react/src/olympusapp.ts index 62bd3cbb..9749113c 100644 --- a/frontend/react/src/olympusapp.ts +++ b/frontend/react/src/olympusapp.ts @@ -197,13 +197,11 @@ export class OlympusApp { } setState(state: OlympusState, subState: OlympusSubState = NO_SUBSTATE) { - if (state !== this.#state || subState !== this.#subState) { - this.#state = state; - this.#subState = subState; + this.#state = state; + this.#subState = subState; - console.log(`App state set to ${state}, substate ${subState}`); - AppStateChangedEvent.dispatch(state, subState); - } + console.log(`App state set to ${state}, substate ${subState}`); + AppStateChangedEvent.dispatch(state, subState); } getState() { diff --git a/frontend/react/src/other/utils.ts b/frontend/react/src/other/utils.ts index f332d030..430c0e38 100644 --- a/frontend/react/src/other/utils.ts +++ b/frontend/react/src/other/utils.ts @@ -315,6 +315,21 @@ export function makeID(length) { return result; } +export function hash(str, seed = 0) { + let h1 = 0xdeadbeef ^ seed, h2 = 0x41c6ce57 ^ seed; + for(let i = 0, ch; i < str.length; i++) { + ch = str.charCodeAt(i); + h1 = Math.imul(h1 ^ ch, 2654435761); + h2 = Math.imul(h2 ^ ch, 1597334677); + } + h1 = Math.imul(h1 ^ (h1 >>> 16), 2246822507); + h1 ^= Math.imul(h2 ^ (h2 >>> 13), 3266489909); + h2 = Math.imul(h2 ^ (h2 >>> 16), 2246822507); + h2 ^= Math.imul(h1 ^ (h1 >>> 13), 3266489909); + + return `${4294967296 * (2097151 & h2) + (h1 >>> 0)}`; +}; + export function byteArrayToInteger(array) { let res = 0; for (let i = 0; i < array.length; i++) { diff --git a/frontend/react/src/ui/components/olstringinput.tsx b/frontend/react/src/ui/components/olstringinput.tsx index dbad1a34..f287fbb5 100644 --- a/frontend/react/src/ui/components/olstringinput.tsx +++ b/frontend/react/src/ui/components/olstringinput.tsx @@ -8,7 +8,7 @@ export function OlStringInput(props: { value: string; className?: string; onChan min-w-32 `} > -
+
diff --git a/frontend/react/src/ui/contextmenus/starredspawncontextmenu.tsx b/frontend/react/src/ui/contextmenus/starredspawncontextmenu.tsx new file mode 100644 index 00000000..939f0c32 --- /dev/null +++ b/frontend/react/src/ui/contextmenus/starredspawncontextmenu.tsx @@ -0,0 +1,108 @@ +import React, { useEffect, useRef, useState } from "react"; +import { NO_SUBSTATE, OlympusState, OlympusSubState } from "../../constants/constants"; +import { OlDropdownItem } from "../components/oldropdown"; +import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; +import { LatLng } from "leaflet"; +import { AppStateChangedEvent, StarredSpawnContextMenuRequestEvent, StarredSpawnsChangedEvent } from "../../events"; +import { getApp } from "../../olympusapp"; +import { SpawnRequestTable } from "../../interfaces"; +import { faStar } from "@fortawesome/free-solid-svg-icons"; + +export function StarredSpawnContextMenu(props: {}) { + const [appState, setAppState] = useState(OlympusState.NOT_INITIALIZED); + const [appSubState, setAppSubState] = useState(NO_SUBSTATE as OlympusSubState); + const [xPosition, setXPosition] = useState(0); + const [yPosition, setYPosition] = useState(0); + const [latlng, setLatLng] = useState(null as null | LatLng); + const [starredSpawns, setStarredSpawns] = useState({} as { [key: string]: SpawnRequestTable }); + + var contentRef = useRef(null); + + useEffect(() => { + AppStateChangedEvent.on((state, subState) => { + setAppState(state); + setAppSubState(subState); + }); + StarredSpawnsChangedEvent.on((starredSpawns) => setStarredSpawns({ ...starredSpawns })); + StarredSpawnContextMenuRequestEvent.on((latlng) => { + setLatLng(latlng); + const containerPoint = getApp().getMap().latLngToContainerPoint(latlng); + setXPosition(getApp().getMap().getContainer().offsetLeft + containerPoint.x); + setYPosition(getApp().getMap().getContainer().offsetTop + containerPoint.y); + }); + }, []); + + useEffect(() => { + if (contentRef.current) { + const content = contentRef.current as HTMLDivElement; + + content.style.left = `${xPosition}px`; + content.style.top = `${yPosition}px`; + + let newXPosition = xPosition; + let newYposition = yPosition; + + let [cxr, cyb] = [content.getBoundingClientRect().x + content.clientWidth, content.getBoundingClientRect().y + content.clientHeight]; + + /* Try and move the content so it is inside the screen */ + if (cxr > window.innerWidth) newXPosition -= cxr - window.innerWidth; + if (cyb > window.innerHeight) newYposition -= cyb - window.innerHeight; + + content.style.left = `${newXPosition}px`; + content.style.top = `${newYposition}px`; + } + }); + + return ( + <> + {appState === OlympusState.STARRED_SPAWN && ( + <> +
+
+ {Object.values(starredSpawns).length > 0? Object.values(starredSpawns).map((spawnRequestTable) => { + return ( + { + if (latlng) { + spawnRequestTable.unit.location = latlng; + getApp().getUnitsManager().spawnUnits(spawnRequestTable.category, [spawnRequestTable.unit], spawnRequestTable.coalition, false); + getApp().setState(OlympusState.IDLE) + } + }} + > + +
+ {getApp().getUnitsManager().getDatabase().getByName(spawnRequestTable.unit.unitType)?.label} ({spawnRequestTable.quickAccessName}) +
+
+ ); + }): +
No starred spawns, use the spawn menu to create a quick access spawn
} +
+
+ + )} + + ); +} diff --git a/frontend/react/src/ui/modals/keybindmodal.tsx b/frontend/react/src/ui/modals/keybindmodal.tsx index 41b41401..e13aa33e 100644 --- a/frontend/react/src/ui/modals/keybindmodal.tsx +++ b/frontend/react/src/ui/modals/keybindmodal.tsx @@ -34,9 +34,9 @@ export function KeybindModal(props: { open: boolean }) { for (let id in shortcuts) { if ( code === shortcuts[id].getOptions().code && - shiftKey == shortcuts[id].getOptions().shiftKey && - altKey == shortcuts[id].getOptions().altKey && - ctrlKey == shortcuts[id].getOptions().shiftKey + shiftKey === (shortcuts[id].getOptions().shiftKey ?? false) && + altKey === (shortcuts[id].getOptions().altKey ?? false) && + ctrlKey === (shortcuts[id].getOptions().shiftKey ?? false) ) { available = false; inUseShortcut = shortcuts[id]; @@ -101,12 +101,12 @@ export function KeybindModal(props: { open: boolean }) { type="button" onClick={() => { if (shortcut && code) { - let options = shortcut.getOptions() + let options = shortcut.getOptions(); options.code = code; options.altKey = altKey; options.shiftKey = shiftKey; options.ctrlKey = ctrlKey; - getApp().getShortcutManager().setShortcutOption(shortcut.getId(), options) + getApp().getShortcutManager().setShortcutOption(shortcut.getId(), options); getApp().setState(OlympusState.OPTIONS); } diff --git a/frontend/react/src/ui/panels/airbasemenu.tsx b/frontend/react/src/ui/panels/airbasemenu.tsx index 636013d3..28432bde 100644 --- a/frontend/react/src/ui/panels/airbasemenu.tsx +++ b/frontend/react/src/ui/panels/airbasemenu.tsx @@ -3,13 +3,13 @@ import { Menu } from "./components/menu"; import { Coalition } from "../../types/types"; import { Airbase } from "../../mission/airbase"; import { FaArrowLeft, FaCompass } from "react-icons/fa6"; -import { UnitBlueprint } from "../../interfaces"; +import { SpawnRequestTable, UnitBlueprint } from "../../interfaces"; import { OlSearchBar } from "../components/olsearchbar"; import { OlAccordion } from "../components/olaccordion"; import { OlUnitListEntry } from "../components/olunitlistentry"; import { olButtonsVisibilityAircraft, olButtonsVisibilityHelicopter } from "../components/olicons"; import { UnitSpawnMenu } from "./unitspawnmenu"; -import { AirbaseSelectedEvent, CommandModeOptionsChangedEvent, UnitDatabaseLoadedEvent } from "../../events"; +import { AirbaseSelectedEvent, CommandModeOptionsChangedEvent, StarredSpawnsChangedEvent, UnitDatabaseLoadedEvent } from "../../events"; import { getApp } from "../../olympusapp"; import { BLUE_COMMANDER, COMMAND_MODE_OPTIONS_DEFAULTS, GAME_MASTER, RED_COMMANDER } from "../../constants/constants"; @@ -30,6 +30,7 @@ export function AirbaseMenu(props: { open: boolean; onClose: () => void; childre const [openAccordion, setOpenAccordion] = useState(CategoryAccordion.NONE); const [commandModeOptions, setCommandModeOptions] = useState(COMMAND_MODE_OPTIONS_DEFAULTS); const [showCost, setShowCost] = useState(false); + const [starredSpawns, setStarredSpawns] = useState({} as { [key: string]: SpawnRequestTable }); useEffect(() => { AirbaseSelectedEvent.on((airbase) => { @@ -54,6 +55,8 @@ export function AirbaseMenu(props: { open: boolean; onClose: () => void; childre setShowCost(!(commandModeOptions.commandMode === GAME_MASTER || !commandModeOptions.restrictSpawns)); setOpenAccordion(CategoryAccordion.NONE); }); + + StarredSpawnsChangedEvent.on((starredSpawns) => setStarredSpawns({ ...starredSpawns })); }, []); useEffect(() => { @@ -114,10 +117,9 @@ export function AirbaseMenu(props: { open: boolean; onClose: () => void; childre <> {Object.keys(runway.headings[0]).map((runwayName) => { return ( -
+
{" "} RWY {runwayName} @@ -267,7 +269,13 @@ export function AirbaseMenu(props: { open: boolean; onClose: () => void; childre )} <> {!(blueprint === null) && ( - + )} diff --git a/frontend/react/src/ui/panels/header.tsx b/frontend/react/src/ui/panels/header.tsx index 46759d1d..74c3591a 100644 --- a/frontend/react/src/ui/panels/header.tsx +++ b/frontend/react/src/ui/panels/header.tsx @@ -18,7 +18,7 @@ import { import { FaChevronLeft, FaChevronRight } from "react-icons/fa6"; import { CommandModeOptionsChangedEvent, ConfigLoadedEvent, HiddenTypesChangedEvent, MapOptionsChangedEvent, MapSourceChangedEvent } from "../../events"; import { BLUE_COMMANDER, COMMAND_MODE_OPTIONS_DEFAULTS, MAP_HIDDEN_TYPES_DEFAULTS, MAP_OPTIONS_DEFAULTS } from "../../constants/constants"; -import { CommandModeOptions, OlympusConfig } from "../../interfaces"; +import { OlympusConfig } from "../../interfaces"; export function Header() { const [mapHiddenTypes, setMapHiddenTypes] = useState(MAP_HIDDEN_TYPES_DEFAULTS); diff --git a/frontend/react/src/ui/panels/sidebar.tsx b/frontend/react/src/ui/panels/sidebar.tsx index 376bb2c7..bf4b53fd 100644 --- a/frontend/react/src/ui/panels/sidebar.tsx +++ b/frontend/react/src/ui/panels/sidebar.tsx @@ -1,25 +1,19 @@ import React, { useEffect, useState } from "react"; import { OlStateButton } from "../components/olstatebutton"; -import { - faGamepad, - faPencil, - faEllipsisV, - faCog, - faQuestionCircle, - faPlusSquare, - faVolumeHigh, - faJ, - faCrown, -} from "@fortawesome/free-solid-svg-icons"; +import { faGamepad, faPencil, faEllipsisV, faCog, faQuestionCircle, faPlusSquare, faVolumeHigh, faJ, faCrown } from "@fortawesome/free-solid-svg-icons"; import { getApp } from "../../olympusapp"; -import { OlympusState } from "../../constants/constants"; +import { NO_SUBSTATE, OlympusState, OlympusSubState, SpawnSubState } from "../../constants/constants"; import { AppStateChangedEvent } from "../../events"; export function SideBar() { const [appState, setAppState] = useState(OlympusState.NOT_INITIALIZED); + const [appSubState, setAppSubState] = useState(NO_SUBSTATE as OlympusSubState); useEffect(() => { - AppStateChangedEvent.on((state, subState) => setAppState(state)); + AppStateChangedEvent.on((state, subState) => { + setAppState(state); + setAppSubState(subState); + }); }, []); return ( @@ -43,7 +37,7 @@ export function SideBar() { onClick={() => { getApp().setState(appState !== OlympusState.SPAWN ? OlympusState.SPAWN : OlympusState.IDLE); }} - checked={appState === OlympusState.SPAWN} + checked={appState === OlympusState.SPAWN && appSubState === SpawnSubState.NO_SUBSTATE} icon={faPlusSquare} tooltip="Hide/show unit spawn menu" > diff --git a/frontend/react/src/ui/panels/spawnmenu.tsx b/frontend/react/src/ui/panels/spawnmenu.tsx index 21ae0e94..95a47017 100644 --- a/frontend/react/src/ui/panels/spawnmenu.tsx +++ b/frontend/react/src/ui/panels/spawnmenu.tsx @@ -5,7 +5,7 @@ import { OlAccordion } from "../components/olaccordion"; import { getApp } from "../../olympusapp"; import { OlUnitListEntry } from "../components/olunitlistentry"; import { UnitSpawnMenu } from "./unitspawnmenu"; -import { UnitBlueprint } from "../../interfaces"; +import { SpawnRequestTable, UnitBlueprint } from "../../interfaces"; import { olButtonsVisibilityAircraft, olButtonsVisibilityGroundunit, @@ -17,7 +17,7 @@ import { faExplosion, faSmog } from "@fortawesome/free-solid-svg-icons"; import { OlEffectListEntry } from "../components/oleffectlistentry"; import { EffectSpawnMenu } from "./effectspawnmenu"; import { BLUE_COMMANDER, COMMAND_MODE_OPTIONS_DEFAULTS, GAME_MASTER, NO_SUBSTATE, OlympusState } from "../../constants/constants"; -import { AppStateChangedEvent, CommandModeOptionsChangedEvent, UnitDatabaseLoadedEvent } from "../../events"; +import { AppStateChangedEvent, CommandModeOptionsChangedEvent, StarredSpawnsChangedEvent, UnitDatabaseLoadedEvent } from "../../events"; enum CategoryAccordion { NONE, @@ -42,6 +42,7 @@ export function SpawnMenu(props: { open: boolean; onClose: () => void; children? const [types, setTypes] = useState({ groundunit: [] as string[], navyunit: [] as string[] }); const [commandModeOptions, setCommandModeOptions] = useState(COMMAND_MODE_OPTIONS_DEFAULTS); const [showCost, setShowCost] = useState(false); + const [starredSpawns, setStarredSpawns] = useState({} as { [key: string]: SpawnRequestTable }); useEffect(() => { if (selectedRole) setBlueprints(getApp()?.getUnitsManager().getDatabase().getByRole(selectedRole)); @@ -86,6 +87,8 @@ export function SpawnMenu(props: { open: boolean; onClose: () => void; children? setShowCost(!(commandModeOptions.commandMode == GAME_MASTER || !commandModeOptions.restrictSpawns)); setOpenAccordion(CategoryAccordion.NONE); }); + + StarredSpawnsChangedEvent.on((starredSpawns) => setStarredSpawns({ ...starredSpawns })); }, []); /* Filter the blueprints according to the label */ @@ -434,6 +437,7 @@ export function SpawnMenu(props: { open: boolean; onClose: () => void; children? )} diff --git a/frontend/react/src/ui/panels/unitspawnmenu.tsx b/frontend/react/src/ui/panels/unitspawnmenu.tsx index 82a1bd53..a40a1e64 100644 --- a/frontend/react/src/ui/panels/unitspawnmenu.tsx +++ b/frontend/react/src/ui/panels/unitspawnmenu.tsx @@ -1,19 +1,22 @@ -import React, { useState, useEffect } from "react"; +import React, { useState, useEffect, useCallback } from "react"; import { OlUnitSummary } from "../components/olunitsummary"; import { OlCoalitionToggle } from "../components/olcoalitiontoggle"; import { OlNumberInput } from "../components/olnumberinput"; import { OlLabelToggle } from "../components/ollabeltoggle"; import { OlRangeSlider } from "../components/olrangeslider"; import { OlDropdownItem, OlDropdown } from "../components/oldropdown"; -import { LoadoutBlueprint, UnitBlueprint } from "../../interfaces"; +import { LoadoutBlueprint, SpawnRequestTable, UnitBlueprint } from "../../interfaces"; +import { OlStateButton } from "../components/olstatebutton"; import { Coalition } from "../../types/types"; import { getApp } from "../../olympusapp"; -import { ftToM } from "../../other/utils"; +import { ftToM, hash } from "../../other/utils"; import { LatLng } from "leaflet"; import { Airbase } from "../../mission/airbase"; import { altitudeIncrements, groupUnitCount, maxAltitudeValues, minAltitudeValues, OlympusState, SpawnSubState } from "../../constants/constants"; +import { faStar } from "@fortawesome/free-solid-svg-icons"; +import { OlStringInput } from "../components/olstringinput"; -export function UnitSpawnMenu(props: { blueprint: UnitBlueprint; spawnAtLocation: boolean; airbase?: Airbase | null; coalition?: Coalition }) { +export function UnitSpawnMenu(props: { starredSpawns: { [key: string]: SpawnRequestTable }, blueprint: UnitBlueprint; spawnAtLocation: boolean; airbase?: Airbase | null; coalition?: Coalition }) { /* Compute the min and max values depending on the unit type */ const minNumber = 1; const maxNumber = groupUnitCount[props.blueprint.category]; @@ -28,61 +31,62 @@ export function UnitSpawnMenu(props: { blueprint: UnitBlueprint; spawnAtLocation const [spawnLoadoutName, setSpawnLoadout] = useState(""); const [spawnAltitude, setSpawnAltitude] = useState((maxAltitude - minAltitude) / 2); const [spawnAltitudeType, setSpawnAltitudeType] = useState(false); + + const [quickAccessName, setQuickAccessName] = useState("No name"); + const [key, setKey] = useState(""); + const [spawnRequestTable, setSpawnRequestTable] = useState(null as null | SpawnRequestTable); /* When the menu is opened show the unit preview on the map as a cursor */ useEffect(() => { - if (props.coalition && props.coalition !== spawnCoalition) { - setSpawnCoalition(props.coalition); + if (props.spawnAtLocation && spawnRequestTable) { + /* Refresh the unique key identified */ + const newKey = hash(JSON.stringify(spawnRequestTable)); + setKey(newKey); + + getApp()?.getMap()?.setSpawnRequestTable(spawnRequestTable); + getApp().setState(OlympusState.SPAWN, SpawnSubState.SPAWN_UNIT); } + }, [spawnRequestTable]); + + /* Callback and effect to update the quick access name of the starredSpawn */ + const updateStarredSpawnQuickAccessNameS = useCallback(() => { + if (key in props.starredSpawns) props.starredSpawns[key].quickAccessName = quickAccessName; + }, [props.starredSpawns, key, quickAccessName]); + useEffect(updateStarredSpawnQuickAccessNameS, [quickAccessName]); + + /* Callback and effect to update the quick access name in the input field */ + const updateQuickAccessName = useCallback(() => { if (props.spawnAtLocation) { - if (props.blueprint !== null) { - getApp() - ?.getMap() - ?.setSpawnRequestTable({ - category: props.blueprint.category, - unit: { - unitType: props.blueprint.name, - location: new LatLng(0, 0), // This will be filled when the user clicks on the map to spawn the unit - skill: "High", - liveryID: "", - altitude: ftToM(spawnAltitude), - loadout: - props.blueprint.loadouts?.find((loadout) => { - return loadout.name === spawnLoadoutName; - })?.code ?? "", - }, - coalition: spawnCoalition, - }); - getApp().setState(OlympusState.SPAWN, SpawnSubState.SPAWN_UNIT); - } else { - if (getApp().getState() === OlympusState.SPAWN) getApp().setState(OlympusState.IDLE); - } + /* If the spawn is starred, set the quick access name */ + if (key in props.starredSpawns && props.starredSpawns[key].quickAccessName) setQuickAccessName(props.starredSpawns[key].quickAccessName); + else setQuickAccessName("No name"); + } + }, [props.starredSpawns, key]) + useEffect(updateQuickAccessName, [key]) + + /* Callback and effect to update the spawn request table */ + const updateSpawnRequestTable = useCallback(() => { + if (props.blueprint !== null) { + setSpawnRequestTable({ + category: props.blueprint.category, + unit: { + unitType: props.blueprint.name, + location: new LatLng(0, 0), // This will be filled when the user clicks on the map to spawn the unit + skill: "High", + liveryID: "", + altitude: ftToM(spawnAltitude), + loadout: props.blueprint.loadouts?.find((loadout) => loadout.name === spawnLoadoutName)?.code ?? "", + }, + coalition: spawnCoalition, + }); } }, [props.blueprint, spawnAltitude, spawnLoadoutName, spawnCoalition]); + useEffect(updateSpawnRequestTable, [props.blueprint, spawnAltitude, spawnLoadoutName, spawnCoalition]); - function spawnAtAirbase() { - getApp() - .getUnitsManager() - .spawnUnits( - props.blueprint.category, - [ - { - unitType: props.blueprint.name, - location: new LatLng(0, 0), // Not relevant spawning at airbase - skill: "High", - liveryID: "", - altitude: 0, - loadout: - props.blueprint.loadouts?.find((loadout) => { - return loadout.name === spawnLoadoutName; - })?.code ?? "", - }, - ], - props.coalition, - false, - props.airbase?.getName() - ); - } + /* Effect to update the coalition if it is force externally */ + useEffect(() => { + if (props.coalition) setSpawnCoalition(props.coalition); + }, [props.coalition]); /* Get a list of all the roles */ const roles: string[] = []; @@ -113,8 +117,8 @@ export function UnitSpawnMenu(props: { blueprint: UnitBlueprint; spawnAtLocation
{!props.coalition && ( )}
+
+
Quick access:
+ { + setQuickAccessName(e.target.value); + }} + value={quickAccessName} + /> + { + key in props.starredSpawns + ? getApp().getMap().removeStarredSpawnRequestTable(key) + : getApp() + .getMap() + .addStarredSpawnRequestTable(key, { + category: props.blueprint.category, + unit: { + unitType: props.blueprint.name, + location: new LatLng(0, 0), // This will be filled when the user clicks on the map to spawn the unit + skill: "High", + liveryID: "", + altitude: ftToM(spawnAltitude), + loadout: props.blueprint.loadouts?.find((loadout) => loadout.name === spawnLoadoutName)?.code ?? "", + }, + coalition: spawnCoalition, + quickAccessName: quickAccessName, + }); + }} + tooltip="Save this spawn for quick access" + checked={key in props.starredSpawns} + icon={faStar} + > +
{["aircraft", "helicopter"].includes(props.blueprint.category) && ( <> {!props.airbase && ( @@ -283,7 +325,10 @@ export function UnitSpawnMenu(props: { blueprint: UnitBlueprint; spawnAtLocation hover:bg-blue-800 `} onClick={() => { - spawnAtAirbase(); + if (spawnRequestTable) + getApp() + .getUnitsManager() + .spawnUnits(spawnRequestTable.category, [spawnRequestTable.unit], spawnRequestTable.coalition, false, props.airbase?.getName()); }} > Spawn diff --git a/frontend/react/src/ui/ui.tsx b/frontend/react/src/ui/ui.tsx index d9f08091..d59cc827 100644 --- a/frontend/react/src/ui/ui.tsx +++ b/frontend/react/src/ui/ui.tsx @@ -8,13 +8,7 @@ import { MainMenu } from "./panels/mainmenu"; import { SideBar } from "./panels/sidebar"; import { OptionsMenu } from "./panels/optionsmenu"; import { MapHiddenTypes, MapOptions } from "../types/types"; -import { - NO_SUBSTATE, - OlympusState, - OlympusSubState, - OptionsSubstate, - UnitControlSubState, -} from "../constants/constants"; +import { NO_SUBSTATE, OlympusState, OlympusSubState, OptionsSubstate, SpawnSubState, UnitControlSubState } from "../constants/constants"; import { getApp, setupApp } from "../olympusapp"; import { LoginModal } from "./modals/login"; @@ -34,6 +28,7 @@ import { AppStateChangedEvent, MapOptionsChangedEvent } from "../events"; import { GameMasterMenu } from "./panels/gamemastermenu"; import { InfoBar } from "./panels/infobar"; import { HotGroupBar } from "./panels/hotgroupsbar"; +import { StarredSpawnContextMenu } from "./contextmenus/starredspawncontextmenu"; export type OlympusUIState = { mainMenuVisible: boolean; @@ -58,8 +53,6 @@ export function UI() { }); }, []); - - return (
{appState === OlympusState.LOGIN && ( <> -
- + `} + >
+ )} @@ -87,7 +79,10 @@ export function UI() {
getApp().setState(OlympusState.IDLE)} /> - getApp().setState(OlympusState.IDLE)} /> + getApp().setState(OlympusState.IDLE)} + /> getApp().setState(OlympusState.IDLE)} /> +