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)} />
+