This commit is contained in:
Pax1601
2024-12-10 20:42:32 +01:00
51 changed files with 1411 additions and 850 deletions

View File

@@ -44,7 +44,6 @@ export class AudioManager {
/* The audio backend must be manually started so that the browser can detect the user is enabling audio.
Otherwise, no playback will be performed. */
#running: boolean = false;
#address: string = "localhost";
#port: number;
#endpoint: string;
#socket: WebSocket | null = null;
@@ -90,18 +89,17 @@ export class AudioManager {
this.#playbackPipeline = new PlaybackPipeline();
/* Connect the audio websocket */
let res = this.#address.match(/(?:http|https):\/\/(.+):/);
if (res === null) res = this.#address.match(/(?:http|https):\/\/(.+)/);
let res = location.toString().match(/(?:http|https):\/\/(.+):/);
if (res === null) res = location.toString().match(/(?:http|https):\/\/(.+)/);
let wsAddress = res ? res[1] : this.#address;
if (this.#endpoint) this.#socket = new WebSocket(`wss://${wsAddress}${this.#endpoint}`);
let wsAddress = res ? res[1] : location.toString();
if (wsAddress.at(wsAddress.length - 1) === "/") wsAddress = wsAddress.substring(0, wsAddress.length - 2)
if (this.#endpoint) this.#socket = new WebSocket(`wss://${wsAddress}/${this.#endpoint}`);
else if (this.#port) this.#socket = new WebSocket(`ws://${wsAddress}:${this.#port}`);
else console.error("The audio backend was enabled but no port/endpoint was provided in the configuration");
if (!this.#socket) return;
this.#socket = new WebSocket(`wss://refugees.dcsolympus.com/audio`); // TODO: remove, used for testing!
/* Log the opening of the connection */
this.#socket.addEventListener("open", (event) => {
console.log("Connection to audio websocket successfull");
@@ -229,10 +227,6 @@ export class AudioManager {
AudioManagerStateChangedEvent.dispatch(this.#running);
}
setAddress(address) {
this.#address = address;
}
setPort(port) {
this.#port = port;
}

View File

@@ -15,7 +15,7 @@ export class SpeechController {
body: JSON.stringify({ data: base64 }), // Send the data in blob format
};
fetch(getApp().getExpressAddress() + `/api/speech/recognize`, requestOptions)
fetch(`./api/speech/recognize`, requestOptions)
.then((response) => {
if (response.status === 200) {
console.log(`Speech recognized correctly`);

View File

@@ -30,7 +30,7 @@ export class TextToSpeechSource extends AudioSource {
this.#loading = true;
fetch(getApp().getExpressAddress() + `/api/speech/generate`, requestOptions)
fetch(`./api/speech/generate`, requestOptions)
.then((response) => {
if (response.status === 200) {
console.log(`Text to speech generate correctly`);

View File

@@ -3,6 +3,7 @@ import { Coalition, MapOptions } from "../types/types";
import { CommandModeOptions } from "../interfaces";
import { ContextAction } from "../unit/contextaction";
import {
faClone,
faExplosion,
faHand,
faLocationCrosshairs,
@@ -264,12 +265,12 @@ export const mapBounds = {
export const defaultMapMirrors = {};
export const defaultMapLayers = {
"AWACS": {
"urlTemplate": 'https://abcd.basemaps.cartocdn.com/dark_nolabels/{z}/{x}/{y}{r}.png',
"minZoom": 1,
"maxZoom": 19,
"attribution": `&copy; <a href="https://www.openstreetmap.org/copyright">OpenStreetMap</a> contributors &copy; <a href="https://carto.com/attributions">CARTO</a>'`
},
AWACS: {
urlTemplate: "https://abcd.basemaps.cartocdn.com/dark_nolabels/{z}/{x}/{y}{r}.png",
minZoom: 1,
maxZoom: 19,
attribution: `&copy; <a href="https://www.openstreetmap.org/copyright">OpenStreetMap</a> contributors &copy; <a href="https://carto.com/attributions">CARTO</a>'`,
},
};
export enum OlympusState {
@@ -286,7 +287,7 @@ export enum OlympusState {
OPTIONS = "Options",
AUDIO = "Audio",
AIRBASE = "Airbase",
GAME_MASTER = "Game master"
GAME_MASTER = "Game master",
}
export const NO_SUBSTATE = "No substate";
@@ -304,7 +305,7 @@ export enum LoginSubState {
NO_SUBSTATE = "No substate",
CREDENTIALS = "Credentials",
COMMAND_MODE = "Command mode",
CONNECT = "Connect"
CONNECT = "Connect",
}
export enum DrawSubState {
@@ -359,7 +360,7 @@ export const MAP_OPTIONS_DEFAULTS: MapOptions = {
cameraPluginMode: "map",
tabletMode: false,
AWACSMode: false,
AWACSCoalition: "blue"
AWACSCoalition: "blue",
};
export const MAP_HIDDEN_TYPES_DEFAULTS = {
@@ -602,9 +603,7 @@ export namespace ContextActions {
ContextActionTarget.POINT,
(units: Unit[], _, targetPosition: LatLng | null) => {
if (targetPosition)
getApp()
.getUnitsManager()
.bombPoint(targetPosition, getApp().getMap().getKeepRelativePositions(), getApp().getMap().getDestinationRotation(), units);
getApp().getUnitsManager().bombPoint(targetPosition, getApp().getMap().getKeepRelativePositions(), getApp().getMap().getDestinationRotation(), units);
},
{ type: ContextActionType.ENGAGE, code: "KeyB", ctrlKey: false, shiftKey: false }
);
@@ -617,11 +616,9 @@ export namespace ContextActions {
ContextActionTarget.POINT,
(units: Unit[], _, targetPosition: LatLng | null) => {
if (targetPosition)
getApp()
.getUnitsManager()
.carpetBomb(targetPosition, getApp().getMap().getKeepRelativePositions(), getApp().getMap().getDestinationRotation(), units);
getApp().getUnitsManager().carpetBomb(targetPosition, getApp().getMap().getKeepRelativePositions(), getApp().getMap().getDestinationRotation(), units);
},
{ type: ContextActionType.ENGAGE, code: "KeyC", ctrlKey: false, shiftKey: false }
{ type: ContextActionType.ENGAGE, code: "KeyH", ctrlKey: false, shiftKey: false }
);
export const LAND = new ContextAction(
@@ -644,9 +641,7 @@ export namespace ContextActions {
ContextActionTarget.POINT,
(units: Unit[], _, targetPosition: LatLng | null) => {
if (targetPosition)
getApp()
.getUnitsManager()
.landAtPoint(targetPosition, getApp().getMap().getKeepRelativePositions(), getApp().getMap().getDestinationRotation(), units);
getApp().getUnitsManager().landAtPoint(targetPosition, getApp().getMap().getKeepRelativePositions(), getApp().getMap().getDestinationRotation(), units);
},
{ type: ContextActionType.ADMIN, code: "KeyK", ctrlKey: false, shiftKey: false }
);
@@ -660,7 +655,7 @@ export namespace ContextActions {
(units: Unit[], _1, _2) => {
getApp().getUnitsManager().createGroup(units);
},
{ type: ContextActionType.OTHER, code: "KeyG", ctrlKey: false, shiftKey: false, altKey: false }
{ type: ContextActionType.OTHER, code: "KeyG", ctrlKey: false, shiftKey: false, altKey: false }
);
export const ATTACK = new ContextAction(
@@ -683,9 +678,7 @@ export namespace ContextActions {
ContextActionTarget.POINT,
(units: Unit[], _, targetPosition: LatLng | null) => {
if (targetPosition)
getApp()
.getUnitsManager()
.fireAtArea(targetPosition, getApp().getMap().getKeepRelativePositions(), getApp().getMap().getDestinationRotation(), units);
getApp().getUnitsManager().fireAtArea(targetPosition, getApp().getMap().getKeepRelativePositions(), getApp().getMap().getDestinationRotation(), units);
},
{ type: ContextActionType.ENGAGE, code: "KeyV", ctrlKey: false, shiftKey: false }
);
@@ -714,6 +707,19 @@ export namespace ContextActions {
(units: Unit[], _1, _2) => {
getApp().getUnitsManager().setAWACSReference(units[0].ID);
},
{ type: ContextActionType.ADMIN, code: "KeyU", ctrlKey: false, shiftKey: false, altKey: false }
{ type: ContextActionType.ADMIN, code: "KeyU", ctrlKey: false, shiftKey: false, altKey: false }
);
export const CLONE = new ContextAction(
"clone",
"Clone unit",
"Clone the unit at the given location",
faClone,
ContextActionTarget.POINT,
(units: Unit[], _1, targetPosition) => {
getApp().getUnitsManager().copy(units);
if (targetPosition) getApp().getUnitsManager().paste(targetPosition);
},
{ type: ContextActionType.ADMIN, code: "KeyC", ctrlKey: false, shiftKey: false, altKey: false }
);
}

View File

@@ -1,7 +1,7 @@
import { AudioSink } from "./audio/audiosink";
import { AudioSource } from "./audio/audiosource";
import { OlympusState, OlympusSubState } from "./constants/constants";
import { CommandModeOptions, OlympusConfig, ServerStatus, SessionData, SpawnRequestTable } from "./interfaces";
import { CommandModeOptions, OlympusConfig, ServerStatus, SessionData, SpawnRequestTable, UnitData } from "./interfaces";
import { CoalitionCircle } from "./map/coalitionarea/coalitioncircle";
import { CoalitionPolygon } from "./map/coalitionarea/coalitionpolygon";
import { Airbase } from "./mission/airbase";
@@ -242,6 +242,32 @@ export class AirbaseSelectedEvent {
}
}
export class SelectionEnabledChangedEvent {
static on(callback: (enabled: boolean) => void) {
document.addEventListener(this.name, (ev: CustomEventInit) => {
callback(ev.detail.enabled);
});
}
static dispatch(enabled: boolean) {
document.dispatchEvent(new CustomEvent(this.name, { detail: { enabled } }));
console.log(`Event ${this.name} dispatched`);
}
};
export class PasteEnabledChangedEvent {
static on(callback: (enabled: boolean) => void) {
document.addEventListener(this.name, (ev: CustomEventInit) => {
callback(ev.detail.enabled);
});
}
static dispatch(enabled: boolean) {
document.dispatchEvent(new CustomEvent(this.name, { detail: { enabled } }));
console.log(`Event ${this.name} dispatched`);
}
};
export class ContactsUpdatedEvent {
static on(callback: () => void) {
document.addEventListener(this.name, (ev: CustomEventInit) => {
@@ -281,6 +307,19 @@ export class ContextActionChangedEvent {
}
}
export class CopiedUnitsEvents {
static on(callback: (unitsData: UnitData[]) => void) {
document.addEventListener(this.name, (ev: CustomEventInit) => {
callback(ev.detail.unitsData);
});
}
static dispatch(unitsData: UnitData[]) {
document.dispatchEvent(new CustomEvent(this.name, { detail: { unitsData } }));
console.log(`Event ${this.name} dispatched`);
}
}
export class UnitUpdatedEvent extends BaseUnitEvent {
static dispatch(unit: Unit) {
document.dispatchEvent(new CustomEvent(this.name, { detail: { unit } }));

View File

@@ -46,7 +46,7 @@ export var BoxSelect = Handler.extend({
},
_onMouseDown: function (e: any) {
if (this._map.getEnableSelection() && e.button == 0) {
if (this._map.getSelectionEnabled() && e.button == 0) {
// Clear the deferred resetState if it hasn't executed yet, otherwise it
// will interrupt the interaction and orphan a box element in the container.
this._clearDeferredResetState();

View File

@@ -53,7 +53,9 @@ import {
MapOptionsChangedEvent,
MapSourceChangedEvent,
MouseMovedEvent,
PasteEnabledChangedEvent,
SelectionClearedEvent,
SelectionEnabledChangedEvent,
SpawnContextMenuRequestEvent,
StarredSpawnsChangedEvent,
UnitDeselectedEvent,
@@ -113,7 +115,8 @@ export class Map extends L.Map {
#lastMouseCoordinates: L.LatLng = new L.LatLng(0, 0);
#previousZoom: number = 0;
#keepRelativePositions: boolean = false;
#enableSelection: boolean = false;
#selectionEnabled: boolean = false;
#pasteEnabled: boolean = false;
/* Camera control plugin */
#slaveDCSCamera: boolean = false;
@@ -363,10 +366,10 @@ export class Map extends L.Map {
shiftKey: false,
ctrlKey: false,
})
.addShortcut("toggleEnableSelection", {
.addShortcut("toggleSelectionEnabled", {
label: "Toggle box selection",
keyUpCallback: () => this.setEnableSelection(false),
keyDownCallback: () => this.setEnableSelection(true),
keyUpCallback: () => this.setSelectionEnabled(false),
keyDownCallback: () => this.setSelectionEnabled(true),
code: "ShiftLeft",
altKey: false,
ctrlKey: false,
@@ -542,6 +545,10 @@ export class Map extends L.Map {
return Object.keys(this.#mapLayers);
}
getMirrors() {
return this.#mapMirrors;
}
setSpawnRequestTable(spawnRequestTable: SpawnRequestTable) {
this.#spawnRequestTable = spawnRequestTable;
}
@@ -574,6 +581,20 @@ export class Map extends L.Map {
}
setContextAction(contextAction: ContextAction | null) {
if (this.#contextAction) {
this.getContainer().classList.remove(`${this.#contextAction.getId()}-cursor`);
Object.values(getApp().getUnitsManager().getUnits()).forEach((unit) => {
unit.getElement()?.querySelector(`[data-object|="unit"]`)?.classList.remove(`${this.#contextAction?.getId()}-cursor`);
});
}
if (contextAction) {
this.getContainer().classList.add(`${contextAction.getId()}-cursor`);
Object.values(getApp().getUnitsManager().getUnits()).forEach((unit) => {
unit.getElement()?.querySelector(`[data-object|="unit"]`)?.classList.add(`${contextAction.getId()}-cursor`);
});
}
this.#contextAction = contextAction;
ContextActionChangedEvent.dispatch(this.#contextAction);
}
@@ -737,12 +758,22 @@ export class Map extends L.Map {
return this.#keepRelativePositions;
}
setEnableSelection(enableSelection: boolean) {
this.#enableSelection = enableSelection;
setSelectionEnabled(selectionEnabled: boolean) {
this.#selectionEnabled = selectionEnabled;
SelectionEnabledChangedEvent.dispatch(selectionEnabled)
}
getEnableSelection() {
return this.#enableSelection;
getSelectionEnabled() {
return this.#selectionEnabled;
}
setPasteEnabled(pasteEnabled: boolean) {
this.#pasteEnabled = pasteEnabled;
PasteEnabledChangedEvent.dispatch(pasteEnabled)
}
getPasteEnabled() {
return this.#pasteEnabled;
}
increaseCameraZoom() {
@@ -787,8 +818,15 @@ export class Map extends L.Map {
this.#currentSpawnMarker = null;
this.#currentEffectMarker?.removeFrom(this);
this.#currentEffectMarker = null;
if (state !== OlympusState.UNIT_CONTROL) getApp().getUnitsManager().deselectAllUnits();
if (state !== OlympusState.UNIT_CONTROL) {
getApp().getUnitsManager().deselectAllUnits();
this.setContextAction(null);
this.setContextActionSet(null);
}
if (state !== OlympusState.DRAW || (state === OlympusState.DRAW && subState !== DrawSubState.EDIT)) this.deselectAllCoalitionAreas();
this.getContainer().classList.remove(`explosion-cursor`);
["white", "blue", "red", "green", "orange"].forEach((color) => this.getContainer().classList.remove(`smoke-${color}-cursor`));
/* Operations to perform when entering a state */
if (state === OlympusState.IDLE) {
@@ -806,10 +844,11 @@ 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");
this.#currentEffectMarker?.addTo(this);
if (this.#effectRequestTable?.type === "explosion") {
this.getContainer().classList.add(`explosion-cursor`);
} else if (this.#effectRequestTable?.type === "smoke") {
this.getContainer().classList.add(`smoke-${this.#effectRequestTable?.smokeColor?.toLowerCase()}-cursor`);
}
}
} else if (state === OlympusState.UNIT_CONTROL) {
console.log(`Context action:`);
@@ -886,6 +925,10 @@ export class Map extends L.Map {
if (!this.#isSelecting) {
console.log(`Left short click at ${e.latlng}`);
if (this.#pasteEnabled) {
getApp().getUnitsManager().paste(e.latlng)
}
/* Execute the short click action */
if (getApp().getState() === OlympusState.IDLE) {
/* Do nothing */
@@ -1001,6 +1044,10 @@ export class Map extends L.Map {
}
getApp().setState(OlympusState.JTAC);
this.#drawIPToTargetLine();
} else if (getApp().getState() === OlympusState.UNIT_CONTROL) {
if (this.#contextAction !== null) this.executeContextAction(null, e.latlng, e.originalEvent);
else if (getApp().getSubState() === NO_SUBSTATE) getApp().setState(OlympusState.IDLE);
else getApp().setState(OlympusState.UNIT_CONTROL);
} else {
if (getApp().getSubState() === NO_SUBSTATE) getApp().setState(OlympusState.IDLE);
else getApp().setState(OlympusState.UNIT_CONTROL);
@@ -1019,8 +1066,7 @@ export class Map extends L.Map {
SpawnContextMenuRequestEvent.dispatch(e.latlng);
getApp().setState(OlympusState.SPAWN_CONTEXT);
} else if (getApp().getState() === OlympusState.UNIT_CONTROL) {
if (this.#contextAction !== null) this.executeContextAction(null, e.latlng, e.originalEvent);
else this.executeDefaultContextAction(null, e.latlng, e.originalEvent);
this.executeDefaultContextAction(null, e.latlng, e.originalEvent);
}
}
@@ -1040,6 +1086,8 @@ export class Map extends L.Map {
if (this.#debounceTimeout) window.clearTimeout(this.#debounceTimeout);
this.setPasteEnabled(false);
if (getApp().getSubState() === NO_SUBSTATE) getApp().setState(OlympusState.IDLE);
else getApp().setState(getApp().getState());
}

View File

@@ -1,6 +1,6 @@
.airbase-icon {
align-items: center;
cursor: pointer;
cursor: url("../images/cursors/pointer.svg"), auto;
display: flex;
justify-content: center;
position: relative;

View File

@@ -1,6 +1,6 @@
.bullseye-icon {
align-items: center;
cursor: pointer;
cursor: url("../images/cursors/pointer.svg"), auto;
display: flex;
justify-content: center;
position: relative;

View File

@@ -1,6 +1,6 @@
/*** Unit marker elements ***/
[data-object|="unit"] {
cursor: pointer;
cursor: url("../images/cursors/pointer.svg"), auto;
display: flex;
height: 100%;
justify-content: center;
@@ -8,6 +8,32 @@
width: 100%;
}
[data-object|="unit"].attack-cursor {
cursor: url("../images/cursors/attack.svg"), auto !important;
}
[data-object|="unit"].follow-cursor {
cursor: url("../images/cursors/follow.svg"), auto !important;
}
[data-object|="unit"].fire-at-area-cursor,
[data-object|="unit"].bomb-cursor,
[data-object|="unit"].carpet-bomb-cursor {
cursor: url("../images/cursors/fire-at-area.svg"), auto !important;
}
[data-object|="unit"].land-cursor {
cursor: url("../images/cursors/land.svg"), auto !important;
}
[data-object|="unit"].land-at-point-cursor {
cursor: url("../images/cursors/land-at-point.svg"), auto !important;
}
[data-object|="unit"].simulate-fire-fight-cursor {
cursor: url("../images/cursors/simulate-fire-fight.svg"), auto !important;
}
[data-awacs-mode] .unit-short-label {
color: transparent !important;
}
@@ -113,42 +139,43 @@
stroke: white;
}
[data-awacs-mode] [data-coalition="blue"] .unit-icon svg {
[data-awacs-mode] [data-coalition="blue"] .unit-icon svg {
fill: transparent !important;
stroke: var(--unit-background-blue) !important;
}
[data-awacs-mode] [data-coalition="red"] .unit-icon svg {
[data-awacs-mode] [data-coalition="red"] .unit-icon svg {
fill: transparent !important;
stroke: var(--unit-background-red) !important;
}
[data-awacs-mode] [data-coalition="neutral"] .unit-icon svg {
[data-awacs-mode] [data-coalition="neutral"] .unit-icon svg {
fill: transparent !important;
stroke: var(--unit-background-neutral) !important;
}
[data-awacs-mode] [data-is-selected] .unit-icon svg {
stroke: #FF0 !important;
stroke: #ff0 !important;
}
[data-awacs-mode] [data-is-selected] .unit-vvi {
background-color: #FF0 !important;
background-color: #ff0 !important;
}
[data-awacs-mode] [data-is-selected] .unit-summary {
color: #FF0 !important;
color: #ff0 !important;
}
[data-awacs-mode] [data-is-selected] .unit-summary::after {
background-color: #FF0 !important;
background-color: #ff0 !important;
}
/*** Cursors ***/
[data-is-dead],
[data-object|="unit-missile"],
[data-object|="unit-bomb"] {
cursor: default;
[data-object|="unit-missile"] *,
[data-object|="unit-bomb"] *{
pointer-events: none;
}
/*** Labels ***/
@@ -236,7 +263,7 @@
translate: 80px 10px;
}
[data-awacs-mode] [data-object|="unit"] .unit-summary.cluster-north {
[data-awacs-mode] [data-object|="unit"] .unit-summary.cluster-north {
translate: 50px -45px;
}
@@ -276,7 +303,7 @@
position: absolute;
z-index: -1;
transform-origin: 0% 0%;
top: 30px
top: 30px;
}
[data-awacs-mode] [data-object|="unit"] .unit-summary.cluster-north::after {
@@ -367,25 +394,16 @@
background-color: var(--secondary-gunmetal-grey);
}
[data-object|="unit"][data-coalition="blue"][data-is-selected]
.unit-short-label {
[data-object|="unit"][data-coalition="blue"][data-is-selected] .unit-short-label {
color: var(--secondary-blue-text);
}
[data-object|="unit"][data-coalition="blue"] .unit-fuel-level,
[data-object|="unit"][data-coalition="blue"] .unit-health-level,
[data-object|="unit"][data-coalition="blue"][data-has-fox-1]
.unit-ammo
> div:nth-child(1),
[data-object|="unit"][data-coalition="blue"][data-has-fox-2]
.unit-ammo
> div:nth-child(2),
[data-object|="unit"][data-coalition="blue"][data-has-fox-3]
.unit-ammo
> div:nth-child(3),
[data-object|="unit"][data-coalition="blue"][data-has-other-ammo]
.unit-ammo
> div:nth-child(4) {
[data-object|="unit"][data-coalition="blue"][data-has-fox-1] .unit-ammo > div:nth-child(1),
[data-object|="unit"][data-coalition="blue"][data-has-fox-2] .unit-ammo > div:nth-child(2),
[data-object|="unit"][data-coalition="blue"][data-has-fox-3] .unit-ammo > div:nth-child(3),
[data-object|="unit"][data-coalition="blue"][data-has-other-ammo] .unit-ammo > div:nth-child(4) {
background-color: var(--primary-blue);
}
@@ -393,25 +411,16 @@
background-color: var(--secondary-blue-outline);
}
[data-object|="unit"][data-coalition="red"][data-is-selected]
.unit-short-label {
[data-object|="unit"][data-coalition="red"][data-is-selected] .unit-short-label {
color: var(--secondary-red-text);
}
[data-object|="unit"][data-coalition="red"] .unit-fuel-level,
[data-object|="unit"][data-coalition="red"] .unit-health-level,
[data-object|="unit"][data-coalition="red"][data-has-fox-1]
.unit-ammo
> div:nth-child(1),
[data-object|="unit"][data-coalition="red"][data-has-fox-2]
.unit-ammo
> div:nth-child(2),
[data-object|="unit"][data-coalition="red"][data-has-fox-3]
.unit-ammo
> div:nth-child(3),
[data-object|="unit"][data-coalition="red"][data-has-other-ammo]
.unit-ammo
> div:nth-child(4) {
[data-object|="unit"][data-coalition="red"][data-has-fox-1] .unit-ammo > div:nth-child(1),
[data-object|="unit"][data-coalition="red"][data-has-fox-2] .unit-ammo > div:nth-child(2),
[data-object|="unit"][data-coalition="red"][data-has-fox-3] .unit-ammo > div:nth-child(3),
[data-object|="unit"][data-coalition="red"][data-has-other-ammo] .unit-ammo > div:nth-child(4) {
background-color: var(--primary-red);
}
@@ -430,15 +439,15 @@
}
[data-object|="unit"][data-state="rtb"] .unit-state {
background-image: url("./images/states/rtb.svg");
background-image: url("../images/states/rtb.svg");
}
[data-object|="unit"][data-state="land"] .unit-state {
background-image: url("./images/states/rtb.svg");
background-image: url("../images/states/rtb.svg");
}
[data-object|="unit"][data-state="idle"] .unit-state {
background-image: url("./images/states/idle.svg");
background-image: url("../images/states/idle.svg");
}
[data-object*="groundunit"][data-state="idle"] .unit-state,
@@ -450,59 +459,59 @@
[data-object|="unit"][data-state="bomb-point"] .unit-state,
[data-object|="unit"][data-state="carpet-bombing"] .unit-state,
[data-object|="unit"][data-state="fire-at-area"] .unit-state {
background-image: url("./images/states/attack.svg");
background-image: url("../images/states/attack.svg");
}
[data-object|="unit"][data-state="follow"] .unit-state {
background-image: url("./images/states/follow.svg");
background-image: url("../images/states/follow.svg");
}
[data-object|="unit"][data-state="refuel"] .unit-state {
background-image: url("./images/states/refuel.svg");
background-image: url("../images/states/refuel.svg");
}
[data-object|="unit"][data-state="human"] .unit-state {
background-image: url("./images/states/human.svg");
background-image: url("../images/states/human.svg");
}
[data-object|="unit"][data-state="dcs"] .unit-state {
background-image: url("./images/states/dcs.svg");
background-image: url("../images/states/dcs.svg");
}
[data-object|="unit"][data-state="land-at-point"] .unit-state {
background-image: url("./images/states/land-at-point.svg");
background-image: url("../images/states/land-at-point.svg");
}
[data-object|="unit"][data-state="no-task"] .unit-state {
background-image: url("./images/states/no-task.svg");
background-image: url("../images/states/no-task.svg");
}
[data-object|="unit"][data-state="off"] .unit-state {
background-image: url("./images/states/off.svg");
background-image: url("../images/states/off.svg");
}
[data-object|="unit"][data-state="tanker"] .unit-state {
background-image: url("./images/states/tanker.svg");
background-image: url("../images/states/tanker.svg");
}
[data-object|="unit"][data-state="AWACS"] .unit-state {
background-image: url("./images/states/awacs.svg");
background-image: url("../images/states/awacs.svg");
}
[data-object|="unit"][data-state="miss-on-purpose"] .unit-state {
background-image: url("./images/states/miss-on-purpose.svg");
background-image: url("../images/states/miss-on-purpose.svg");
}
[data-object|="unit"][data-state="scenic-aaa"] .unit-state {
background-image: url("./images/states/scenic-aaa.svg");
background-image: url("../images/states/scenic-aaa.svg");
}
[data-object|="unit"][data-state="simulate-fire-fight"] .unit-state {
background-image: url("./images/states/simulate-fire-fight.svg");
background-image: url("../images/states/simulate-fire-fight.svg");
}
[data-object|="unit"] .unit-health::before {
background-image: url("./images/icons/health.svg");
background-image: url("../images/icons/health.svg");
background-repeat: no-repeat;
background-size: contain;
content: " ";
@@ -541,11 +550,12 @@
opacity: 0.5;
}
.unit-bullseye, .unit-braa {
.unit-bullseye,
.unit-braa {
display: none;
}
[data-awacs-mode] .unit-bullseye,
[data-awacs-mode] .unit-bullseye,
[data-awacs-mode] .unit-braa {
display: inline;
}

View File

@@ -132,7 +132,7 @@
}
.ol-target-icon {
background-image: url("./images/markers/target.svg");
background-image: url("../images/markers/target.svg");
height: 100%;
width: 100%;
}
@@ -182,3 +182,55 @@
path.leaflet-interactive:focus {
outline: none;
}
.attack-cursor {
cursor: url("../images/cursors/attack.svg"), auto !important;
}
.follow-cursor {
cursor: url("../images/cursors/follow.svg"), auto !important;
}
.fire-at-area-cursor, .bomb-cursor, .carpet-bomb-cursor {
cursor: url("../images/cursors/fire-at-area.svg"), auto !important;
}
.land-cursor {
cursor: url("../images/cursors/land.svg"), auto !important;
}
.land-at-point-cursor {
cursor: url("../images/cursors/land-at-point.svg"), auto !important;
}
.simulate-fire-fight-cursor {
cursor: url("../images/cursors/simulate-fire-fight.svg"), auto !important;
}
#map-container.leaflet-grab {
cursor: url("../images/cursors/grab.svg") 16 16, auto;
}
.explosion-cursor {
cursor: url("../images/cursors/explosion.svg"), auto !important;
}
.smoke-white-cursor {
cursor: url("../images/cursors/smoke-white.svg"), auto !important;
}
.smoke-blue-cursor {
cursor: url("../images/cursors/smoke-blue.svg"), auto !important;
}
.smoke-red-cursor {
cursor: url("../images/cursors/smoke-red.svg"), auto !important;
}
.smoke-green-cursor {
cursor: url("../images/cursors/smoke-green.svg"), auto !important;
}
.smoke-orange-cursor {
cursor: url("../images/cursors/smoke-orange.svg"), auto !important;
}

View File

@@ -270,7 +270,7 @@ export class MissionManager {
}
var xhr = new XMLHttpRequest();
xhr.open("GET", getApp().getExpressAddress() + `/api/airbases/${this.#theatre.toLowerCase()}/${callsign}`, true);
xhr.open("GET", `./api/airbases/${this.#theatre.toLowerCase()}/${callsign}`, true);
xhr.responseType = "json";
xhr.onload = () => {
var status = xhr.status;

View File

@@ -105,18 +105,6 @@ export class OlympusApp {
return this.#AWACSController;
}
getExpressAddress() {
let address = `${window.location.href.split("?")[0].replace("vite/", "").replace("vite", "")}`;
if (address[address.length - 1] !== "/") address += "/"
return address;
}
getBackendAddress() {
let address = `${window.location.href.split("?")[0].replace("vite/", "").replace("vite", "")}`;
if (address[address.length - 1] !== "/") address += "/"
return address + "olympus"
}
start() {
/* Initialize base functionalitites */
this.#shortcutManager = new ShortcutManager(); /* Keep first */
@@ -133,10 +121,6 @@ export class OlympusApp {
/* Controllers */
this.#AWACSController = new AWACSController();
/* Set the address of the server */
this.getServerManager().setAddress(this.getBackendAddress());
this.getAudioManager().setAddress(this.getExpressAddress());
/* Check if we are running the latest version */
const request = new Request("https://raw.githubusercontent.com/Pax1601/DCSOlympus/main/version.json");
fetch(request)
@@ -157,7 +141,7 @@ export class OlympusApp {
});
/* Load the config file from the server */
const configRequest = new Request(this.getExpressAddress() + "resources/config", {
const configRequest = new Request("./resources/config", {
headers: {
'Cache-Control': 'no-cache',
}
@@ -187,7 +171,7 @@ export class OlympusApp {
}
ConfigLoadedEvent.dispatch(this.#config as OlympusConfig);
})
.catch((error) => console.error);
.catch((error) => console.error(error));
this.#shortcutManager?.addShortcut("idle", {
label: "Deselect all",
@@ -217,7 +201,7 @@ export class OlympusApp {
body: JSON.stringify(profile), // Send the data in JSON format
};
fetch(this.getExpressAddress() + `/resources/profile/${username}`, requestOptions)
fetch(`./resources/profile/${username}`, requestOptions)
.then((response) => {
if (response.status === 200) {
console.log(`Profile for ${username} saved correctly`);
@@ -239,7 +223,7 @@ export class OlympusApp {
body: "", // Send the data in JSON format
};
fetch(this.getExpressAddress() + `/resources/profile/reset/${username}`, requestOptions)
fetch(`./resources/profile/reset/${username}`, requestOptions)
.then((response) => {
if (response.status === 200) {
console.log(`Profile for ${username} reset correctly`);
@@ -260,7 +244,7 @@ export class OlympusApp {
body: "", // Send the data in JSON format
};
fetch(this.getExpressAddress() + `/resources/profile/delete/all`, requestOptions)
fetch(`./resources/profile/delete/all`, requestOptions)
.then((response) => {
if (response.status === 200) {
console.log(`All profiles reset correctly`);

View File

@@ -306,7 +306,7 @@ export function convertDateAndTimeToDate(dateAndTime: DateAndTime) {
export function getGroundElevation(latlng: LatLng, callback: CallableFunction) {
/* Get the ground elevation from the server endpoint */
const xhr = new XMLHttpRequest();
xhr.open("GET", getApp().getExpressAddress() + `/api/elevation/${latlng.lat}/${latlng.lng}`, true);
xhr.open("GET", `./api/elevation/${latlng.lat}/${latlng.lng}`, true);
xhr.timeout = 500; // ms
xhr.responseType = "json";
xhr.onload = () => {

View File

@@ -19,7 +19,7 @@ import { MapOptionsChangedEvent, ServerStatusUpdatedEvent } from "../events";
export class ServerManager {
#connected: boolean = false;
#paused: boolean = false;
#REST_ADDRESS = "http://localhost:3001/olympus";
#REST_ADDRESS = "./olympus";
#username: null | string = null;
#password: null | string = null;
#sessionHash: string | null = null;
@@ -164,7 +164,7 @@ export class ServerManager {
getConfig(callback: CallableFunction) {
var xmlHttp = new XMLHttpRequest();
xmlHttp.open("GET", getApp().getExpressAddress() + "config", true);
xmlHttp.open("GET", "./config", true);
xmlHttp.onload = function (e) {
var data = JSON.parse(xmlHttp.responseText);
callback(data);
@@ -175,12 +175,6 @@ export class ServerManager {
xmlHttp.send(null);
}
setAddress(address: string) {
this.#REST_ADDRESS = address;
console.log(`Setting REST address to ${this.#REST_ADDRESS}`);
}
getAirbases(callback: CallableFunction, errorCallback: CallableFunction = () => {}) {
this.GET(callback, errorCallback, AIRBASES_URI);
}

View File

@@ -87,7 +87,7 @@ export class SessionDataManager {
body: JSON.stringify({ sessionHash }), // Send the data in JSON format
};
fetch(getApp().getExpressAddress() + `/resources/sessiondata/load/${getApp().getServerManager().getUsername()}`, requestOptions)
fetch(`./resources/sessiondata/load/${getApp().getServerManager().getUsername()}`, requestOptions)
.then((response) => {
if (response.status === 200) {
console.log(`Session data for profile ${getApp().getServerManager().getUsername()} and session hash ${sessionHash} loaded correctly`);
@@ -118,7 +118,7 @@ export class SessionDataManager {
body: JSON.stringify({ sessionHash: this.#sessionHash, sessionData: this.#sessionData }), // Send the data in JSON format
};
fetch(getApp().getExpressAddress() + `/resources/sessiondata/save/${getApp().getServerManager().getUsername()}`, requestOptions)
fetch(`./resources/sessiondata/save/${getApp().getServerManager().getUsername()}`, requestOptions)
.then((response) => {
if (response.status === 200) {
console.log(`Session data for profile ${getApp().getServerManager().getUsername()} and session hash ${this.#sessionHash} saved correctly`);

View File

@@ -9,7 +9,8 @@ export function OlStateButton(props: {
buttonColor?: string | null;
checked: boolean;
icon?: IconProp;
tooltip: string;
tooltip?: string | JSX.Element | JSX.Element[];
tooltipPosition?: string;
onClick: () => void;
onMouseUp?: () => void;
onMouseDown?: () => void;
@@ -21,7 +22,8 @@ export function OlStateButton(props: {
const className =
(props.className ?? "") +
`
h-[40px] w-[40px] flex-none rounded-md text-lg font-medium
pointer-events-auto h-[40px] w-[40px] flex-none rounded-md text-lg
font-medium
dark:bg-olympus-600 dark:text-gray-300
`;
@@ -57,12 +59,12 @@ export function OlStateButton(props: {
setHover(false);
}}
>
<div className="m-auto flex w-fit content-center justify-center gap-2">
<div className={`m-auto flex w-fit content-center justify-center gap-2`}>
{props.icon && <FontAwesomeIcon icon={props.icon} className="m-auto" style={{ color: textColor }} />}
{props.children}
</div>
</button>
{hover && <OlTooltip buttonRef={buttonRef} content={props.tooltip} />}
{hover && props.tooltip && <OlTooltip buttonRef={buttonRef} content={props.tooltip} position={props.tooltipPosition}/>}
</>
);
}

View File

@@ -1,6 +1,6 @@
import React, { useEffect, useRef, useState } from "react";
export function OlTooltip(props: { content: string; buttonRef: React.MutableRefObject<null> }) {
export function OlTooltip(props: { content: string | JSX.Element | JSX.Element[]; buttonRef: React.MutableRefObject<null>; position?: string }) {
var contentRef = useRef(null);
function setPosition(content: HTMLDivElement, button: HTMLButtonElement) {
@@ -13,18 +13,18 @@ export function OlTooltip(props: { content: string; buttonRef: React.MutableRefO
let [cxl, cyt, cxr, cyb, cw, ch] = [
content.getBoundingClientRect().x,
content.getBoundingClientRect().y,
content.getBoundingClientRect().x + content.clientWidth,
content.getBoundingClientRect().y + content.clientHeight,
content.clientWidth,
content.clientHeight,
content.getBoundingClientRect().x + content.offsetWidth,
content.getBoundingClientRect().y + content.offsetHeight,
content.offsetWidth,
content.offsetHeight,
];
let [bxl, byt, bxr, byb, bbw, bh] = [
button.getBoundingClientRect().x,
button.getBoundingClientRect().y,
button.getBoundingClientRect().x + button.clientWidth,
button.getBoundingClientRect().y + button.clientHeight,
button.clientWidth,
button.clientHeight,
button.getBoundingClientRect().x + button.offsetWidth,
button.getBoundingClientRect().y + button.offsetHeight,
button.offsetWidth,
button.offsetHeight,
];
/* Limit the maximum height */
@@ -37,19 +37,29 @@ export function OlTooltip(props: { content: string; buttonRef: React.MutableRefO
var cxc = (cxl + cxr) / 2;
var bxc = (bxl + bxr) / 2;
/* Compute the x and y offsets needed to align the button and element horizontally, and to put the content below the button */
var offsetX = bxc - cxc;
var offsetY = byb - cyt + 8;
/* Compute the x and y offsets needed to align the button and element horizontally, and to put the content depending on the requested position */
var offsetX = 0;
var offsetY = 0;
if (props.position === undefined || props.position === "below") {
offsetX = bxc - cxc;
offsetY = byb - cyt + 8;
} else if (props.position === "side") {
offsetX = bxr + 8;
offsetY = byt - cyt + (bh - ch) / 2;
}
/* Compute the new position of the left and right margins of the content */
cxl += offsetX;
cxr += offsetX;
cyb += offsetY;
let ncxl = cxl + offsetX;
let ncxr = cxr + offsetX;
let ncyb = cyb + offsetY;
/* Try and move the content so it is inside the screen */
if (cxl < 0) offsetX -= cxl;
if (cxr > window.innerWidth) offsetX -= cxr - window.innerWidth;
if (cyb > window.innerHeight) offsetY -= bh + ch + 16;
if (ncxl < 0) offsetX -= cxl;
if (ncxr > window.innerWidth) {
offsetX = bxl - cxl - cw - 12;
}
if (ncyb > window.innerHeight) offsetY -= bh + ch + 16;
/* Apply the offset */
content.style.left = `${offsetX}px`;

View File

@@ -33,9 +33,7 @@ export function OlUnitSummary(props: { blueprint: UnitBlueprint; coalition: Coal
</div>
</div>
<div
className={`
flex h-fit flex-col justify-between px-2 leading-normal
`}
className={`flex h-fit flex-col justify-between px-2 leading-normal`}
>
<p
className={`

View File

@@ -6,7 +6,7 @@ import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { faArrowRight, faCheckCircle, faExternalLink } from "@fortawesome/free-solid-svg-icons";
import { getApp, VERSION } from "../../olympusapp";
import { sha256 } from "js-sha256";
import { BLUE_COMMANDER, GAME_MASTER, LoginSubState, NO_SUBSTATE, OlympusState, RED_COMMANDER } from "../../constants/constants";
import { LoginSubState, NO_SUBSTATE, OlympusState } from "../../constants/constants";
import { OlDropdown, OlDropdownItem } from "../components/oldropdown";
import { AppStateChangedEvent } from "../../events";

View File

@@ -5,6 +5,7 @@ import { MAP_OPTIONS_DEFAULTS, NO_SUBSTATE, OlympusState, OlympusSubState, Spawn
import { AppStateChangedEvent, ContextActionSetChangedEvent, MapOptionsChangedEvent, ShortcutsChangedEvent } from "../../events";
import { ContextAction } from "../../unit/contextaction";
import { ContextActionSet } from "../../unit/contextactionset";
import { MapToolBar } from "./maptoolbar";
export function ControlsPanel(props: {}) {
const [controls, setControls] = useState(
@@ -19,8 +20,8 @@ export function ControlsPanel(props: {}) {
const [appState, setAppState] = useState(OlympusState.NOT_INITIALIZED);
const [appSubState, setAppSubState] = useState(NO_SUBSTATE as OlympusSubState);
const [mapOptions, setMapOptions] = useState(MAP_OPTIONS_DEFAULTS);
const [shortcuts, setShortcuts] = useState({})
const [contextActionSet, setContextActionSet] = useState(null as null | ContextActionSet)
const [shortcuts, setShortcuts] = useState({});
const [contextActionSet, setContextActionSet] = useState(null as null | ContextActionSet);
useEffect(() => {
AppStateChangedEvent.on((state, subState) => {
@@ -74,53 +75,28 @@ export function ControlsPanel(props: {}) {
}
);
} else if (appState === OlympusState.UNIT_CONTROL) {
if (!mapOptions.tabletMode) {
controls = Object.values(contextActionSet?.getContextActions() ?? {})
.sort((a: ContextAction, b: ContextAction) => (a.getLabel() > b.getLabel() ? 1 : -1))
.filter((contextAction: ContextAction) => contextAction.getOptions().code)
.map((contextAction: ContextAction) => {
let actions: (string | IconDefinition)[] = [];
contextAction.getOptions().shiftKey && actions.push("Shift");
contextAction.getOptions().altKey && actions.push("Alt");
contextAction.getOptions().ctrlKey && actions.push("Ctrl");
actions.push(
(contextAction.getOptions().code as string)
.replace("Key", "")
.replace("ControlLeft", "Left Ctrl")
.replace("AltLeft", "Left Alt")
.replace("ShiftLeft", "Left Shift")
.replace("ControlRight", "Right Ctrl")
.replace("AltRight", "Right Alt")
.replace("ShiftRight", "Right Shift")
);
return {
actions: actions,
text: contextAction.getLabel(),
};
});
controls.unshift({
actions: ["RMB"],
text: "Move",
});
controls.push({
actions: ["RMB", "Hold"],
target: faMap,
text: "Show point actions",
});
controls.push({
actions: ["RMB", "Hold"],
target: faFighterJet,
text: "Show unit actions",
});
controls.push({
actions: shortcuts["toggleRelativePositions"]?.toActions(),
text: "Activate group movement",
});
controls.push({
actions: [...shortcuts["toggleRelativePositions"]?.toActions(), "Wheel"],
text: "Rotate formation",
});
}
controls.unshift({
actions: ["RMB"],
text: "Move",
});
controls.push({
actions: ["RMB", "Hold"],
target: faMap,
text: "Show point actions",
});
controls.push({
actions: ["RMB", "Hold"],
target: faFighterJet,
text: "Show unit actions",
});
controls.push({
actions: shortcuts["toggleRelativePositions"]?.toActions(),
text: "Activate group movement",
});
controls.push({
actions: [...shortcuts["toggleRelativePositions"]?.toActions(), "Wheel"],
text: "Rotate formation",
});
} else if (appState === OlympusState.SPAWN) {
controls = [
{
@@ -151,8 +127,8 @@ export function ControlsPanel(props: {}) {
controls = baseControls;
controls.push({
actions: ["LMB"],
text: "Return to idle state"
})
text: "Return to idle state",
});
}
setControls(controls);
@@ -163,11 +139,13 @@ export function ControlsPanel(props: {}) {
return (
<div
className={`
absolute right-[0px]
absolute right-[0px] top-16
${mapOptions.showMinimap ? `bottom-[233px]` : `bottom-[65px]`}
flex w-[310px] flex-col items-center justify-between gap-1 p-3 text-sm
pointer-events-none flex w-[310px] flex-col items-center justify-between
gap-1 p-3 text-sm
`}
>
<MapToolBar />
{controls?.map((control) => {
return (
<div
@@ -189,9 +167,14 @@ export function ControlsPanel(props: {}) {
return (
<div key={idx} className="flex gap-1">
<div>
{typeof action === "string" || typeof action === "number" ? action : <FontAwesomeIcon icon={action} className={`
my-auto ml-auto
`} />}
{typeof action === "string" || typeof action === "number" ? (
action
) : (
<FontAwesomeIcon
icon={action}
className={`my-auto ml-auto`}
/>
)}
</div>
{idx < control.actions.length - 1 && typeof control.actions[idx + 1] === "string" && <div>+</div>}
{idx < control.actions.length - 1 && typeof control.actions[idx + 1] === "number" && <div>x</div>}

View File

@@ -35,8 +35,7 @@ export function Header() {
MapOptionsChangedEvent.on((mapOptions) => setMapOptions({ ...mapOptions }));
MapSourceChangedEvent.on((source) => setMapSource(source));
ConfigLoadedEvent.on((config: OlympusConfig) => {
var sources = Object.keys(config.frontend.mapMirrors).concat(Object.keys(config.frontend.mapLayers)).concat(getApp().getMap().getLayers());
var sources = Object.keys(getApp().getMap().getMirrors()).concat(getApp().getMap().getLayers());
setMapSources(sources);
});
CommandModeOptionsChangedEvent.on((commandModeOptions) => {

View File

@@ -0,0 +1,275 @@
import React, { useEffect, useRef, useState } from "react";
import { ContextActionSet } from "../../unit/contextactionset";
import { OlStateButton } from "../components/olstatebutton";
import { getApp } from "../../olympusapp";
import { ContextAction, ContextActionOptions } from "../../unit/contextaction";
import { CONTEXT_ACTION_COLORS, ContextActionTarget, MAP_OPTIONS_DEFAULTS } from "../../constants/constants";
import { FaChevronDown, FaChevronUp } from "react-icons/fa6";
import { OlympusState } from "../../constants/constants";
import {
AppStateChangedEvent,
ContextActionChangedEvent,
ContextActionSetChangedEvent,
CopiedUnitsEvents,
MapOptionsChangedEvent,
PasteEnabledChangedEvent,
SelectedUnitsChangedEvent,
SelectionClearedEvent,
SelectionEnabledChangedEvent,
ShortcutsChangedEvent,
} from "../../events";
import { faCopy, faObjectGroup, faPaste } from "@fortawesome/free-solid-svg-icons";
import { Shortcut } from "../../shortcut/shortcut";
import { ShortcutOptions, UnitData } from "../../interfaces";
import { Unit } from "../../unit/unit";
export function MapToolBar(props: {}) {
const [appState, setAppState] = useState(OlympusState.IDLE);
const [contextActionSet, setcontextActionSet] = useState(null as ContextActionSet | null);
const [contextAction, setContextAction] = useState(null as ContextAction | null);
const [scrolledTop, setScrolledTop] = useState(true);
const [scrolledBottom, setScrolledBottom] = useState(false);
const [mapOptions, setMapOptions] = useState(MAP_OPTIONS_DEFAULTS);
const [selectionEnabled, setSelectionEnabled] = useState(false);
const [pasteEnabled, setPasteEnabled] = useState(false);
const [controller, setController] = useState(new AbortController());
const [shortcuts, setShortcuts] = useState(
{} as {
[key: string]: Shortcut;
}
);
const [selectedUnits, setSelectedUnits] = useState([] as Unit[]);
const [copiedUnitsData, setCopiedUnitsData] = useState([] as UnitData[]);
/* Initialize the "scroll" position of the element */
var scrollRef = useRef(null);
useEffect(() => {
if (scrollRef.current) onScroll(scrollRef.current);
});
useEffect(() => {
AppStateChangedEvent.on((state, subState) => setAppState(state));
ContextActionSetChangedEvent.on((contextActionSet) => setcontextActionSet(contextActionSet));
ContextActionChangedEvent.on((contextAction) => setContextAction(contextAction));
MapOptionsChangedEvent.on((mapOptions) => setMapOptions(mapOptions));
SelectionEnabledChangedEvent.on((selectionEnabled) => setSelectionEnabled(selectionEnabled));
PasteEnabledChangedEvent.on((pasteEnabled) => setPasteEnabled(pasteEnabled));
ShortcutsChangedEvent.on((shortcuts) => setShortcuts({ ...shortcuts }));
SelectedUnitsChangedEvent.on((selectedUnits) => setSelectedUnits(selectedUnits));
SelectionClearedEvent.on(() => setSelectedUnits([]));
CopiedUnitsEvents.on((unitsData) => setCopiedUnitsData(unitsData));
}, []);
function onScroll(el) {
const sl = el.scrollTop;
const sr = el.scrollHeight - el.scrollTop - el.clientHeight;
sl < 1 && !scrolledTop && setScrolledTop(true);
sl > 1 && scrolledTop && setScrolledTop(false);
sr < 1 && !scrolledBottom && setScrolledBottom(true);
sr > 1 && scrolledBottom && setScrolledBottom(false);
}
function shortcutCombination(options: ShortcutOptions | ContextActionOptions) {
if (options === undefined) return <></>;
return (
<>
{options.ctrlKey && (
<kbd
className={`
my-auto ml-auto text-nowrap rounded-lg border border-gray-200
bg-gray-100 px-2 py-1 text-xs font-semibold text-gray-800
dark:border-gray-500 dark:bg-gray-600 dark:text-gray-100
`}
>
Ctrl
</kbd>
)}
{options.altKey && (
<kbd
className={`
my-auto ml-auto text-nowrap rounded-lg border border-gray-200
bg-gray-100 px-2 py-1 text-xs font-semibold text-gray-800
dark:border-gray-500 dark:bg-gray-600 dark:text-gray-100
`}
>
Alt
</kbd>
)}
{options.shiftKey && (
<kbd
className={`
my-auto ml-auto text-nowrap rounded-lg border border-gray-200
bg-gray-100 px-2 py-1 text-xs font-semibold text-gray-800
dark:border-gray-500 dark:bg-gray-600 dark:text-gray-100
`}
>
Shift
</kbd>
)}
{options.code && (
<kbd
className={`
my-auto ml-auto text-nowrap rounded-lg border border-gray-200
bg-gray-100 px-2 py-1 text-xs font-semibold text-gray-800
dark:border-gray-500 dark:bg-gray-600 dark:text-gray-100
`}
>
{options.code?.replace("Key", "")}
</kbd>
)}
</>
);
}
let reorderedActions: ContextAction[] = contextActionSet
? Object.values(contextActionSet.getContextActions()).sort((a: ContextAction, b: ContextAction) => (a.getOptions().type < b.getOptions().type ? -1 : 1))
: [];
return (
<>
<>
<div
className={`
relative top-0 mb-auto ml-auto flex max-h-[calc(100%-200px)] gap-2
rounded-md bg-olympus-900
`}
>
{!scrolledTop && (
<FaChevronUp
className={`
absolute top-0 h-6 w-full rounded-lg px-3.5 py-1 text-gray-200
dark:bg-olympus-900
`}
/>
)}
<div className={`flex flex-col gap-2 overflow-y-auto no-scrollbar p-2`} onScroll={(ev) => onScroll(ev.target)} ref={scrollRef}>
<>
<div className="flex flex-col gap-1">
<OlStateButton
key={"select"}
checked={selectionEnabled}
icon={faObjectGroup}
tooltip={
<div className="flex content-center gap-2">
{shortcutCombination(shortcuts["toggleSelectionEnabled"]?.getOptions())}
<div className="my-auto">Box selection</div>
</div>
}
tooltipPosition="side"
onClick={() => {
getApp().getMap().setSelectionEnabled(!selectionEnabled);
if (!selectionEnabled) {
getApp()
.getMap()
.getContainer()
.addEventListener(
"mouseup",
() => {
getApp().getMap().setSelectionEnabled(false);
},
{ once: true, signal: controller.signal }
);
} else {
controller.abort();
}
}}
/>
</div>
{selectedUnits.length > 0 && (
<div className="flex flex-col gap-1">
<OlStateButton
key={"copy"}
checked={false}
icon={faCopy}
tooltip={
<div className="flex content-center gap-2">
{shortcutCombination(shortcuts["copyUnits"]?.getOptions())}
<div className="my-auto">Copy selected units</div>
</div>
}
tooltipPosition="side"
onClick={() => {
getApp().getUnitsManager().copy(selectedUnits);
}}
/>
</div>
)}
{copiedUnitsData.length > 0 && (
<div className="flex flex-col gap-1">
<OlStateButton key={"paste"} checked={pasteEnabled} icon={faPaste} tooltip={
<div className="flex content-center gap-2">
{shortcutCombination(shortcuts["pasteUnits"]?.getOptions())}
<div className="my-auto">Paste copied units</div>
</div>
} tooltipPosition="side" onClick={() => {
getApp().getMap().setPasteEnabled(!pasteEnabled)
}} />
</div>
)}
</>
{reorderedActions.map((contextActionIt: ContextAction) => {
return (
<div className="flex flex-col gap-1">
<OlStateButton
key={contextActionIt.getId()}
checked={contextActionIt === contextAction}
icon={contextActionIt.getIcon()}
tooltip={
<div className="flex content-center gap-2">
{shortcutCombination(contextActionIt.getOptions())}
<div className="my-auto">{contextActionIt.getLabel()}</div>
</div>
}
tooltipPosition="side"
buttonColor={CONTEXT_ACTION_COLORS[contextActionIt.getOptions().type ?? 0]}
onClick={() => {
if (contextActionIt.getTarget() === ContextActionTarget.NONE) {
contextActionIt.executeCallback(null, null);
} else {
contextActionIt !== contextAction ? getApp().getMap().setContextAction(contextActionIt) : getApp().getMap().setContextAction(null);
}
}}
/>
</div>
);
})}
</div>
{!scrolledBottom && (
<FaChevronDown
className={`
absolute bottom-0 h-6 w-full rounded-lg px-3.5 py-1
text-gray-200
dark:bg-olympus-900
`}
/>
)}
</div>
</>
{/*}
{contextAction && (
<div
className={`
absolute left-[50%] top-16 flex translate-x-[calc(-50%+2rem)]
items-center gap-2 rounded-md bg-gray-200 p-4
dark:bg-olympus-800
`}
>
<FontAwesomeIcon
icon={contextAction.getIcon()}
className={`
mr-2 hidden text-xl text-blue-500
md:block
`}
/>
<div className={`text-gray-200`}>{contextAction.getDescription()}</div>
</div>
)}
{*/}
</>
);
}

View File

@@ -20,7 +20,7 @@ export function MiniMapPanel(props: {}) {
useEffect(() => {
let miniMap = document.querySelector(".leaflet-control-minimap");
if (miniMap) {
miniMap.classList.add("rounded-t-lg");
miniMap.classList.add("rounded-b-lg");
}
});

View File

@@ -32,6 +32,7 @@ export function SideBar() {
checked={appState === OlympusState.MAIN_MENU}
icon={faEllipsisV}
tooltip="Hide/show main menu"
tooltipPosition="side"
></OlStateButton>
<OlStateButton
onClick={() => {
@@ -40,6 +41,7 @@ export function SideBar() {
checked={appState === OlympusState.SPAWN}
icon={faPlusSquare}
tooltip="Hide/show unit spawn menu"
tooltipPosition="side"
></OlStateButton>
<OlStateButton
onClick={() => {
@@ -48,6 +50,7 @@ export function SideBar() {
checked={appState === OlympusState.UNIT_CONTROL}
icon={faGamepad}
tooltip="Hide/show selection tool and unit control menu"
tooltipPosition="side"
></OlStateButton>
<OlStateButton
onClick={() => {
@@ -56,6 +59,7 @@ export function SideBar() {
checked={appState === OlympusState.DRAW}
icon={faPencil}
tooltip="Hide/show drawing menu"
tooltipPosition="side"
></OlStateButton>
<OlStateButton
onClick={() => {
@@ -64,6 +68,7 @@ export function SideBar() {
checked={appState === OlympusState.AUDIO}
icon={faVolumeHigh}
tooltip="Hide/show audio menu"
tooltipPosition="side"
></OlStateButton>
{/*}<OlStateButton
onClick={() => {
@@ -80,6 +85,7 @@ export function SideBar() {
checked={appState === OlympusState.AWACS}
icon={faA}
tooltip="Hide/show AWACS menu"
tooltipPosition="side"
></OlStateButton>
<OlStateButton
onClick={() => {
@@ -88,6 +94,7 @@ export function SideBar() {
checked={appState === OlympusState.GAME_MASTER}
icon={faCrown}
tooltip="Hide/show Game Master menu"
tooltipPosition="side"
></OlStateButton>
</div>
</div>
@@ -98,6 +105,7 @@ export function SideBar() {
checked={false}
icon={faQuestionCircle}
tooltip="Open user guide on separate window"
tooltipPosition="side"
></OlStateButton>
<OlStateButton
onClick={() => {
@@ -106,6 +114,7 @@ export function SideBar() {
checked={appState === OlympusState.OPTIONS}
icon={faCog}
tooltip="Hide/show settings menu"
tooltipPosition="side"
></OlStateButton>
</div>
</div>

View File

@@ -1,137 +0,0 @@
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, ContextActionTarget, MAP_OPTIONS_DEFAULTS } from "../../constants/constants";
import { FaChevronDown,FaChevronUp } from "react-icons/fa6";
import { OlympusState } from "../../constants/constants";
import { AppStateChangedEvent, ContextActionChangedEvent, ContextActionSetChangedEvent, MapOptionsChangedEvent } from "../../events";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
export function UnitControlBar(props: {}) {
const [appState, setAppState] = useState(OlympusState.NOT_INITIALIZED);
const [contextActionSet, setcontextActionSet] = useState(null as ContextActionSet | null);
const [contextAction, setContextAction] = useState(null as ContextAction | null);
const [scrolledTop, setScrolledTop] = useState(true);
const [scrolledBottom, setScrolledBottom] = useState(false);
const [menuHidden, setMenuHidden] = useState(false);
const [mapOptions, setMapOptions] = useState(MAP_OPTIONS_DEFAULTS);
/* Initialize the "scroll" position of the element */
var scrollRef = useRef(null);
useEffect(() => {
if (scrollRef.current) onScroll(scrollRef.current);
});
useEffect(() => {
AppStateChangedEvent.on((state, subState) => setAppState(state));
ContextActionSetChangedEvent.on((contextActionSet) => setcontextActionSet(contextActionSet));
ContextActionChangedEvent.on((contextAction) => setContextAction(contextAction));
MapOptionsChangedEvent.on((mapOptions) => setMapOptions({ ...mapOptions }));
}, []);
function onScroll(el) {
const sl = el.scrollTop;
const sr = el.scrollHeight - el.scrollTop - el.clientHeight;
sl < 1 && !scrolledTop && setScrolledTop(true);
sl > 1 && scrolledTop && setScrolledTop(false);
sr < 1 && !scrolledBottom && setScrolledBottom(true);
sr > 1 && scrolledBottom && setScrolledBottom(false);
}
let reorderedActions: ContextAction[] = contextActionSet
? Object.values(contextActionSet.getContextActions()).sort((a: ContextAction, b: ContextAction) => (a.getOptions().type < b.getOptions().type ? -1 : 1))
: [];
return (
<>
{appState === OlympusState.UNIT_CONTROL && contextActionSet && Object.keys(contextActionSet.getContextActions()).length > 0 && (
<>
{mapOptions.tabletMode && (
<>
<div
data-menuhidden={menuHidden}
className={`
absolute right-2 top-16 flex max-h-[80%] gap-2 rounded-md
bg-gray-200
dark:bg-olympus-900
`}
>
{!scrolledTop && (
<FaChevronUp
className={`
absolute top-0 h-6 w-full rounded-lg px-2 py-3.5
text-gray-200
dark:bg-olympus-900
`}
/>
)}
<div className={`
flex flex-col gap-2 overflow-y-auto no-scrollbar p-2
`} onScroll={(ev) => onScroll(ev.target)} ref={scrollRef}>
{reorderedActions.map((contextActionIt: ContextAction) => {
return (
<div className="flex flex-col gap-1">
<OlStateButton
key={contextActionIt.getId()}
checked={contextActionIt === contextAction}
icon={contextActionIt.getIcon()}
tooltip={contextActionIt.getLabel()}
buttonColor={CONTEXT_ACTION_COLORS[contextActionIt.getOptions().type ?? 0]}
onClick={() => {
if (contextActionIt.getTarget() === ContextActionTarget.NONE) {
contextActionIt.executeCallback(null, null);
} else {
contextActionIt !== contextAction
? getApp().getMap().setContextAction(contextActionIt)
: getApp().getMap().setContextAction(null);
}
}}
/>
</div>
);
})}
</div>
{!scrolledBottom && (
<FaChevronDown
className={`
absolute bottom-0 h-6 w-full rounded-lg px-2 py-3.5
text-gray-200
dark:bg-olympus-900
`}
/>
)}
</div>
</>
)}
{contextAction && (
<div
className={`
absolute left-[50%] top-16 flex translate-x-[calc(-50%+2rem)]
items-center gap-2 rounded-md bg-gray-200 p-4
dark:bg-olympus-800
`}
>
<FontAwesomeIcon
icon={contextAction.getIcon()}
className={`
mr-2 hidden text-xl text-blue-500
md:block
`}
/>
<div
className={`text-gray-200`}
>
{contextAction.getDescription()}
</div>
</div>
)}
</>
)}
</>
);
}

View File

@@ -13,7 +13,7 @@ import { getApp, setupApp } from "../olympusapp";
import { LoginModal } from "./modals/loginmodal";
import { MiniMapPanel } from "./panels/minimappanel";
import { UnitControlBar } from "./panels/unitcontrolbar";
import { MapToolBar } from "./panels/maptoolbar";
import { DrawingMenu } from "./panels/drawingmenu";
import { ControlsPanel } from "./panels/controlspanel";
import { MapContextMenu } from "./contextmenus/mapcontextmenu";
@@ -105,7 +105,6 @@ export function UI() {
<CoordinatesPanel />
<RadiosSummaryPanel />
<UnitControlBar />
<SideBar />
<InfoBar />
<HotGroupBar />

View File

@@ -836,6 +836,9 @@ export abstract class Unit extends CustomMarker {
contextActionSet.addContextAction(this, ContextActions.PATH);
contextActionSet.addContextAction(this, ContextActions.DELETE);
contextActionSet.addContextAction(this, ContextActions.EXPLODE);
contextActionSet.addContextAction(this, ContextActions.CENTER_MAP);
contextActionSet.addContextAction(this, ContextActions.CLONE);
contextActionSet.addContextAction(this, ContextActions.ATTACK);
contextActionSet.addDefaultContextAction(this, ContextActions.MOVE);
}
@@ -1357,16 +1360,18 @@ export abstract class Unit extends CustomMarker {
this.#debounceTimeout = window.setTimeout(() => {
console.log(`Left short click on ${this.getUnitName()}`);
if (!e.originalEvent.ctrlKey) getApp().getUnitsManager().deselectAllUnits();
this.setSelected(!this.getSelected());
if (getApp().getState() === OlympusState.UNIT_CONTROL && getApp().getMap().getContextAction()) {
if (getApp().getMap().getContextAction()?.getTarget() === ContextActionTarget.UNIT) getApp().getMap().executeContextAction(this, null, e.originalEvent);
else getApp().getMap().executeContextAction(null, this.getPosition(), e.originalEvent);
} else {
if (!e.originalEvent.ctrlKey) getApp().getUnitsManager().deselectAllUnits();
this.setSelected(!this.getSelected());
}
}, SHORT_PRESS_MILLISECONDS);
}
#onRightShortClick(e: any) {
console.log(`Right short click on ${this.getUnitName()}`);
if (getApp().getState() === OlympusState.UNIT_CONTROL && getApp().getMap().getContextAction()?.getTarget() === ContextActionTarget.UNIT)
getApp().getMap().executeContextAction(this, null, e.originalEvent);
}
#onRightLongClick(e: any) {
@@ -1848,7 +1853,7 @@ export abstract class AirUnit extends Unit {
showAmmo: belongsToCommandedCoalition,
showSummary: belongsToCommandedCoalition || this.getDetectionMethods().some((value) => [VISUAL, OPTIC, RADAR, IRST, DLINK].includes(value)),
showCallsign: belongsToCommandedCoalition && (!getApp().getMap().getOptions().AWACSMode || this.getHuman()),
rotateToHeading: false
rotateToHeading: false,
} as ObjectIconOptions;
}
@@ -1857,10 +1862,8 @@ export abstract class AirUnit extends Unit {
/* Context actions to be executed immediately */
contextActionSet.addContextAction(this, ContextActions.REFUEL);
contextActionSet.addContextAction(this, ContextActions.CENTER_MAP);
/* Context actions that require a target unit */
contextActionSet.addContextAction(this, ContextActions.ATTACK);
contextActionSet.addContextAction(this, ContextActions.FOLLOW);
contextActionSet.addContextAction(this, ContextActions.SET_AWACS_REFERENCE);
@@ -1946,10 +1949,6 @@ export class GroundUnit extends Unit {
/* Context actions to be executed immediately */
contextActionSet.addContextAction(this, ContextActions.GROUP);
contextActionSet.addContextAction(this, ContextActions.CENTER_MAP);
/* Context actions that require a target unit */
contextActionSet.addContextAction(this, ContextActions.ATTACK);
/* Context actions that require a target position */
if (this.canTargetPoint()) {
@@ -2015,10 +2014,6 @@ export class NavyUnit extends Unit {
/* Context actions to be executed immediately */
contextActionSet.addContextAction(this, ContextActions.GROUP);
contextActionSet.addContextAction(this, ContextActions.CENTER_MAP);
/* Context actions that require a target unit */
contextActionSet.addContextAction(this, ContextActions.ATTACK);
/* Context actions that require a target position */
contextActionSet.addContextAction(this, ContextActions.FIRE_AT_AREA);

View File

@@ -17,6 +17,7 @@ import {
AWACSReferenceChangedEvent,
CommandModeOptionsChangedEvent,
ContactsUpdatedEvent,
CopiedUnitsEvents,
HotgroupsChangedEvent,
SelectedUnitsChangedEvent,
SelectionClearedEvent,
@@ -48,10 +49,10 @@ export class UnitsManager {
constructor() {
this.#unitDatabase = new UnitDatabase();
this.#unitDatabase.load(getApp().getExpressAddress() + "api/databases/units/aircraftdatabase", "aircraft");
this.#unitDatabase.load(getApp().getExpressAddress() + "api/databases/units/helicopterdatabase", "helicopter");
this.#unitDatabase.load(getApp().getExpressAddress() + "api/databases/units/groundunitdatabase", "groundunit");
this.#unitDatabase.load(getApp().getExpressAddress() + "api/databases/units/navyunitdatabase", "navyunit");
this.#unitDatabase.load("./api/databases/units/aircraftdatabase", "aircraft");
this.#unitDatabase.load("./api/databases/units/helicopterdatabase", "helicopter");
this.#unitDatabase.load("./api/databases/units/groundunitdatabase", "groundunit");
this.#unitDatabase.load("./api/databases/units/navyunitdatabase", "navyunit");
CommandModeOptionsChangedEvent.on(() => {
Object.values(this.#units).forEach((unit: Unit) => unit.updateVisibility());
@@ -1234,6 +1235,8 @@ export class UnitsManager {
)
); /* Can be applied to humans too */
getApp().addInfoMessage(`${this.#copiedUnits.length} units copied`);
CopiedUnitsEvents.dispatch(this.#copiedUnits)
}
/*********************** Unit manipulation functions ************************/
@@ -1241,7 +1244,7 @@ export class UnitsManager {
*
* @returns True if units were pasted successfully
*/
paste() {
paste(location?: LatLng) {
let spawnPoints = 0;
/* If spawns are restricted, check that the user has the necessary spawn points */
@@ -1285,7 +1288,10 @@ export class UnitsManager {
var units: { ID: number; location: LatLng }[] = [];
let markers: TemporaryUnitMarker[] = [];
groups[groupName].forEach((unit: UnitData) => {
var position = new LatLng(
var position = location ? new LatLng(
location.lat + unit.position.lat - avgLat,
location.lng + unit.position.lng - avgLng
) : new LatLng(
getApp().getMap().getMouseCoordinates().lat + unit.position.lat - avgLat,
getApp().getMap().getMouseCoordinates().lng + unit.position.lng - avgLng
);