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 && (
-
-
-
- {contextAction.getDescription()}
-
-
- )}
- >
- )}
- >
+ );
+ })}
+
);
}
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 (