Added spawn context menu and controls rework

This commit is contained in:
Davide Passoni 2024-11-19 17:45:46 +01:00
parent 430d8db15d
commit 38f6788fa8
34 changed files with 1568 additions and 726 deletions

View File

@ -42,7 +42,7 @@ export class AudioManager {
config.audio.WSPort ? this.setPort(config.audio.WSPort) : this.setEndpoint(config.audio.WSEndpoint);
});
let PTTKeys = ["KeyZ", "KeyX", "KeyC", "KeyV", "KeyB", "KeyN", "KeyM", "KeyK", "KeyL"];
let PTTKeys = ["KeyZ", "KeyX", "KeyC", "KeyV", "KeyB", "KeyN", "KeyM", "KeyComma", "KeyDot"];
PTTKeys.forEach((key, idx) => {
getApp()
.getShortcutManager()
@ -50,7 +50,8 @@ export class AudioManager {
label: `PTT ${idx} active`,
keyDownCallback: () => this.getSinks()[idx]?.setPtt(true),
keyUpCallback: () => this.getSinks()[idx]?.setPtt(false),
code: key
code: key,
shiftKey: true
});
});
}

View File

@ -265,7 +265,7 @@ export enum OlympusState {
MAIN_MENU = "Main menu",
UNIT_CONTROL = "Unit control",
SPAWN = "Spawn",
STARRED_SPAWN = "Starred spawn",
SPAWN_CONTEXT = "Spawn context",
DRAW = "Draw",
JTAC = "JTAC",
OPTIONS = "Options",
@ -336,6 +336,7 @@ export const MAP_OPTIONS_DEFAULTS: MapOptions = {
cameraPluginRatio: 1,
cameraPluginEnabled: false,
cameraPluginMode: "map",
tabletMode: false
};
export const MAP_HIDDEN_TYPES_DEFAULTS = {
@ -457,7 +458,7 @@ export namespace ContextActions {
{
executeImmediately: true,
type: ContextActionType.MOVE,
hotkey: "KeyZ",
code: "Space",
}
);
@ -474,7 +475,7 @@ export namespace ContextActions {
.getUnitsManager()
.addDestination(targetPosition, getApp().getMap().getOptions().keepRelativePositions, getApp().getMap().getDestinationRotation(), units);
},
{ type: ContextActionType.MOVE, hotkey: "KeyX" }
{ type: ContextActionType.MOVE, code: null }
);
export const PATH = new ContextAction(
@ -489,7 +490,7 @@ export namespace ContextActions {
.getUnitsManager()
.addDestination(targetPosition, getApp().getMap().getOptions().keepRelativePositions, getApp().getMap().getDestinationRotation(), units);
},
{ type: ContextActionType.MOVE, hotkey: "KeyC" }
{ type: ContextActionType.MOVE, code: "ControlLeft" }
);
export const DELETE = new ContextAction(
@ -504,6 +505,7 @@ export namespace ContextActions {
{
executeImmediately: true,
type: ContextActionType.DELETE,
code: "Delete"
}
);
@ -520,6 +522,8 @@ export namespace ContextActions {
{
executeImmediately: true,
type: ContextActionType.DELETE,
code: "Delete",
ctrlKey: true
}
);
@ -532,7 +536,7 @@ export namespace ContextActions {
(units: Unit[]) => {
getApp().getMap().centerOnUnit(units[0]);
},
{ executeImmediately: true, type: ContextActionType.OTHER }
{ executeImmediately: true, type: ContextActionType.OTHER, code: "KeyM", altKey: true }
);
export const REFUEL = new ContextAction(
@ -544,7 +548,7 @@ export namespace ContextActions {
(units: Unit[]) => {
getApp().getUnitsManager().refuel(units);
},
{ executeImmediately: true, type: ContextActionType.ADMIN }
{ executeImmediately: true, type: ContextActionType.ADMIN, code: "KeyV" }
);
export const FOLLOW = new ContextAction(
@ -562,7 +566,7 @@ export namespace ContextActions {
);
}
},
{ type: ContextActionType.ADMIN }
{ type: ContextActionType.ADMIN, code: "KeyF" }
);
export const BOMB = new ContextAction(
@ -577,7 +581,7 @@ export namespace ContextActions {
.getUnitsManager()
.bombPoint(targetPosition, getApp().getMap().getOptions().keepRelativePositions, getApp().getMap().getDestinationRotation(), units);
},
{ type: ContextActionType.ENGAGE }
{ type: ContextActionType.ENGAGE, code: "KeyB" }
);
export const CARPET_BOMB = new ContextAction(
@ -592,7 +596,7 @@ export namespace ContextActions {
.getUnitsManager()
.carpetBomb(targetPosition, getApp().getMap().getOptions().keepRelativePositions, getApp().getMap().getDestinationRotation(), units);
},
{ type: ContextActionType.ENGAGE }
{ type: ContextActionType.ENGAGE, code: "KeyB", altKey: true }
);
export const LAND = new ContextAction(
@ -604,7 +608,7 @@ export namespace ContextActions {
(units: Unit[], _, targetPosition: LatLng | null) => {
if (targetPosition) getApp().getUnitsManager().landAt(targetPosition, units);
},
{ type: ContextActionType.ADMIN }
{ type: ContextActionType.ADMIN, code: "KeyL" }
);
export const LAND_AT_POINT = new ContextAction(
@ -619,7 +623,7 @@ export namespace ContextActions {
.getUnitsManager()
.landAtPoint(targetPosition, getApp().getMap().getOptions().keepRelativePositions, getApp().getMap().getDestinationRotation(), units);
},
{ type: ContextActionType.ADMIN }
{ type: ContextActionType.ADMIN, code: "KeyL", altKey: true }
);
export const GROUP = new ContextAction(
@ -631,7 +635,7 @@ export namespace ContextActions {
(units: Unit[], _1, _2) => {
getApp().getUnitsManager().createGroup(units);
},
{ executeImmediately: true, type: ContextActionType.OTHER }
{ executeImmediately: true, type: ContextActionType.OTHER, code: "KeyG" }
);
export const ATTACK = new ContextAction(
@ -643,7 +647,7 @@ export namespace ContextActions {
(units: Unit[], targetUnit: Unit | null, _) => {
if (targetUnit) getApp().getUnitsManager().attackUnit(targetUnit.ID, units);
},
{ type: ContextActionType.ENGAGE }
{ type: ContextActionType.ENGAGE, code: "KeyZ" }
);
export const FIRE_AT_AREA = new ContextAction(
@ -658,7 +662,7 @@ export namespace ContextActions {
.getUnitsManager()
.fireAtArea(targetPosition, getApp().getMap().getOptions().keepRelativePositions, getApp().getMap().getDestinationRotation(), units);
},
{ type: ContextActionType.ENGAGE }
{ type: ContextActionType.ENGAGE, code: "KeyZ", altKey: true }
);
export const SIMULATE_FIRE_FIGHT = new ContextAction(
@ -673,6 +677,6 @@ export namespace ContextActions {
.getUnitsManager()
.simulateFireFight(targetPosition, getApp().getMap().getOptions().keepRelativePositions, getApp().getMap().getDestinationRotation(), units);
},
{ type: ContextActionType.ADMIN }
{ type: ContextActionType.ADMIN, code: "KeyX" }
);
}

View File

@ -98,19 +98,6 @@ export class InfoPopupEvent {
}
}
export class HideMenuEvent {
static on(callback: (hidden: boolean) => void) {
document.addEventListener(this.name, (ev: CustomEventInit) => {
callback(ev.detail.hidden);
});
}
static dispatch(hidden: boolean) {
document.dispatchEvent(new CustomEvent(this.name, { detail: { hidden } }));
console.log(`Event ${this.name} dispatched`);
}
}
export class ShortcutsChangedEvent {
static on(callback: (shortcuts: { [key: string]: Shortcut }) => void) {
document.addEventListener(this.name, (ev: CustomEventInit) => {
@ -332,7 +319,7 @@ export class UnitContextMenuRequestEvent {
}
}
export class StarredSpawnContextMenuRequestEvent {
export class SpawnContextMenuRequestEvent {
static on(callback: (latlng: L.LatLng) => void) {
document.addEventListener(this.name, (ev: CustomEventInit) => {
callback(ev.detail.latlng);

View File

@ -53,7 +53,7 @@ import {
MapSourceChangedEvent,
MouseMovedEvent,
SelectionClearedEvent,
StarredSpawnContextMenuRequestEvent,
SpawnContextMenuRequestEvent,
StarredSpawnsChangedEvent,
UnitDeselectedEvent,
UnitSelectedEvent,
@ -79,12 +79,6 @@ export class Map extends L.Map {
#mapLayers: any = defaultMapLayers;
#mapMirrors: any = defaultMapMirrors;
/* Inputs timers */
#mouseCooldownTimer: number = 0;
#shortPressTimer: number = 0;
#longPressTimer: number = 0;
#selecting: boolean = false;
/* Camera keyboard panning control */
defaultPanDelta: number = 100;
#panInterval: number | null = null;
@ -103,13 +97,13 @@ export class Map extends L.Map {
#miniMapPolyline: L.Polyline;
/* Other state controls */
#isMouseOnCooldown: boolean = false;
#isZooming: boolean = false;
#isDragging: boolean = false;
#isMouseDown: boolean = false;
#lastMousePosition: L.Point = new L.Point(0, 0);
#lastMouseCoordinates: L.LatLng = new L.LatLng(0, 0);
#previousZoom: number = 0;
#selecting: boolean = false;
/* Camera control plugin */
#slaveDCSCamera: boolean = false;
@ -189,10 +183,10 @@ export class Map extends L.Map {
this.on("selectionstart", (e: any) => this.#onSelectionStart(e));
this.on("selectionend", (e: any) => this.#onSelectionEnd(e));
this.on("dblclick", (e: any) => this.#onDoubleClick(e));
this.on("mouseup", (e: any) => this.#onMouseUp(e));
this.on("mousedown", (e: any) => this.#onMouseDown(e));
this.on("contextmenu", (e: any) => e.originalEvent.preventDefault());
this.on("click", (e: any) => this.#onLeftClick(e));
this.on("contextmenu", (e: any) => this.#onRightClick(e));
this.on("mousemove", (e: any) => this.#onMouseMove(e));
@ -301,67 +295,78 @@ export class Map extends L.Map {
label: "Hide/show labels",
keyUpCallback: () => this.setOption("showUnitLabels", !this.getOptions().showUnitLabels),
code: "KeyL",
shiftKey: true
})
.addShortcut("toggleAcquisitionRings", {
label: "Hide/show acquisition rings",
keyUpCallback: () => this.setOption("showUnitsAcquisitionRings", !this.getOptions().showUnitsAcquisitionRings),
code: "KeyE",
shiftKey: true
})
.addShortcut("toggleEngagementRings", {
label: "Hide/show engagement rings",
keyUpCallback: () => this.setOption("showUnitsEngagementRings", !this.getOptions().showUnitsEngagementRings),
code: "KeyQ",
shiftKey: true
})
.addShortcut("toggleHideShortEngagementRings", {
label: "Hide/show short range rings",
keyUpCallback: () => this.setOption("hideUnitsShortRangeRings", !this.getOptions().hideUnitsShortRangeRings),
code: "KeyR",
shiftKey: true
})
.addShortcut("toggleDetectionLines", {
label: "Hide/show detection lines",
keyUpCallback: () => this.setOption("showUnitTargets", !this.getOptions().showUnitTargets),
code: "KeyF",
shiftKey: true
})
.addShortcut("toggleGroupMembers", {
label: "Hide/show group members",
keyUpCallback: () => this.setOption("hideGroupMembers", !this.getOptions().hideGroupMembers),
code: "KeyG",
shiftKey: true
})
.addShortcut("toggleRelativePositions", {
label: "Toggle group movement mode",
keyUpCallback: () => this.setOption("keepRelativePositions", !this.getOptions().keepRelativePositions),
code: "KeyP",
shiftKey: true
})
.addShortcut("increaseCameraZoom", {
label: "Increase camera zoom",
altKey: true,
keyUpCallback: () => this.increaseCameraZoom(),
code: "Equal",
shiftKey: true
})
.addShortcut("decreaseCameraZoom", {
label: "Decrease camera zoom",
altKey: true,
keyUpCallback: () => this.decreaseCameraZoom(),
code: "Minus",
shiftKey: true
});
for (let contextActionName in ContextActions) {
if (ContextActions[contextActionName].getOptions().hotkey) {
const contextAction = ContextActions[contextActionName] as ContextAction;
if (contextAction.getOptions().code) {
getApp()
.getShortcutManager()
.addShortcut(`${contextActionName}Hotkey`, {
label: ContextActions[contextActionName].getLabel(),
code: ContextActions[contextActionName].getOptions().hotkey,
shiftKey: true,
keyUpCallback: () => {
label: contextAction.getLabel(),
code: contextAction.getOptions().code as string,
shiftKey: contextAction.getOptions().shiftKey,
altKey: contextAction.getOptions().altKey,
ctrlKey: contextAction.getOptions().ctrlKey,
keyDownCallback: () => {
const contextActionSet = this.getContextActionSet();
if (
getApp().getState() === OlympusState.UNIT_CONTROL &&
contextActionSet &&
ContextActions[contextActionName].getId() in contextActionSet.getContextActions()
) {
if (ContextActions[contextActionName].getOptions().executeImmediately) ContextActions[contextActionName].executeCallback();
else this.setContextAction(ContextActions[contextActionName]);
if (getApp().getState() === OlympusState.UNIT_CONTROL && contextActionSet && contextAction.getId() in contextActionSet.getContextActions()) {
if (contextAction.getOptions().executeImmediately) contextAction.executeCallback(null, null);
else this.setContextAction(contextAction);
}
},
keyUpCallback: () => {
if (getApp().getState() === OlympusState.UNIT_CONTROL && !contextAction.getOptions().executeImmediately) {
this.setContextAction(null);
}
},
});
@ -517,212 +522,6 @@ export class Map extends L.Map {
ContextActionChangedEvent.dispatch(this.#contextAction);
}
getCurrentControls() {
//const touch = matchMedia("(hover: none)").matches;
//if (getApp().getState() === IDLE) {
// return [
// {
// actions: [touch ? faHandPointer : "LMB"],
// target: faJetFighter,
// text: "Select unit",
// },
// touch
// ? {
// actions: [faHandPointer, "Drag"],
// target: faMap,
// text: "Box selection",
// }
// : {
// actions: ["Shift", "LMB", "Drag"],
// target: faMap,
// text: "Box selection",
// },
// {
// actions: [touch ? faHandPointer : "LMB", "Drag"],
// target: faMap,
// text: "Move map location",
// },
// ];
//} else if (getApp().getState() === OlympusState.SPAWN_UNIT) {
// return [
// {
// actions: [touch ? faHandPointer : "LMB"],
// target: faMap,
// text: "Spawn unit",
// },
// {
// actions: [touch ? faHandPointer : "LMB", 2],
// target: faMap,
// text: "Exit spawn mode",
// },
// {
// actions: [touch ? faHandPointer : "LMB", "Drag"],
// target: faMap,
// text: "Move map location",
// },
// ];
//} else if (getApp().getState() === SPAWN_EFFECT) {
// return [
// {
// actions: [touch ? faHandPointer : "LMB"],
// target: faMap,
// text: "Spawn effect",
// },
// {
// actions: [touch ? faHandPointer : "LMB", 2],
// target: faMap,
// text: "Exit spawn mode",
// },
// {
// actions: [touch ? faHandPointer : "LMB", "Drag"],
// target: faMap,
// text: "Move map location",
// },
// ];
//} else if (getApp().getState() === CONTEXT_ACTION) {
// let controls = [
// {
// actions: [touch ? faHandPointer : "LMB"],
// target: faMap,
// text: "Deselect units",
// },
// {
// actions: [touch ? faHandPointer : "LMB", "Drag"],
// target: faMap,
// text: "Move map location",
// },
// ];
//
// if (this.#contextAction) {
// controls.push({
// actions: [touch ? faHandPointer : "LMB"],
// target: this.#contextAction.getTarget() === "unit" ? faJetFighter : faMap,
// text: this.#contextAction?.getLabel() ?? "",
// });
// }
//
// if (!touch && this.#defaultContextAction) {
// controls.push({
// actions: ["RMB"],
// target: faMap,
// text: this.#defaultContextAction?.getLabel() ?? "",
// });
// controls.push({
// actions: ["RMB", "hold"],
// target: faMap,
// text: "Open context menu",
// });
// }
//
// return controls;
//} else if (getApp().getState() === COALITIONAREA_EDIT) {
// return [
// {
// actions: [touch ? faHandPointer : "LMB"],
// target: faDrawPolygon,
// text: "Select shape",
// },
// {
// actions: [touch ? faHandPointer : "LMB", 2],
// target: faMap,
// text: "Exit drawing mode",
// },
// {
// actions: [touch ? faHandPointer : "LMB", "Drag"],
// target: faMap,
// text: "Move map location",
// },
// ];
//} else if (getApp().getState() === COALITIONAREA_DRAW_POLYGON) {
// return [
// {
// actions: [touch ? faHandPointer : "LMB"],
// target: faMap,
// text: "Add vertex to polygon",
// },
// {
// actions: [touch ? faHandPointer : "LMB", 2],
// target: faMap,
// text: "Finalize polygon",
// },
// {
// actions: [touch ? faHandPointer : "LMB", "Drag"],
// target: faMap,
// text: "Move map location",
// },
// ];
//} else if (getApp().getState() === COALITIONAREA_DRAW_CIRCLE) {
// return [
// {
// actions: [touch ? faHandPointer : "LMB"],
// target: faMap,
// text: "Add circle",
// },
// {
// actions: [touch ? faHandPointer : "LMB", "Drag"],
// target: faMap,
// text: "Move map location",
// },
// ];
//} else if (getApp().getState() === SELECT_JTAC_TARGET) {
// return [
// {
// actions: [touch ? faHandPointer : "LMB"],
// target: faMap,
// text: "Set unit/location as target",
// },
// {
// actions: [touch ? faHandPointer : "LMB", 2],
// target: faMap,
// text: "Exit selection mode",
// },
// {
// actions: [touch ? faHandPointer : "LMB", "Drag"],
// target: faMap,
// text: "Move map location",
// },
// ];
//} else if (getApp().getState() === SELECT_JTAC_ECHO) {
// return [
// {
// actions: [touch ? faHandPointer : "LMB"],
// target: faMap,
// text: "Set location as ECHO point",
// },
// {
// actions: [touch ? faHandPointer : "LMB", 2],
// target: faMap,
// text: "Exit selection mode",
// },
// {
// actions: [touch ? faHandPointer : "LMB", "Drag"],
// target: faMap,
// text: "Move map location",
// },
// ];
//} else if (getApp().getState() === SELECT_JTAC_IP) {
// return [
// {
// actions: [touch ? faHandPointer : "LMB"],
// target: faMap,
// text: "Set location as IP point",
// },
// {
// actions: [touch ? faHandPointer : "LMB", 2],
// target: faMap,
// text: "Exit selection mode",
// },
// {
// actions: [touch ? faHandPointer : "LMB", "Drag"],
// target: faMap,
// text: "Move map location",
// },
// ];
//} else {
// return [];
//}
}
deselectAllCoalitionAreas() {
if (this.getSelectedCoalitionArea() !== null) {
CoalitionAreaSelectedEvent.dispatch(null);
@ -905,12 +704,6 @@ export class Map extends L.Map {
this.#contextActionSet?.getDefaultContextAction()?.executeCallback(targetUnit, targetPosition, originalEvent);
}
preventClicks() {
console.log("Preventing clicks on map");
window.clearTimeout(this.#shortPressTimer);
window.clearTimeout(this.#longPressTimer);
}
/* Event handlers */
#onStateChanged(state: OlympusState, subState: OlympusSubState) {
/* Operations to perform when leaving a state */
@ -979,71 +772,26 @@ export class Map extends L.Map {
#onMouseUp(e: any) {
this.#isMouseDown = false;
window.clearTimeout(this.#longPressTimer);
this.scrollWheelZoom.enable();
this.dragging.enable();
this.#isRotatingDestination = false;
this.#isMouseOnCooldown = true;
this.#mouseCooldownTimer = window.setTimeout(() => {
this.#isMouseOnCooldown = false;
}, 200);
}
#onMouseDown(e: any) {
this.#isMouseDown = true;
if (this.#isMouseOnCooldown) {
return;
}
if (this.#contextAction?.getTarget() === ContextActionTarget.POINT && e.originalEvent?.button === 2) this.#isRotatingDestination = true;
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);
}, 200);
this.#longPressTimer = window.setTimeout(() => {
/* If the mouse is still being pressed, execute the long press action */
if (this.#isMouseDown && !this.#isDragging && !this.#isZooming) this.#onLongPress(e);
}, 350);
}
#onDoubleClick(e: any) {
console.log(`Double click at ${e.latlng}`);
window.clearTimeout(this.#shortPressTimer);
window.clearTimeout(this.#longPressTimer);
if (getApp().getState() === OlympusState.IDLE) {
StarredSpawnContextMenuRequestEvent.dispatch(e.latlng);
getApp().setState(OlympusState.STARRED_SPAWN);
} else {
if (getApp().getSubState() !== NO_SUBSTATE) {
getApp().setState(getApp().getState(), NO_SUBSTATE);
} else {
getApp().setState(OlympusState.IDLE);
}
}
}
#onShortPress(e: any) {
let pressLocation: L.LatLng;
if (e.type === "touchstart") pressLocation = this.containerPointToLatLng(this.mouseEventToContainerPoint(e.touches[0]));
else pressLocation = new L.LatLng(e.latlng.lat, e.latlng.lng);
console.log(`Short press at ${pressLocation}`);
#onLeftClick(e: L.LeafletMouseEvent) {
console.log(`Left click at ${e.latlng}`);
/* Execute the short click action */
if (getApp().getState() === OlympusState.IDLE) {
/* Do nothing */
} else if (getApp().getState() === OlympusState.SPAWN_CONTEXT) {
getApp().setState(OlympusState.IDLE);
} else if (getApp().getState() === OlympusState.SPAWN) {
if (getApp().getSubState() === SpawnSubState.SPAWN_UNIT) {
if (e.originalEvent?.button != 2 && this.#spawnRequestTable !== null) {
this.#spawnRequestTable.unit.location = pressLocation;
if (this.#spawnRequestTable !== null) {
this.#spawnRequestTable.unit.location = e.latlng;
getApp()
.getUnitsManager()
.spawnUnits(
@ -1054,24 +802,23 @@ export class Map extends L.Map {
undefined,
undefined,
(hash) => {
this.addTemporaryMarker(pressLocation, this.#spawnRequestTable?.unit.unitType ?? "unknown", this.#spawnRequestTable?.coalition ?? "blue", hash);
this.addTemporaryMarker(e.latlng, this.#spawnRequestTable?.unit.unitType ?? "unknown", this.#spawnRequestTable?.coalition ?? "blue", hash);
}
);
}
} else if (getApp().getSubState() === SpawnSubState.SPAWN_EFFECT) {
if (e.originalEvent?.button != 2 && this.#effectRequestTable !== null) {
if (this.#effectRequestTable !== null) {
if (this.#effectRequestTable.type === "explosion") {
if (this.#effectRequestTable.explosionType === "High explosive") getApp().getServerManager().spawnExplosion(50, "normal", pressLocation);
else if (this.#effectRequestTable.explosionType === "Napalm") getApp().getServerManager().spawnExplosion(50, "napalm", pressLocation);
else if (this.#effectRequestTable.explosionType === "White phosphorous")
getApp().getServerManager().spawnExplosion(50, "phosphorous", pressLocation);
if (this.#effectRequestTable.explosionType === "High explosive") getApp().getServerManager().spawnExplosion(50, "normal", e.latlng);
else if (this.#effectRequestTable.explosionType === "Napalm") getApp().getServerManager().spawnExplosion(50, "napalm", e.latlng);
else if (this.#effectRequestTable.explosionType === "White phosphorous") getApp().getServerManager().spawnExplosion(50, "phosphorous", e.latlng);
this.addExplosionMarker(pressLocation);
this.addExplosionMarker(e.latlng);
} else if (this.#effectRequestTable.type === "smoke") {
getApp()
.getServerManager()
.spawnSmoke(this.#effectRequestTable.smokeColor ?? "white", pressLocation);
this.addSmokeMarker(pressLocation, this.#effectRequestTable.smokeColor ?? "white");
.spawnSmoke(this.#effectRequestTable.smokeColor ?? "white", e.latlng);
this.addSmokeMarker(e.latlng, this.#effectRequestTable.smokeColor ?? "white");
}
}
}
@ -1079,18 +826,18 @@ export class Map extends L.Map {
if (getApp().getSubState() === DrawSubState.DRAW_POLYGON) {
const selectedArea = this.getSelectedCoalitionArea();
if (selectedArea && selectedArea instanceof CoalitionPolygon) {
selectedArea.addTemporaryLatLng(pressLocation);
selectedArea.addTemporaryLatLng(e.latlng);
}
} else if (getApp().getSubState() === DrawSubState.DRAW_CIRCLE) {
const selectedArea = this.getSelectedCoalitionArea();
if (selectedArea && selectedArea instanceof CoalitionCircle) {
if (selectedArea.getLatLng().lat == 0 && selectedArea.getLatLng().lng == 0) selectedArea.setLatLng(pressLocation);
if (selectedArea.getLatLng().lat == 0 && selectedArea.getLatLng().lng == 0) selectedArea.setLatLng(e.latlng);
getApp().setState(OlympusState.DRAW, DrawSubState.EDIT);
}
} else if (getApp().getSubState() == DrawSubState.NO_SUBSTATE) {
this.deselectAllCoalitionAreas();
for (let idx = 0; idx < this.#coalitionAreas.length; idx++) {
if (areaContains(pressLocation, this.#coalitionAreas[idx])) {
if (areaContains(e.latlng, this.#coalitionAreas[idx])) {
this.#coalitionAreas[idx].setSelected(true);
getApp().setState(OlympusState.DRAW, DrawSubState.EDIT);
break;
@ -1098,17 +845,13 @@ 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, e.originalEvent);
else getApp().setState(OlympusState.IDLE);
} else if (e.originalEvent?.buttons === 2) {
this.executeDefaultContextAction(null, pressLocation, e.originalEvent);
}
if (this.#contextAction !== null) this.executeContextAction(null, e.latlng, e.originalEvent);
else getApp().setState(OlympusState.IDLE);
} else if (getApp().getState() === OlympusState.JTAC) {
// TODO less redundant way to do this
if (getApp().getSubState() === JTACSubState.SELECT_TARGET) {
if (!this.#targetPoint) {
this.#targetPoint = new TextMarker(pressLocation, "BP", "rgb(37 99 235)", { interactive: true, draggable: true });
this.#targetPoint = new TextMarker(e.latlng, "BP", "rgb(37 99 235)", { interactive: true, draggable: true });
this.#targetPoint.addTo(this);
this.#targetPoint.on("dragstart", (event) => {
event.target.options["freeze"] = true;
@ -1120,10 +863,10 @@ export class Map extends L.Map {
this.#targetPoint.on("click", (event) => {
getApp().setState(OlympusState.JTAC);
});
} else this.#targetPoint.setLatLng(pressLocation);
} else this.#targetPoint.setLatLng(e.latlng);
} else if (getApp().getSubState() === JTACSubState.SELECT_ECHO_POINT) {
if (!this.#ECHOPoint) {
this.#ECHOPoint = new TextMarker(pressLocation, "BP", "rgb(37 99 235)", { interactive: true, draggable: true });
this.#ECHOPoint = new TextMarker(e.latlng, "BP", "rgb(37 99 235)", { interactive: true, draggable: true });
this.#ECHOPoint.addTo(this);
this.#ECHOPoint.on("dragstart", (event) => {
event.target.options["freeze"] = true;
@ -1135,10 +878,10 @@ export class Map extends L.Map {
this.#ECHOPoint.on("click", (event) => {
getApp().setState(OlympusState.JTAC);
});
} else this.#ECHOPoint.setLatLng(pressLocation);
} else this.#ECHOPoint.setLatLng(e.latlng);
} else if (getApp().getSubState() === JTACSubState.SELECT_IP) {
if (!this.#IPPoint) {
this.#IPPoint = new TextMarker(pressLocation, "BP", "rgb(37 99 235)", { interactive: true, draggable: true });
this.#IPPoint = new TextMarker(e.latlng, "BP", "rgb(37 99 235)", { interactive: true, draggable: true });
this.#IPPoint.addTo(this);
this.#IPPoint.on("dragstart", (event) => {
event.target.options["freeze"] = true;
@ -1150,7 +893,7 @@ export class Map extends L.Map {
this.#IPPoint.on("click", (event) => {
getApp().setState(OlympusState.JTAC);
});
} else this.#IPPoint.setLatLng(pressLocation);
} else this.#IPPoint.setLatLng(e.latlng);
}
getApp().setState(OlympusState.JTAC);
this.#drawIPToTargetLine();
@ -1158,40 +901,26 @@ export class Map extends L.Map {
}
}
#onLongPress(e: any) {
let pressLocation: L.LatLng;
if (e.type === "touchstart") pressLocation = this.containerPointToLatLng(this.mouseEventToContainerPoint(e.touches[0]));
else pressLocation = new L.LatLng(e.latlng.lat, e.latlng.lng);
#onRightClick(e: L.LeafletMouseEvent) {
e.originalEvent.preventDefault();
console.log(`Long press at ${pressLocation}`);
console.log(`Right click at ${e.latlng}`);
if (!this.#isDragging && !this.#isZooming) {
this.deselectAllCoalitionAreas();
if (getApp().getState() === OlympusState.IDLE) {
if (e.type === "touchstart") document.dispatchEvent(new CustomEvent("forceboxselect", { detail: e }));
else document.dispatchEvent(new CustomEvent("forceboxselect", { detail: e.originalEvent }));
if (getApp().getState() === OlympusState.IDLE || getApp().getState() === OlympusState.SPAWN_CONTEXT) {
SpawnContextMenuRequestEvent.dispatch(e.latlng);
getApp().setState(OlympusState.SPAWN_CONTEXT);
} else if (getApp().getState() === OlympusState.UNIT_CONTROL) {
if (e.originalEvent?.button === 2) {
if (!this.getContextAction()) {
getApp().setState(OlympusState.UNIT_CONTROL, UnitControlSubState.MAP_CONTEXT_MENU);
MapContextMenuRequestEvent.dispatch(pressLocation);
}
} else {
if (this.#contextAction?.getTarget() === ContextActionTarget.POINT) {
this.dragging.disable();
this.#isRotatingDestination = true;
} else {
if (e.type === "touchstart") document.dispatchEvent(new CustomEvent("forceboxselect", { detail: e }));
else document.dispatchEvent(new CustomEvent("forceboxselect", { detail: e.originalEvent }));
}
if (!this.getContextAction()) {
getApp().setState(OlympusState.UNIT_CONTROL, UnitControlSubState.MAP_CONTEXT_MENU);
MapContextMenuRequestEvent.dispatch(e.latlng);
}
}
}
}
#onMouseMove(e: any) {
window.clearTimeout(this.#longPressTimer);
if (!this.#isRotatingDestination) {
this.#lastMousePosition.x = e.originalEvent.x;
this.#lastMousePosition.y = e.originalEvent.y;
@ -1200,8 +929,8 @@ export class Map extends L.Map {
MouseMovedEvent.dispatch(e.latlng);
getGroundElevation(e.latlng, (elevation) => {
MouseMovedEvent.dispatch(e.latlng, elevation);
})
});
if (this.#currentSpawnMarker) this.#currentSpawnMarker.setLatLng(e.latlng);
if (this.#currentEffectMarker) this.#currentEffectMarker.setLatLng(e.latlng);
} else {

View File

@ -8,6 +8,7 @@ export class SmokeMarker extends CustomMarker {
constructor(latlng: LatLngExpression, color: string, options?: MarkerOptions) {
super(latlng, options);
this.options.interactive = false;
this.setZIndexOffset(9999);
this.#color = color;
window.setTimeout(() => {

View File

@ -4,6 +4,7 @@ import { CustomMarker } from "./custommarker";
export class TargetMarker extends CustomMarker {
constructor(latlng: LatLngExpression, options?: MarkerOptions) {
super(latlng, options);
this.options.interactive = false;
this.setZIndexOffset(9999);
}

View File

@ -176,4 +176,8 @@
.ol-explosion-icon {
fill: red;
}
path.leaflet-interactive:focus {
outline: none;
}

View File

@ -4,7 +4,7 @@ import { SVGInjector } from "@tanem/svg-injector";
import { AirbaseChartData, AirbaseOptions } from "../interfaces";
import { getApp } from "../olympusapp";
import { OlympusState } from "../constants/constants";
import { AirbaseSelectedEvent } from "../events";
import { AirbaseSelectedEvent, AppStateChangedEvent } from "../events";
// TODO add ability to select the marker
export class Airbase extends CustomMarker {
@ -27,6 +27,12 @@ export class Airbase extends CustomMarker {
this.#name = options.name;
this.#img = document.createElement("img");
AppStateChangedEvent.on((state, subState) => {
const element = this.getElement();
if (element)
element.style.pointerEvents = (state === OlympusState.IDLE || state === OlympusState.AIRBASE)? "all": "none";
})
AirbaseSelectedEvent.on((airbase) => {
this.#selected = airbase == this;
if (this.getElement()?.querySelector(".airbase-icon"))

View File

@ -1,10 +1,16 @@
import { DivIcon } from "leaflet";
import { DivIcon, LatLngExpression, MarkerOptions } from "leaflet";
import { CustomMarker } from "../map/markers/custommarker";
import { SVGInjector } from "@tanem/svg-injector";
export class Bullseye extends CustomMarker {
#coalition: string = "";
constructor(latlng: LatLngExpression, options?: MarkerOptions) {
super(latlng, options);
this.options.interactive = false;
this.setZIndexOffset(9999);
}
createIcon() {
var icon = new DivIcon({
className: "leaflet-bullseye-marker",

View File

@ -93,11 +93,11 @@ export class OlympusApp {
*/
getExpressAddress() {
return `${window.location.href.split("?")[0].replace("vite/", "").replace("vite", "")}express`
return `${window.location.href.split("?")[0].replace("vite/", "").replace("vite", "")}express`;
}
getBackendAddress() {
return `${window.location.href.split("?")[0].replace("vite/", "").replace("vite", "")}olympus`
return `${window.location.href.split("?")[0].replace("vite/", "").replace("vite", "")}olympus`;
}
start() {
@ -150,6 +150,17 @@ export class OlympusApp {
ConfigLoadedEvent.dispatch(this.#config as OlympusConfig);
this.setState(OlympusState.LOGIN);
});
this.#shortcutManager?.addShortcut("idle", {
label: "Deselect all",
keyUpCallback: (ev: KeyboardEvent) => {
this.setState(OlympusState.IDLE);
},
code: "Escape",
});
this.#shortcutManager.checkShortcuts();
}
getConfig() {

View File

@ -87,29 +87,34 @@ export const zeroPad = function (num: number, places: number) {
return string;
};
export function latLngToMGRS(lat: number, lng: number, precision: number = 4): MGRS | false {
export function latLngToMGRS(lat: number, lng: number, precision: number = 4): MGRS | undefined {
if (precision < 0 || precision > 6) {
console.error("latLngToMGRS: precision must be a number >= 0 and <= 6. Given precision: " + precision);
return false;
return undefined;
}
const mgrs = new Converter({}).LLtoMGRS(lat, lng, precision);
const match = mgrs.match(new RegExp(`^(\\d{2})([A-Z])([A-Z])([A-Z])(\\d+)$`));
const easting = match[5].substr(0, match[5].length / 2);
const northing = match[5].substr(match[5].length / 2);
if (match) {
const easting = match[5].substr(0, match[5].length / 2);
const northing = match[5].substr(match[5].length / 2);
let output: MGRS = {
bandLetter: match[2],
columnLetter: match[3],
groups: [match[1] + match[2], match[3] + match[4], easting, northing],
easting: easting,
northing: northing,
precision: precision,
rowLetter: match[4],
string: match[0],
zoneNumber: match[1],
};
let output: MGRS = {
bandLetter: match[2],
columnLetter: match[3],
groups: [match[1] + match[2], match[3] + match[4], easting, northing],
easting: easting,
northing: northing,
precision: precision,
rowLetter: match[4],
string: match[0],
zoneNumber: match[1],
};
return output;
return output;
} else {
return undefined;
}
}
export function latLngToUTM(lat: number, lng: number) {
@ -317,19 +322,20 @@ export function makeID(length) {
}
export function hash(str, seed = 0) {
let h1 = 0xdeadbeef ^ seed, h2 = 0x41c6ce57 ^ seed;
for(let i = 0, ch; i < str.length; i++) {
ch = str.charCodeAt(i);
h1 = Math.imul(h1 ^ ch, 2654435761);
h2 = Math.imul(h2 ^ ch, 1597334677);
let h1 = 0xdeadbeef ^ seed,
h2 = 0x41c6ce57 ^ seed;
for (let i = 0, ch; i < str.length; i++) {
ch = str.charCodeAt(i);
h1 = Math.imul(h1 ^ ch, 2654435761);
h2 = Math.imul(h2 ^ ch, 1597334677);
}
h1 = Math.imul(h1 ^ (h1 >>> 16), 2246822507);
h1 = Math.imul(h1 ^ (h1 >>> 16), 2246822507);
h1 ^= Math.imul(h2 ^ (h2 >>> 13), 3266489909);
h2 = Math.imul(h2 ^ (h2 >>> 16), 2246822507);
h2 = Math.imul(h2 ^ (h2 >>> 16), 2246822507);
h2 ^= Math.imul(h1 ^ (h1 >>> 13), 3266489909);
return `${4294967296 * (2097151 & h2) + (h1 >>> 0)}`;
};
}
export function byteArrayToInteger(array) {
let res = 0;

View File

@ -42,7 +42,7 @@ export class ServerManager {
keyUpCallback: () => {
this.setPaused(!this.getPaused());
},
code: "Space"
code: "Enter"
})
}

View File

@ -1,22 +1,26 @@
import { ShortcutChangedEvent, ShortcutsChangedEvent } from "../events";
import { AppStateChangedEvent, ShortcutChangedEvent, ShortcutsChangedEvent } from "../events";
import { ShortcutOptions } from "../interfaces";
import { keyEventWasInInput } from "../other/utils";
export class Shortcut {
#id: string;
#options: ShortcutOptions;
#keydown: boolean = false;
constructor(id, options: ShortcutOptions) {
this.#id = id;
this.#options = options;
AppStateChangedEvent.on(() => this.#keydown = false)
/* Key up event is mandatory */
document.addEventListener("keyup", (ev: any) => {
this.#keydown = false;
if (keyEventWasInInput(ev) || options.code !== ev.code) return;
if (
(typeof options.altKey !== "boolean" || (typeof options.altKey === "boolean" && ev.altKey === options.altKey)) &&
(typeof options.ctrlKey !== "boolean" || (typeof options.ctrlKey === "boolean" && ev.ctrlKey === options.ctrlKey)) &&
(typeof options.shiftKey !== "boolean" || (typeof options.shiftKey === "boolean" && ev.shiftKey === options.shiftKey))
ev.altKey === (options.altKey ?? ev.code.indexOf("Alt") >= 0) &&
ev.ctrlKey === (options.ctrlKey ?? ev.code.indexOf("Ctrl") >= 0) &&
ev.shiftKey === (options.shiftKey ?? ev.code.indexOf("Shift") >= 0)
)
options.keyUpCallback(ev);
});
@ -24,11 +28,12 @@ export class Shortcut {
/* Key down event is optional */
if (options.keyDownCallback) {
document.addEventListener("keydown", (ev: any) => {
if (keyEventWasInInput(ev) || options.code !== ev.code) return;
if (this.#keydown || keyEventWasInInput(ev) || options.code !== ev.code) return;
this.#keydown = true;
if (
(typeof options.altKey !== "boolean" || (typeof options.altKey === "boolean" && ev.altKey === options.altKey)) &&
(typeof options.ctrlKey !== "boolean" || (typeof options.ctrlKey === "boolean" && ev.ctrlKey === options.ctrlKey)) &&
(typeof options.shiftKey !== "boolean" || (typeof options.shiftKey === "boolean" && ev.shiftKey === options.shiftKey))
ev.altKey === (options.altKey ?? ev.code.indexOf("Alt") >= 0) &&
ev.ctrlKey === (options.ctrlKey ?? ev.code.indexOf("Control") >= 0) &&
ev.shiftKey === (options.shiftKey ?? ev.code.indexOf("Shift") >= 0)
)
if (options.keyDownCallback) options.keyDownCallback(ev);
});

View File

@ -39,4 +39,24 @@ export class ShortcutManager {
this.#shortcuts[id].setOptions(shortcutOptions);
ShortcutsChangedEvent.dispatch(this.#shortcuts);
}
checkShortcuts() {
for (let id in this.#shortcuts) {
const shortcut = this.#shortcuts[id];
for (let otherid in this.#shortcuts) {
if (id != otherid) {
const otherShortcut = this.#shortcuts[otherid];
if (shortcut.getOptions().code === otherShortcut.getOptions().code) {
if (
(shortcut.getOptions().altKey ?? false) === (otherShortcut.getOptions().altKey ?? false) &&
(shortcut.getOptions().ctrlKey ?? false) === (otherShortcut.getOptions().ctrlKey ?? false) &&
(shortcut.getOptions().shiftKey ?? false) === (otherShortcut.getOptions().shiftKey ?? false)
) {
console.error("Duplicate shortcut: " + shortcut.getOptions().label + " and " + otherShortcut.getOptions().label)
}
}
}
}
}
}
}

View File

@ -26,6 +26,7 @@ export type MapOptions = {
cameraPluginRatio: number;
cameraPluginEnabled: boolean;
cameraPluginMode: string;
tabletMode: boolean;
};
export type MapHiddenTypes = {

View File

@ -1,7 +1,7 @@
import React from "react";
import { Coalition } from "../../types/types";
export function OlCoalitionToggle(props: { coalition: Coalition | undefined; onClick: () => void }) {
export function OlCoalitionToggle(props: { coalition: Coalition | undefined; onClick: () => void; showLabel?: boolean }) {
return (
<div className="inline-flex cursor-pointer items-center" onClick={props.onClick}>
<button className="peer sr-only" />
@ -26,15 +26,17 @@ export function OlCoalitionToggle(props: { coalition: Coalition | undefined; onC
rtl:data-[coalition='red']:after:-translate-x-full
`}
></div>
<span
className={`
ms-3 overflow-hidden text-ellipsis text-nowrap text-gray-900
dark:text-white
data-[flash='true']:after:animate-pulse
`}
>
{props.coalition ? `${props.coalition[0].toLocaleUpperCase() + props.coalition.substring(1)}` : "Diff. values"}
</span>
{props.showLabel && (
<span
className={`
ms-3 overflow-hidden text-ellipsis text-nowrap text-gray-900
dark:text-white
data-[flash='true']:after:animate-pulse
`}
>
{props.coalition ? `${props.coalition[0].toLocaleUpperCase() + props.coalition.substring(1)}` : "Diff. values"}
</span>
)}
</div>
);
}

View File

@ -113,7 +113,6 @@ export function MapContextMenu(props: {}) {
contextActionIt.executeCallback(unit, null);
}
}
getApp().setState(OlympusState.UNIT_CONTROL);
}}
>
<FontAwesomeIcon className="my-auto" icon={contextActionIt.getIcon()} />

View File

@ -0,0 +1,606 @@
import React, { useEffect, useRef, useState } from "react";
import { BLUE_COMMANDER, COMMAND_MODE_OPTIONS_DEFAULTS, GAME_MASTER, NO_SUBSTATE, OlympusState, OlympusSubState } from "../../constants/constants";
import { LatLng } from "leaflet";
import {
AppStateChangedEvent,
CommandModeOptionsChangedEvent,
SpawnContextMenuRequestEvent,
StarredSpawnsChangedEvent,
UnitDatabaseLoadedEvent,
} from "../../events";
import { getApp } from "../../olympusapp";
import { SpawnRequestTable, UnitBlueprint } from "../../interfaces";
import { faArrowLeft, faEllipsisVertical, faExplosion, faListDots, faSearch, faSmog, faStar } from "@fortawesome/free-solid-svg-icons";
import { EffectSpawnMenu } from "../panels/effectspawnmenu";
import { UnitSpawnMenu } from "../panels/unitspawnmenu";
import { OlEffectListEntry } from "../components/oleffectlistentry";
import {
olButtonsVisibilityAircraft,
olButtonsVisibilityGroundunit,
olButtonsVisibilityGroundunitSam,
olButtonsVisibilityHelicopter,
olButtonsVisibilityNavyunit,
} from "../components/olicons";
import { OlUnitListEntry } from "../components/olunitlistentry";
import { OlSearchBar } from "../components/olsearchbar";
import { OlStateButton } from "../components/olstatebutton";
import { OlDropdownItem } from "../components/oldropdown";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { OlCoalitionToggle } from "../components/olcoalitiontoggle";
import { Coalition } from "../../types/types";
import { CompactUnitSpawnMenu } from "../panels/compactunitspawnmenu";
import { CompactEffectSpawnMenu } from "../panels/compacteffectspawnmenu";
enum CategoryGroup {
NONE,
AIRCRAFT,
HELICOPTER,
AIR_DEFENCE,
GROUND_UNIT,
NAVY_UNIT,
EFFECT,
SEARCH,
STARRED,
}
export function SpawnContextMenu(props: {}) {
const [appState, setAppState] = useState(OlympusState.NOT_INITIALIZED);
const [appSubState, setAppSubState] = useState(NO_SUBSTATE as OlympusSubState);
const [xPosition, setXPosition] = useState(0);
const [yPosition, setYPosition] = useState(0);
const [latlng, setLatLng] = useState(null as null | LatLng);
const [starredSpawns, setStarredSpawns] = useState({} as { [key: string]: SpawnRequestTable });
const [openAccordion, setOpenAccordion] = useState(CategoryGroup.NONE);
const [blueprint, setBlueprint] = useState(null as null | UnitBlueprint);
const [effect, setEffect] = useState(null as null | string);
const [filterString, setFilterString] = useState("");
const [selectedRole, setSelectedRole] = useState(null as null | string);
const [selectedType, setSelectedType] = useState(null as null | string);
const [blueprints, setBlueprints] = useState([] as UnitBlueprint[]);
const [roles, setRoles] = useState({ aircraft: [] as string[], helicopter: [] as string[] });
const [types, setTypes] = useState({ groundunit: [] as string[], navyunit: [] as string[] });
const [commandModeOptions, setCommandModeOptions] = useState(COMMAND_MODE_OPTIONS_DEFAULTS);
const [showCost, setShowCost] = useState(false);
const [spawnCoalition, setSpawnCoalition] = useState("blue" as Coalition);
const [showMore, setShowMore] = useState(false);
useEffect(() => {
if (selectedRole) setBlueprints(getApp()?.getUnitsManager().getDatabase().getByRole(selectedRole));
else if (selectedType) setBlueprints(getApp()?.getUnitsManager().getDatabase().getByType(selectedType));
else setBlueprints(getApp()?.getUnitsManager().getDatabase().getBlueprints());
}, [selectedRole, selectedType, openAccordion]);
useEffect(() => {
UnitDatabaseLoadedEvent.on(() => {
setRoles({
aircraft: getApp()
?.getUnitsManager()
.getDatabase()
.getRoles((unit) => unit.category === "aircraft"),
helicopter: getApp()
?.getUnitsManager()
.getDatabase()
.getRoles((unit) => unit.category === "helicopter"),
});
setTypes({
groundunit: getApp()
?.getUnitsManager()
.getDatabase()
.getTypes((unit) => unit.category === "groundunit"),
navyunit: getApp()
?.getUnitsManager()
.getDatabase()
.getTypes((unit) => unit.category === "navyunit"),
});
});
CommandModeOptionsChangedEvent.on((commandModeOptions) => {
setCommandModeOptions(commandModeOptions);
setShowCost(!(commandModeOptions.commandMode == GAME_MASTER || !commandModeOptions.restrictSpawns));
setOpenAccordion(CategoryGroup.NONE);
});
StarredSpawnsChangedEvent.on((starredSpawns) => setStarredSpawns({ ...starredSpawns }));
}, []);
useEffect(() => {
setBlueprint(null);
setEffect(null);
setSelectedType(null);
setSelectedRole(null);
}, [openAccordion]);
/* Filter the blueprints according to the label */
const filteredBlueprints: UnitBlueprint[] = [];
if (blueprints && filterString !== "") {
blueprints.forEach((blueprint) => {
if (blueprint.enabled && (filterString === "" || blueprint.label.toLowerCase().includes(filterString.toLowerCase()))) filteredBlueprints.push(blueprint);
});
}
var contentRef = useRef(null);
useEffect(() => {
AppStateChangedEvent.on((state, subState) => {
setAppState(state);
setAppSubState(subState);
});
StarredSpawnsChangedEvent.on((starredSpawns) => setStarredSpawns({ ...starredSpawns }));
SpawnContextMenuRequestEvent.on((latlng) => {
setLatLng(latlng);
const containerPoint = getApp().getMap().latLngToContainerPoint(latlng);
setXPosition(getApp().getMap().getContainer().offsetLeft + containerPoint.x);
setYPosition(getApp().getMap().getContainer().offsetTop + containerPoint.y);
});
}, []);
useEffect(() => {
if (contentRef.current) {
const content = contentRef.current as HTMLDivElement;
content.style.left = `${xPosition}px`;
content.style.top = `${yPosition}px`;
let newXPosition = xPosition;
let newYposition = yPosition;
let [cxr, cyb] = [content.getBoundingClientRect().x + content.clientWidth, content.getBoundingClientRect().y + content.clientHeight];
/* Try and move the content so it is inside the screen */
if (cxr > window.innerWidth) newXPosition -= cxr - window.innerWidth;
if (cyb > window.innerHeight) newYposition -= cyb - window.innerHeight;
content.style.left = `${newXPosition}px`;
content.style.top = `${newYposition}px`;
}
});
return (
<>
{appState === OlympusState.SPAWN_CONTEXT && (
<>
<div
ref={contentRef}
className={`
absolute flex w-[395px] flex-wrap gap-2 rounded-md bg-olympus-800
`}
>
<div className="flex w-full flex-col gap-4 px-6 py-3">
<div className="flex flex-wrap justify-between gap-2">
<OlCoalitionToggle
coalition={spawnCoalition}
onClick={() => {
spawnCoalition === "blue" && setSpawnCoalition("neutral");
spawnCoalition === "neutral" && setSpawnCoalition("red");
spawnCoalition === "red" && setSpawnCoalition("blue");
}}
/>
<OlStateButton
checked={openAccordion === CategoryGroup.AIRCRAFT}
onClick={() => (openAccordion !== CategoryGroup.AIRCRAFT ? setOpenAccordion(CategoryGroup.AIRCRAFT) : setOpenAccordion(CategoryGroup.NONE))}
icon={olButtonsVisibilityAircraft}
tooltip="Show aircraft units"
buttonColor={spawnCoalition === "blue" ? "#2563eb" : spawnCoalition === "neutral" ? "#9ca3af" : "#ef4444"}
/>
<OlStateButton
checked={openAccordion === CategoryGroup.HELICOPTER}
onClick={() =>
openAccordion !== CategoryGroup.HELICOPTER ? setOpenAccordion(CategoryGroup.HELICOPTER) : setOpenAccordion(CategoryGroup.NONE)
}
icon={olButtonsVisibilityHelicopter}
tooltip="Show helicopter units"
buttonColor={spawnCoalition === "blue" ? "#2563eb" : spawnCoalition === "neutral" ? "#9ca3af" : "#ef4444"}
/>
<OlStateButton
checked={openAccordion === CategoryGroup.AIR_DEFENCE}
onClick={() =>
openAccordion !== CategoryGroup.AIR_DEFENCE ? setOpenAccordion(CategoryGroup.AIR_DEFENCE) : setOpenAccordion(CategoryGroup.NONE)
}
icon={olButtonsVisibilityGroundunitSam}
tooltip="Show air defence units"
buttonColor={spawnCoalition === "blue" ? "#2563eb" : spawnCoalition === "neutral" ? "#9ca3af" : "#ef4444"}
/>
<OlStateButton
checked={openAccordion === CategoryGroup.GROUND_UNIT}
onClick={() =>
openAccordion !== CategoryGroup.GROUND_UNIT ? setOpenAccordion(CategoryGroup.GROUND_UNIT) : setOpenAccordion(CategoryGroup.NONE)
}
icon={olButtonsVisibilityGroundunit}
tooltip="Show ground units"
buttonColor={spawnCoalition === "blue" ? "#2563eb" : spawnCoalition === "neutral" ? "#9ca3af" : "#ef4444"}
/>
<OlStateButton
checked={openAccordion === CategoryGroup.NAVY_UNIT}
onClick={() => (openAccordion !== CategoryGroup.NAVY_UNIT ? setOpenAccordion(CategoryGroup.NAVY_UNIT) : setOpenAccordion(CategoryGroup.NONE))}
icon={olButtonsVisibilityNavyunit}
tooltip="Show navy units"
buttonColor={spawnCoalition === "blue" ? "#2563eb" : spawnCoalition === "neutral" ? "#9ca3af" : "#ef4444"}
/>
<OlStateButton checked={showMore} onClick={() => setShowMore(!showMore)} icon={faEllipsisVertical} tooltip="Show more options" />
{showMore && (
<>
<OlStateButton
checked={openAccordion === CategoryGroup.EFFECT}
onClick={() => (openAccordion !== CategoryGroup.EFFECT ? setOpenAccordion(CategoryGroup.EFFECT) : setOpenAccordion(CategoryGroup.NONE))}
icon={faExplosion}
tooltip="Show effects"
className="ml-auto"
/>
<OlStateButton
checked={openAccordion === CategoryGroup.SEARCH}
onClick={() => (openAccordion !== CategoryGroup.SEARCH ? setOpenAccordion(CategoryGroup.SEARCH) : setOpenAccordion(CategoryGroup.NONE))}
icon={faSearch}
tooltip="Search unit"
/>
<OlStateButton
checked={openAccordion === CategoryGroup.STARRED}
onClick={() => (openAccordion !== CategoryGroup.STARRED ? setOpenAccordion(CategoryGroup.STARRED) : setOpenAccordion(CategoryGroup.NONE))}
icon={faStar}
tooltip="Show starred spanws"
/>
</>
)}
</div>
{blueprint === null && effect === null && openAccordion !== CategoryGroup.NONE && (
<div className="mb-3 flex flex-col gap-4">
<>
<>
{openAccordion === CategoryGroup.AIRCRAFT && (
<>
<div className="flex flex-wrap gap-1">
{roles.aircraft.sort().map((role) => {
return (
<div
key={role}
data-selected={selectedRole === role}
className={`
cursor-pointer rounded-full bg-olympus-900
px-2 py-0.5 text-xs font-bold
text-olympus-50
data-[selected='true']:bg-blue-500
data-[selected='true']:text-gray-200
`}
onClick={() => {
selectedRole === role ? setSelectedRole(null) : setSelectedRole(role);
}}
>
{role}
</div>
);
})}
</div>
<div
className={`
flex max-h-[350px] flex-col gap-1
overflow-y-scroll no-scrollbar
`}
>
{blueprints
?.sort((a, b) => (a.label > b.label ? 1 : -1))
.filter((blueprint) => blueprint.category === "aircraft")
.map((blueprint) => {
return (
<OlUnitListEntry
key={blueprint.name}
icon={olButtonsVisibilityAircraft}
blueprint={blueprint}
onClick={() => setBlueprint(blueprint)}
showCost={showCost}
cost={getApp().getUnitsManager().getDatabase().getSpawnPointsByName(blueprint.name)}
/>
);
})}
</div>
</>
)}
{openAccordion === CategoryGroup.HELICOPTER && (
<>
<div className="flex flex-wrap gap-1">
{roles.helicopter.sort().map((role) => {
return (
<div
key={role}
data-selected={selectedRole === role}
className={`
cursor-pointer rounded-full bg-olympus-900
px-2 py-0.5 text-xs font-bold
text-olympus-50
data-[selected='true']:bg-blue-500
data-[selected='true']:text-gray-200
`}
onClick={() => {
selectedRole === role ? setSelectedRole(null) : setSelectedRole(role);
}}
>
{role}
</div>
);
})}
</div>
<div
className={`
flex max-h-[350px] flex-col gap-1
overflow-y-scroll no-scrollbar
`}
>
{blueprints
?.sort((a, b) => (a.label > b.label ? 1 : -1))
.filter((blueprint) => blueprint.category === "helicopter")
.map((blueprint) => {
return (
<OlUnitListEntry
key={blueprint.name}
icon={olButtonsVisibilityHelicopter}
blueprint={blueprint}
onClick={() => setBlueprint(blueprint)}
showCost={showCost}
cost={getApp().getUnitsManager().getDatabase().getSpawnPointsByName(blueprint.name)}
/>
);
})}
</div>
</>
)}
{openAccordion === CategoryGroup.AIR_DEFENCE && (
<>
<div className="flex flex-wrap gap-1">
{types.groundunit
.sort()
?.filter((type) => type === "SAM Site" || type === "AAA")
.map((type) => {
return (
<div
key={type}
data-selected={selectedType === type}
className={`
cursor-pointer rounded-full bg-olympus-900
px-2 py-0.5 text-xs font-bold
text-olympus-50
data-[selected='true']:bg-blue-500
data-[selected='true']:text-gray-200
`}
onClick={() => {
selectedType === type ? setSelectedType(null) : setSelectedType(type);
}}
>
{type}
</div>
);
})}
</div>
<div
className={`
flex max-h-[350px] flex-col gap-1
overflow-y-scroll no-scrollbar
`}
>
{blueprints
?.sort((a, b) => (a.label > b.label ? 1 : -1))
.filter((blueprint) => blueprint.category === "groundunit" && (blueprint.type === "SAM Site" || blueprint.type === "AAA"))
.map((blueprint) => {
return (
<OlUnitListEntry
key={blueprint.name}
icon={olButtonsVisibilityGroundunit}
blueprint={blueprint}
onClick={() => setBlueprint(blueprint)}
showCost={showCost}
cost={getApp().getUnitsManager().getDatabase().getSpawnPointsByName(blueprint.name)}
/>
);
})}
</div>
</>
)}
{openAccordion === CategoryGroup.GROUND_UNIT && (
<>
<div className="flex flex-wrap gap-1">
{types.groundunit
.sort()
?.filter((type) => type !== "SAM Site" && type !== "AAA")
.map((type) => {
return (
<div
key={type}
data-selected={selectedType === type}
className={`
cursor-pointer rounded-full bg-olympus-900
px-2 py-0.5 text-xs font-bold
text-olympus-50
data-[selected='true']:bg-blue-500
data-[selected='true']:text-gray-200
`}
onClick={() => {
selectedType === type ? setSelectedType(null) : setSelectedType(type);
}}
>
{type}
</div>
);
})}
</div>
<div
className={`
flex max-h-[350px] flex-col gap-1
overflow-y-scroll no-scrollbar
`}
>
{blueprints
?.sort((a, b) => (a.label > b.label ? 1 : -1))
.filter((blueprint) => blueprint.category === "groundunit" && blueprint.type !== "SAM Site" && blueprint.type !== "AAA")
.map((blueprint) => {
return (
<OlUnitListEntry
key={blueprint.name}
icon={olButtonsVisibilityGroundunit}
blueprint={blueprint}
onClick={() => setBlueprint(blueprint)}
showCost={showCost}
cost={getApp().getUnitsManager().getDatabase().getSpawnPointsByName(blueprint.name)}
/>
);
})}
</div>
</>
)}
{openAccordion === CategoryGroup.NAVY_UNIT && (
<>
<div className="flex flex-wrap gap-1">
{types.navyunit.sort().map((type) => {
return (
<div
key={type}
data-selected={selectedType === type}
className={`
cursor-pointer rounded-full bg-olympus-900
px-2 py-0.5 text-xs font-bold
text-olympus-50
data-[selected='true']:bg-blue-500
data-[selected='true']:text-gray-200
`}
onClick={() => {
selectedType === type ? setSelectedType(null) : setSelectedType(type);
}}
>
{type}
</div>
);
})}
</div>
<div
className={`
flex max-h-[350px] flex-col gap-1
overflow-y-scroll no-scrollbar
`}
>
{blueprints
?.sort((a, b) => (a.label > b.label ? 1 : -1))
.filter((blueprint) => blueprint.category === "navyunit")
.map((blueprint) => {
return (
<OlUnitListEntry
key={blueprint.name}
icon={olButtonsVisibilityNavyunit}
blueprint={blueprint}
onClick={() => setBlueprint(blueprint)}
showCost={showCost}
cost={getApp().getUnitsManager().getDatabase().getSpawnPointsByName(blueprint.name)}
/>
);
})}
</div>
</>
)}
{openAccordion === CategoryGroup.EFFECT && (
<>
<div
className={`
flex max-h-[350px] flex-col gap-1
overflow-y-scroll no-scrollbar
`}
>
<OlEffectListEntry
key={"explosion"}
icon={faExplosion}
label={"Explosion"}
onClick={() => {
setEffect("explosion");
}}
/>
<OlEffectListEntry
key={"smoke"}
icon={faSmog}
label={"Smoke"}
onClick={() => {
setEffect("smoke");
}}
/>
</div>
</>
)}
{openAccordion === CategoryGroup.SEARCH && (
<div className="flex flex-col gap-2">
<OlSearchBar onChange={(value) => setFilterString(value)} text={filterString} />
<div
className={`
flex max-h-[350px] flex-col gap-1
overflow-y-scroll no-scrollbar
`}
>
{filteredBlueprints.length > 0 ? (
filteredBlueprints.map((blueprint) => {
return (
<OlUnitListEntry
key={blueprint.name}
icon={olButtonsVisibilityNavyunit}
blueprint={blueprint}
onClick={() => setBlueprint(blueprint)}
showCost={showCost}
cost={getApp().getUnitsManager().getDatabase().getSpawnPointsByName(blueprint.name)}
/>
);
})
) : filterString === "" ? (
<span className={`text-gray-200`}>Type to search</span>
) : (
<span className={`text-gray-200`}>No results</span>
)}
</div>
</div>
)}
{openAccordion === CategoryGroup.STARRED && (
<div className="flex flex-col gap-2">
{Object.values(starredSpawns).length > 0 ? (
Object.values(starredSpawns).map((spawnRequestTable) => {
return (
<OlDropdownItem
className={`
flex w-full content-center gap-2 text-sm
text-white
`}
onClick={() => {
if (latlng) {
spawnRequestTable.unit.location = latlng;
getApp()
.getUnitsManager()
.spawnUnits(spawnRequestTable.category, Array(spawnRequestTable.amount).fill(spawnRequestTable.unit), spawnRequestTable.coalition, false);
getApp().setState(OlympusState.IDLE);
}
}}
>
<FontAwesomeIcon
data-coalition={spawnRequestTable.coalition}
className={`
my-auto
data-[coalition='blue']:text-blue-500
data-[coalition='neutral']:text-gay-500
data-[coalition='red']:text-red-500
`}
icon={faStar}
/>
<div>
{getApp().getUnitsManager().getDatabase().getByName(spawnRequestTable.unit.unitType)?.label} (
{spawnRequestTable.quickAccessName})
</div>
</OlDropdownItem>
);
})
) : (
<div className="p-2 text-sm text-white">No starred spawns, use the spawn menu to create a quick access spawn</div>
)}
</div>
)}
</>
</>
</div>
)}
{!(blueprint === null) && <CompactUnitSpawnMenu blueprint={blueprint} starredSpawns={starredSpawns} latlng={latlng} coalition={spawnCoalition} onBack={() => setBlueprint(null)}/>}
{!(effect === null) && latlng && <CompactEffectSpawnMenu effect={effect} latlng={latlng} onBack={() => setEffect(null)} />}
</div>
</div>
</>
)}
</>
);
}

View File

@ -1,108 +0,0 @@
import React, { useEffect, useRef, useState } from "react";
import { NO_SUBSTATE, OlympusState, OlympusSubState } from "../../constants/constants";
import { OlDropdownItem } from "../components/oldropdown";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { LatLng } from "leaflet";
import { AppStateChangedEvent, StarredSpawnContextMenuRequestEvent, StarredSpawnsChangedEvent } from "../../events";
import { getApp } from "../../olympusapp";
import { SpawnRequestTable } from "../../interfaces";
import { faStar } from "@fortawesome/free-solid-svg-icons";
export function StarredSpawnContextMenu(props: {}) {
const [appState, setAppState] = useState(OlympusState.NOT_INITIALIZED);
const [appSubState, setAppSubState] = useState(NO_SUBSTATE as OlympusSubState);
const [xPosition, setXPosition] = useState(0);
const [yPosition, setYPosition] = useState(0);
const [latlng, setLatLng] = useState(null as null | LatLng);
const [starredSpawns, setStarredSpawns] = useState({} as { [key: string]: SpawnRequestTable });
var contentRef = useRef(null);
useEffect(() => {
AppStateChangedEvent.on((state, subState) => {
setAppState(state);
setAppSubState(subState);
});
StarredSpawnsChangedEvent.on((starredSpawns) => setStarredSpawns({ ...starredSpawns }));
StarredSpawnContextMenuRequestEvent.on((latlng) => {
setLatLng(latlng);
const containerPoint = getApp().getMap().latLngToContainerPoint(latlng);
setXPosition(getApp().getMap().getContainer().offsetLeft + containerPoint.x);
setYPosition(getApp().getMap().getContainer().offsetTop + containerPoint.y);
});
}, []);
useEffect(() => {
if (contentRef.current) {
const content = contentRef.current as HTMLDivElement;
content.style.left = `${xPosition}px`;
content.style.top = `${yPosition}px`;
let newXPosition = xPosition;
let newYposition = yPosition;
let [cxr, cyb] = [content.getBoundingClientRect().x + content.clientWidth, content.getBoundingClientRect().y + content.clientHeight];
/* Try and move the content so it is inside the screen */
if (cxr > window.innerWidth) newXPosition -= cxr - window.innerWidth;
if (cyb > window.innerHeight) newYposition -= cyb - window.innerHeight;
content.style.left = `${newXPosition}px`;
content.style.top = `${newYposition}px`;
}
});
return (
<>
{appState === OlympusState.STARRED_SPAWN && (
<>
<div
ref={contentRef}
className={`
absolute flex min-w-80 max-w-80 gap-2 rounded-md bg-olympus-600
`}
>
<div
className={`
flex w-full flex-col gap-2 overflow-y-auto no-scrollbar p-2
`}
>
{Object.values(starredSpawns).length > 0? Object.values(starredSpawns).map((spawnRequestTable) => {
return (
<OlDropdownItem
className={`
flex w-full content-center gap-2 text-sm text-white
`}
onClick={() => {
if (latlng) {
spawnRequestTable.unit.location = latlng;
getApp().getUnitsManager().spawnUnits(spawnRequestTable.category, [spawnRequestTable.unit], spawnRequestTable.coalition, false);
getApp().setState(OlympusState.IDLE)
}
}}
>
<FontAwesomeIcon
data-coalition={spawnRequestTable.coalition}
className={`
my-auto
data-[coalition='blue']:text-blue-500
data-[coalition='neutral']:text-gay-500
data-[coalition='red']:text-red-500
`}
icon={faStar}
/>
<div>
{getApp().getUnitsManager().getDatabase().getByName(spawnRequestTable.unit.unitType)?.label} ({spawnRequestTable.quickAccessName})
</div>
</OlDropdownItem>
);
}):
<div className="p-2 text-sm text-white">No starred spawns, use the spawn menu to create a quick access spawn</div>}
</div>
</div>
</>
)}
</>
);
}

View File

@ -272,7 +272,6 @@ export function AirbaseMenu(props: { open: boolean; onClose: () => void; childre
<UnitSpawnMenu
blueprint={blueprint}
starredSpawns={starredSpawns}
spawnAtLocation={false}
airbase={airbase}
coalition={(airbase?.getCoalition() ?? "blue") as Coalition}
/>

View File

@ -0,0 +1,101 @@
import React, { useEffect, useState } from "react";
import { OlDropdown, OlDropdownItem } from "../components/oldropdown";
import { getApp } from "../../olympusapp";
import { OlympusState, SpawnSubState } from "../../constants/constants";
import { OlStateButton } from "../components/olstatebutton";
import { faArrowLeft, faSmog } from "@fortawesome/free-solid-svg-icons";
import { LatLng } from "leaflet";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
export function CompactEffectSpawnMenu(props: { effect: string; latlng: LatLng; onBack: () => void }) {
const [explosionType, setExplosionType] = useState("High explosive");
const [smokeColor, setSmokeColor] = useState("white");
return (
<div className="flex h-full flex-col gap-4">
{props.effect === "explosion" && (
<>
<div className="flex">
<FontAwesomeIcon
onClick={props.onBack}
icon={faArrowLeft}
className={`
my-auto mr-1 h-4 cursor-pointer rounded-md p-2
dark:text-gray-500 dark:hover:bg-gray-700 dark:hover:text-white
`}
/>
<span className="my-auto text-white">Explosion type</span>
</div>
<OlDropdown label={explosionType} className="w-full">
{["High explosive", "Napalm", "White phosphorous"].map((optionExplosionType) => {
return (
<OlDropdownItem
key={optionExplosionType}
onClick={() => {
setExplosionType(optionExplosionType);
}}
>
{optionExplosionType}
</OlDropdownItem>
);
})}
</OlDropdown>
</>
)}
{props.effect === "smoke" && (
<>
<div className="flex">
<FontAwesomeIcon
onClick={props.onBack}
icon={faArrowLeft}
className={`
my-auto mr-1 h-4 cursor-pointer rounded-md p-2
dark:text-gray-500 dark:hover:bg-gray-700 dark:hover:text-white
`}
/>
<span className="my-auto text-white">Smoke color</span>
</div>
<div className="flex w-full gap-2">
{["white", "blue", "red", "green", "orange"].map((optionSmokeColor) => {
return (
<OlStateButton
checked={smokeColor === optionSmokeColor}
icon={faSmog}
onClick={() => {
setSmokeColor(optionSmokeColor);
}}
tooltip=""
buttonColor={optionSmokeColor}
/>
);
})}
</div>
</>
)}
<button
type="button"
className={`
m-2 rounded-lg bg-blue-600 px-5 py-2.5 text-sm font-medium text-white
focus:outline-none focus:ring-4
`}
onClick={() => {
if (props.effect === "explosion") {
if (explosionType === "High explosive") getApp().getServerManager().spawnExplosion(50, "normal", props.latlng);
else if (explosionType === "Napalm") getApp().getServerManager().spawnExplosion(50, "napalm", props.latlng);
else if (explosionType === "White phosphorous") getApp().getServerManager().spawnExplosion(50, "phosphorous", props.latlng);
getApp().getMap().addExplosionMarker(props.latlng);
} else if (props.effect === "smoke") {
getApp().getServerManager().spawnSmoke(smokeColor, props.latlng);
getApp()
.getMap()
.addSmokeMarker(props.latlng, smokeColor ?? "white");
}
getApp().setState(OlympusState.IDLE);
}}
>
Spawn
</button>
</div>
);
}

View File

@ -0,0 +1,445 @@
import React, { useState, useEffect, useCallback } from "react";
import { OlUnitSummary } from "../components/olunitsummary";
import { OlCoalitionToggle } from "../components/olcoalitiontoggle";
import { OlNumberInput } from "../components/olnumberinput";
import { OlLabelToggle } from "../components/ollabeltoggle";
import { OlRangeSlider } from "../components/olrangeslider";
import { OlDropdownItem, OlDropdown } from "../components/oldropdown";
import { LoadoutBlueprint, SpawnRequestTable, UnitBlueprint } from "../../interfaces";
import { OlStateButton } from "../components/olstatebutton";
import { Coalition } from "../../types/types";
import { getApp } from "../../olympusapp";
import { ftToM, hash } from "../../other/utils";
import { LatLng } from "leaflet";
import { Airbase } from "../../mission/airbase";
import { altitudeIncrements, groupUnitCount, maxAltitudeValues, minAltitudeValues, OlympusState, SpawnSubState } from "../../constants/constants";
import { faArrowLeft, faStar } from "@fortawesome/free-solid-svg-icons";
import { OlStringInput } from "../components/olstringinput";
import { countryCodes } from "../data/codes";
import { OlAccordion } from "../components/olaccordion";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
export function CompactUnitSpawnMenu(props: {
starredSpawns: { [key: string]: SpawnRequestTable };
blueprint: UnitBlueprint;
onBack: () => void;
latlng?: LatLng | null;
airbase?: Airbase | null;
coalition?: Coalition;
}) {
/* Compute the min and max values depending on the unit type */
const minNumber = 1;
const maxNumber = groupUnitCount[props.blueprint.category];
const minAltitude = minAltitudeValues[props.blueprint.category];
const maxAltitude = maxAltitudeValues[props.blueprint.category];
const altitudeStep = altitudeIncrements[props.blueprint.category];
/* State initialization */
const [spawnCoalition, setSpawnCoalition] = useState("blue" as Coalition);
const [spawnNumber, setSpawnNumber] = useState(1);
const [spawnRole, setSpawnRole] = useState("");
const [spawnLoadoutName, setSpawnLoadout] = useState("");
const [spawnAltitude, setSpawnAltitude] = useState((maxAltitude - minAltitude) / 2);
const [spawnAltitudeType, setSpawnAltitudeType] = useState(false);
const [spawnLiveryID, setSpawnLiveryID] = useState("");
const [spawnSkill, setSpawnSkill] = useState("High");
const [showAdvancedOptions, setShowAdvancedOptions] = useState(false);
const [showLoadout, setShowLoadout] = useState(false);
const [showUnitSummary, setShowUnitSummary] = useState(false);
const [quickAccessName, setQuickAccessName] = useState("No name");
const [key, setKey] = useState("");
const [spawnRequestTable, setSpawnRequestTable] = useState(null as null | SpawnRequestTable);
/* When the menu is opened show the unit preview on the map as a cursor */
useEffect(() => {
if (!props.airbase && !props.latlng && spawnRequestTable) {
/* Refresh the unique key identified */
const newKey = hash(JSON.stringify(spawnRequestTable));
setKey(newKey);
getApp()?.getMap()?.setSpawnRequestTable(spawnRequestTable);
getApp().setState(OlympusState.SPAWN, SpawnSubState.SPAWN_UNIT);
}
}, [spawnRequestTable]);
/* Callback and effect to update the quick access name of the starredSpawn */
const updateStarredSpawnQuickAccessNameS = useCallback(() => {
if (key in props.starredSpawns) props.starredSpawns[key].quickAccessName = quickAccessName;
}, [props.starredSpawns, key, quickAccessName]);
useEffect(updateStarredSpawnQuickAccessNameS, [quickAccessName]);
/* Callback and effect to update the quick access name in the input field */
const updateQuickAccessName = useCallback(() => {
/* If the spawn is starred, set the quick access name */
if (key in props.starredSpawns && props.starredSpawns[key].quickAccessName) setQuickAccessName(props.starredSpawns[key].quickAccessName);
else setQuickAccessName("No name");
}, [props.starredSpawns, key]);
useEffect(updateQuickAccessName, [key]);
/* Callback and effect to update the spawn request table */
const updateSpawnRequestTable = useCallback(() => {
if (props.blueprint !== null) {
setSpawnRequestTable({
category: props.blueprint.category,
unit: {
unitType: props.blueprint.name,
location: props.latlng ?? new LatLng(0, 0), // This will be filled when the user clicks on the map to spawn the unit
skill: spawnSkill,
liveryID: spawnLiveryID,
altitude: ftToM(spawnAltitude),
loadout: props.blueprint.loadouts?.find((loadout) => loadout.name === spawnLoadoutName)?.code ?? "",
},
amount: spawnNumber,
coalition: spawnCoalition,
});
}
}, [props.blueprint, spawnAltitude, spawnLoadoutName, spawnCoalition, spawnNumber, spawnLiveryID, spawnSkill]);
useEffect(updateSpawnRequestTable, [props.blueprint, spawnAltitude, spawnLoadoutName, spawnCoalition, spawnNumber, spawnLiveryID, spawnSkill]);
/* Effect to update the coalition if it is force externally */
useEffect(() => {
if (props.coalition) setSpawnCoalition(props.coalition);
}, [props.coalition]);
/* Get a list of all the roles */
const roles: string[] = [];
(props.blueprint as UnitBlueprint).loadouts?.forEach((loadout) => {
loadout.roles.forEach((role) => {
!roles.includes(role) && roles.push(role);
});
});
/* Initialize the role */
spawnRole === "" && roles.length > 0 && setSpawnRole(roles[0]);
/* Get a list of all the loadouts */
const loadouts: LoadoutBlueprint[] = [];
(props.blueprint as UnitBlueprint).loadouts?.forEach((loadout) => {
loadout.roles.includes(spawnRole) && loadouts.push(loadout);
});
/* Initialize the loadout */
spawnLoadoutName === "" && loadouts.length > 0 && setSpawnLoadout(loadouts[0].name);
const spawnLoadout = props.blueprint.loadouts?.find((loadout) => {
return loadout.name === spawnLoadoutName;
});
return (
<div className="flex flex-col">
<div className="flex h-fit flex-col gap-3">
<div className="flex">
<FontAwesomeIcon
onClick={props.onBack}
icon={faArrowLeft}
className={`
my-auto mr-1 h-4 cursor-pointer rounded-md p-2
dark:text-gray-500 dark:hover:bg-gray-700 dark:hover:text-white
`}
/>
<h5 className="my-auto text-gray-200">{props.blueprint.label}</h5>
<OlNumberInput
className={"ml-auto"}
value={spawnNumber}
min={minNumber}
max={maxNumber}
onDecrease={() => {
setSpawnNumber(Math.max(minNumber, spawnNumber - 1));
}}
onIncrease={() => {
setSpawnNumber(Math.min(maxNumber, spawnNumber + 1));
}}
onChange={(ev) => {
!isNaN(Number(ev.target.value)) && setSpawnNumber(Math.max(minNumber, Math.min(maxNumber, Number(ev.target.value))));
}}
/>
</div>
<div
className={`
inline-flex w-full flex-row content-center justify-between gap-2
`}
>
<div className="my-auto text-sm text-white">Quick access: </div>
<OlStringInput
onChange={(e) => {
setQuickAccessName(e.target.value);
}}
value={quickAccessName}
/>
<OlStateButton
onClick={() => {
if (spawnRequestTable)
key in props.starredSpawns
? getApp().getMap().removeStarredSpawnRequestTable(key)
: getApp().getMap().addStarredSpawnRequestTable(key, spawnRequestTable);
}}
tooltip="Save this spawn for quick access"
checked={key in props.starredSpawns}
icon={faStar}
></OlStateButton>
</div>
{["aircraft", "helicopter"].includes(props.blueprint.category) && (
<>
{!props.airbase && (
<div>
<div
className={`
flex flex-row content-center items-center justify-between
`}
>
<div className="flex flex-col">
<span
className={`
font-normal
dark:text-white
`}
>
Altitude
</span>
<span
className={`
font-bold
dark:text-blue-500
`}
>{`${Intl.NumberFormat("en-US").format(spawnAltitude)} FT`}</span>
</div>
<OlLabelToggle toggled={spawnAltitudeType} leftLabel={"AGL"} rightLabel={"ASL"} onClick={() => setSpawnAltitudeType(!spawnAltitudeType)} />
</div>
<OlRangeSlider
onChange={(ev) => setSpawnAltitude(Number(ev.target.value))}
value={spawnAltitude}
min={minAltitude}
max={maxAltitude}
step={altitudeStep}
/>
</div>
)}
<div className="flex content-center justify-between gap-2">
<span
className={`
my-auto font-normal
dark:text-white
`}
>
Role
</span>
<OlDropdown label={spawnRole} className="w-64">
{roles.map((role) => {
return (
<OlDropdownItem
onClick={() => {
setSpawnRole(role);
setSpawnLoadout("");
}}
className={`w-full`}
>
{role}
</OlDropdownItem>
);
})}
</OlDropdown>
</div>
<div className="flex content-center justify-between gap-2">
<span
className={`
my-auto font-normal
dark:text-white
`}
>
Weapons
</span>
<OlDropdown label={spawnLoadoutName} className={`w-64`}>
{loadouts.map((loadout) => {
return (
<OlDropdownItem
onClick={() => {
setSpawnLoadout(loadout.name);
}}
className={`w-full`}
>
<span
className={`
w-full overflow-hidden text-ellipsis text-nowrap
text-left w-max-full
`}
>
{loadout.name}
</span>
</OlDropdownItem>
);
})}
</OlDropdown>
</div>
</>
)}
<OlAccordion
onClick={() => {
setShowAdvancedOptions(!showAdvancedOptions);
}}
open={showAdvancedOptions}
title="Advanced options"
>
<div className="flex flex-col gap-2">
<div className="flex content-center justify-between gap-2">
<span
className={`
my-auto font-normal
dark:text-white
`}
>
Livery
</span>
<OlDropdown
label={props.blueprint.liveries ? (props.blueprint.liveries[spawnLiveryID]?.name ?? "Default") : "No livery"}
className={`w-64`}
>
{props.blueprint.liveries &&
Object.keys(props.blueprint.liveries)
.sort((ida, idb) => {
if (props.blueprint.liveries) {
if (props.blueprint.liveries[ida].countries.length > 1) return 1;
return props.blueprint.liveries[ida].countries[0] > props.blueprint.liveries[idb].countries[0] ? 1 : -1;
} else return -1;
})
.map((id) => {
let country = Object.values(countryCodes).find((countryCode) => {
if (props.blueprint.liveries && countryCode.liveryCodes?.includes(props.blueprint.liveries[id].countries[0])) return true;
});
return (
<OlDropdownItem
onClick={() => {
setSpawnLiveryID(id);
}}
className={`w-full`}
>
<span
className={`
w-full content-center overflow-hidden
text-ellipsis text-nowrap text-left w-max-full
flex gap-2
`}
>
{props.blueprint.liveries && props.blueprint.liveries[id].countries.length == 1 && (
<img src={`images/countries/${country?.flagCode.toLowerCase()}.svg`} className={`
h-6
`} />
)}
<div className="my-auto truncate">
<span
className={`
w-full overflow-hidden text-left w-max-full
`}
>
{props.blueprint.liveries ? props.blueprint.liveries[id].name : ""}
</span>
</div>
</span>
</OlDropdownItem>
);
})}
</OlDropdown>
</div>
<div className="flex content-center justify-between gap-2">
<span
className={`
my-auto font-normal
dark:text-white
`}
>
Skill
</span>
<OlDropdown label={spawnSkill} className={`w-64`}>
{["Average", "Good", "High", "Excellent"].map((skill) => {
return (
<OlDropdownItem
onClick={() => {
setSpawnSkill(skill);
}}
className={`w-full`}
>
<span
className={`
w-full content-center overflow-hidden text-ellipsis
text-nowrap text-left w-max-full flex gap-2
`}
>
<div className="my-auto">{skill}</div>
</span>
</OlDropdownItem>
);
})}
</OlDropdown>
</div>
</div>
</OlAccordion>
</div>
<OlAccordion
onClick={() => {
setShowUnitSummary(!showUnitSummary);
}}
open={showUnitSummary}
title="Unit summary"
>
<OlUnitSummary blueprint={props.blueprint} coalition={spawnCoalition} />
</OlAccordion>
{spawnLoadout && spawnLoadout.items.length > 0 && (
<OlAccordion
onClick={() => {
setShowLoadout(!showLoadout);
}}
open={showLoadout}
title="Loadout"
>
{spawnLoadout.items.map((item) => {
return (
<div className="flex content-center gap-2">
<div
className={`
my-auto w-6 min-w-6 rounded-full py-0.5 text-center text-sm
font-bold text-gray-500
dark:bg-[#17212D]
`}
>
{item.quantity}
</div>
<div
className={`
my-auto overflow-hidden text-ellipsis text-nowrap text-sm
dark:text-gray-300
`}
>
{item.name}
</div>
</div>
);
})}
</OlAccordion>
)}
{(props.latlng || props.airbase) && (
<button
type="button"
data-coalition={props.coalition ?? "blue"}
className={`
m-2 rounded-lg px-5 py-2.5 text-sm font-medium text-white
data-[coalition='blue']:bg-blue-600
data-[coalition='neutral']:bg-gray-400
data-[coalition='red']:bg-red-500
focus:outline-none focus:ring-4
`}
onClick={() => {
if (spawnRequestTable)
getApp()
.getUnitsManager()
.spawnUnits(spawnRequestTable.category, Array(spawnRequestTable.amount).fill(spawnRequestTable.unit), spawnRequestTable.coalition, false, props.airbase?.getName() ?? undefined);
getApp().setState(OlympusState.IDLE)
}}
>
Spawn
</button>
)}
</div>
);
}

View File

@ -2,7 +2,6 @@ import { faArrowLeft, faClose } from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import React, { useEffect, useState } from "react";
import { FaChevronDown, FaChevronUp } from "react-icons/fa";
import { HideMenuEvent } from "../../../events";
export function Menu(props: {
title: string;
@ -16,11 +15,7 @@ export function Menu(props: {
const [hide, setHide] = useState(true);
if (!props.open && hide) setHide(false);
useEffect(() => {
HideMenuEvent.dispatch(hide)
}, [hide])
return (
<div
data-open={props.open}

View File

@ -1,15 +1,17 @@
import React, { useEffect, useState } from "react";
import React, { useCallback, useEffect, useState } from "react";
import { faHandPointer, faJetFighter, faMap, IconDefinition } from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { MAP_OPTIONS_DEFAULTS, NO_SUBSTATE, OlympusState, OlympusSubState, SpawnSubState } from "../../constants/constants";
import { ContextActionTarget, MAP_OPTIONS_DEFAULTS, NO_SUBSTATE, OlympusState, OlympusSubState, SpawnSubState } from "../../constants/constants";
import { AppStateChangedEvent, MapOptionsChangedEvent } from "../../events";
import { getApp } from "../../olympusapp";
import { ContextAction } from "../../unit/contextaction";
export function ControlsPanel(props: {}) {
const [controls, setControls] = useState(
null as
| {
actions: (string | number | IconDefinition)[];
target: IconDefinition;
target: IconDefinition | null;
text: string;
}[]
| null
@ -26,11 +28,11 @@ export function ControlsPanel(props: {}) {
MapOptionsChangedEvent.on((mapOptions) => setMapOptions({ ...mapOptions }));
}, []);
useEffect(() => {
const callback = useCallback(() => {
const touch = matchMedia("(hover: none)").matches;
let controls: {
actions: (string | number | IconDefinition)[];
target: IconDefinition;
target: IconDefinition | null;
text: string;
}[] = [];
@ -42,7 +44,7 @@ export function ControlsPanel(props: {}) {
text: "Select unit",
},
{
actions: [touch ? faHandPointer : "LMB", 2],
actions: touch ? [faHandPointer, "Hold"] : ["RMB"],
target: faMap,
text: "Quick spawn menu",
},
@ -57,6 +59,58 @@ export function ControlsPanel(props: {}) {
text: "Move map location",
},
];
} else if (appState === OlympusState.SPAWN_CONTEXT) {
controls = [
{
actions: [touch ? faHandPointer : "LMB"],
target: faJetFighter,
text: "Close context menu",
},
{
actions: touch ? [faHandPointer, "Hold"] : ["RMB"],
target: faMap,
text: "Move context menu",
},
{
actions: touch ? [faHandPointer, "Drag"] : ["Shift", "LMB", "Drag"],
target: faMap,
text: "Box selection",
},
{
actions: [touch ? faHandPointer : "LMB", "Drag"],
target: faMap,
text: "Move map location",
},
];
} else if (appState === OlympusState.UNIT_CONTROL) {
if (!mapOptions.tabletMode) {
controls = Object.values(getApp().getMap().getContextActionSet()?.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", "Ctrl LH")
.replace("AltLeft", "Alt LH")
.replace("ShiftLeft", "Shift LH")
.replace("ControlRight", "Ctrl RH")
.replace("AltRight", "Alt RH")
.replace("ShiftRight", "Shift RH")
);
contextAction.getTarget() !== ContextActionTarget.NONE && actions.push(touch ? faHandPointer : "LMB");
return {
actions: actions,
target:
contextAction.getTarget() === ContextActionTarget.NONE ? null : contextAction.getTarget() === ContextActionTarget.POINT ? faMap : faJetFighter,
text: contextAction.getLabel(),
};
});
}
} else if (appState === OlympusState.SPAWN) {
controls = [
{
@ -91,7 +145,9 @@ export function ControlsPanel(props: {}) {
}
setControls(controls);
}, [appState, appSubState]);
}, [appState, appSubState, mapOptions]);
useEffect(callback, [appState, appSubState, mapOptions]);
return (
<div
@ -122,9 +178,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

@ -15,7 +15,7 @@ import {
olButtonsVisibilityNavyunit,
olButtonsVisibilityOlympus,
} from "../components/olicons";
import { FaChevronLeft, FaChevronRight } from "react-icons/fa6";
import { FaChevronLeft, FaChevronRight, FaComputer, FaTabletScreenButton } from "react-icons/fa6";
import { CommandModeOptionsChangedEvent, ConfigLoadedEvent, HiddenTypesChangedEvent, MapOptionsChangedEvent, MapSourceChangedEvent } from "../../events";
import { BLUE_COMMANDER, COMMAND_MODE_OPTIONS_DEFAULTS, MAP_HIDDEN_TYPES_DEFAULTS, MAP_OPTIONS_DEFAULTS } from "../../constants/constants";
import { OlympusConfig } from "../../interfaces";
@ -111,12 +111,18 @@ export function Header() {
</div>
</div>
{commandModeOptions.commandMode === BLUE_COMMANDER && (
<div
className={`flex h-full rounded-md bg-blue-600 px-4 text-white`}
>
<div className={`flex h-full rounded-md bg-blue-600 px-4 text-white`}>
<span className="my-auto font-bold">BLUE Commander ({commandModeOptions.spawnPoints.blue} points)</span>
</div>
)}
<div
className="cursor-pointer rounded-full bg-blue-500 px-4 py-2 text-white"
onClick={() => {
getApp().getMap().setOption("tabletMode", !mapOptions.tabletMode);
}}
>
{mapOptions.tabletMode ? <FaTabletScreenButton /> : <FaComputer />}
</div>
<div className={`flex h-fit flex-row items-center justify-start gap-1`}>
<OlLockStateButton
checked={!mapOptions.protectDCSUnits}
@ -213,7 +219,7 @@ export function Header() {
.getMap()
.setOption("cameraPluginMode", mapOptions.cameraPluginMode === "live" ? "map" : "live");
}}
></OlLabelToggle>
/>
<OlStateButton
checked={mapOptions.cameraPluginEnabled}
icon={faCamera}

View File

@ -1,5 +1,5 @@
import React, { useEffect, useState } from "react";
import { AppStateChangedEvent, ContextActionChangedEvent, HideMenuEvent, HotgroupsChangedEvent, InfoPopupEvent } from "../../events";
import { AppStateChangedEvent, ContextActionChangedEvent, HotgroupsChangedEvent, InfoPopupEvent } from "../../events";
import { OlympusState } from "../../constants/constants";
import { ContextAction } from "../../unit/contextaction";
import { OlStateButton } from "../components/olstatebutton";
@ -13,7 +13,6 @@ export function HotGroupBar(props: {}) {
useEffect(() => {
AppStateChangedEvent.on((state, subState) => setAppState(state));
HideMenuEvent.on((hidden) => setMenuHidden(hidden));
HotgroupsChangedEvent.on((hotgroups) => setHotgroups({ ...hotgroups }));
}, []);

View File

@ -1,5 +1,5 @@
import React, { useEffect, useState } from "react";
import { AppStateChangedEvent, ContextActionChangedEvent, HideMenuEvent, InfoPopupEvent } from "../../events";
import { AppStateChangedEvent, ContextActionChangedEvent, InfoPopupEvent } from "../../events";
import { OlympusState } from "../../constants/constants";
import { ContextAction } from "../../unit/contextaction";
@ -7,35 +7,16 @@ export function InfoBar(props: {}) {
const [messages, setMessages] = useState([] as string[]);
const [appState, setAppState] = useState(OlympusState.NOT_INITIALIZED);
const [contextAction, setContextAction] = useState(null as ContextAction | null);
const [menuHidden, setMenuHidden] = useState(false);
useEffect(() => {
InfoPopupEvent.on((messages) => setMessages([...messages]));
AppStateChangedEvent.on((state, subState) => setAppState(state));
ContextActionChangedEvent.on((contextAction) => setContextAction(contextAction));
HideMenuEvent.on((hidden) => setMenuHidden(hidden));
}, []);
let topString = "";
if (appState === OlympusState.UNIT_CONTROL) {
if (contextAction === null) {
topString = "top-36";
} else {
topString = "top-48";
}
} else {
topString = "top-16";
}
return (
<div
data-menuhidden={menuHidden || appState === OlympusState.IDLE}
className={`
absolute left-[50%]
data-[menuhidden='false']:translate-x-[calc(200px-50%+2rem)]
data-[menuhidden='true']:translate-x-[calc(-50%+2rem)]
${topString}
`}
className={`absolute left-[50%] top-16`}
>
{messages.slice(Math.max(0, messages.length - 4), Math.max(0, messages.length)).map((message, idx) => {
return (

View File

@ -436,7 +436,6 @@ export function SpawnMenu(props: { open: boolean; onClose: () => void; children?
{!(blueprint === null) && (
<UnitSpawnMenu
blueprint={blueprint}
spawnAtLocation={true}
starredSpawns={starredSpawns}
coalition={commandModeOptions.commandMode !== GAME_MASTER ? (commandModeOptions.commandMode === BLUE_COMMANDER ? "blue" : "red") : undefined}
/>

View File

@ -3,19 +3,20 @@ 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 { CONTEXT_ACTION_COLORS, MAP_OPTIONS_DEFAULTS } from "../../constants/constants";
import { FaInfoCircle } from "react-icons/fa";
import { FaChevronLeft, FaChevronRight } from "react-icons/fa6";
import { FaChevronDown, FaChevronLeft, FaChevronRight, FaChevronUp } from "react-icons/fa6";
import { OlympusState } from "../../constants/constants";
import { AppStateChangedEvent, ContextActionChangedEvent, ContextActionSetChangedEvent, HideMenuEvent } from "../../events";
import { AppStateChangedEvent, ContextActionChangedEvent, ContextActionSetChangedEvent, MapOptionsChangedEvent } from "../../events";
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 [scrolledLeft, setScrolledLeft] = useState(true);
const [scrolledRight, setScrolledRight] = useState(false);
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);
@ -27,18 +28,18 @@ export function UnitControlBar(props: {}) {
AppStateChangedEvent.on((state, subState) => setAppState(state));
ContextActionSetChangedEvent.on((contextActionSet) => setcontextActionSet(contextActionSet));
ContextActionChangedEvent.on((contextAction) => setContextAction(contextAction));
HideMenuEvent.on((hidden) => setMenuHidden(hidden));
MapOptionsChangedEvent.on((mapOptions) => setMapOptions({ ...mapOptions }));
}, []);
function onScroll(el) {
const sl = el.scrollLeft;
const sr = el.scrollWidth - el.scrollLeft - el.clientWidth;
const sl = el.scrollTop;
const sr = el.scrollHeight - el.scrollTop - el.clientHeight;
sl < 1 && !scrolledLeft && setScrolledLeft(true);
sl > 1 && scrolledLeft && setScrolledLeft(false);
sl < 1 && !scrolledTop && setScrolledTop(true);
sl > 1 && scrolledTop && setScrolledTop(false);
sr < 1 && !scrolledRight && setScrolledRight(true);
sr > 1 && scrolledRight && setScrolledRight(false);
sr < 1 && !scrolledBottom && setScrolledBottom(true);
sr > 1 && scrolledBottom && setScrolledBottom(false);
}
let reorderedActions: ContextAction[] = contextActionSet
@ -49,70 +50,68 @@ export function UnitControlBar(props: {}) {
<>
{appState === OlympusState.UNIT_CONTROL && contextActionSet && Object.keys(contextActionSet.getContextActions()).length > 0 && (
<>
<div
data-menuhidden={menuHidden}
className={`
absolute left-[50%] top-16 flex max-w-[80%] gap-2 rounded-md
bg-gray-200
dark:bg-olympus-900
data-[menuhidden='false']:translate-x-[calc(200px-50%+2rem)]
data-[menuhidden='true']:translate-x-[calc(-50%+2rem)]
`}
>
{!scrolledLeft && (
<FaChevronLeft
{mapOptions.tabletMode && (
<>
<div
data-menuhidden={menuHidden}
className={`
absolute left-0 h-full w-6 rounded-lg px-2 py-3.5
text-gray-200
absolute right-2 top-16 flex max-h-[80%] gap-2 rounded-md
bg-gray-200
dark:bg-olympus-900
`}
/>
)}
<div className="flex gap-2 overflow-x-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.getOptions().executeImmediately) {
contextActionIt.executeCallback(null, null);
} else {
contextActionIt !== contextAction ? getApp().getMap().setContextAction(contextActionIt) : getApp().getMap().setContextAction(null);
}
}}
/>
<div
className={`
rounded-sm bg-gray-400 text-center text-xs font-bold
text-olympus-800
`}
>
{(contextActionIt.getOptions().hotkey ?? "").replace("Key", "")}
</div>
</div>
);
})}
</div>
{!scrolledRight && (
<FaChevronRight
className={`
absolute right-0 h-full w-6 rounded-lg px-2 py-3.5
text-gray-200
dark:bg-olympus-900
`}
/>
)}
</div>
{/*}
>
{!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.getOptions().executeImmediately) {
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-32 flex min-w-[300px]
absolute left-[50%] top-16 flex min-w-[300px]
translate-x-[calc(-50%+2rem)] items-center gap-2 rounded-md
bg-gray-200 p-4
dark:bg-olympus-800
@ -135,7 +134,6 @@ export function UnitControlBar(props: {}) {
</div>
</div>
)}
{*/}
</>
)}
</>

View File

@ -21,7 +21,6 @@ import { OlAccordion } from "../components/olaccordion";
export function UnitSpawnMenu(props: {
starredSpawns: { [key: string]: SpawnRequestTable };
blueprint: UnitBlueprint;
spawnAtLocation: boolean;
airbase?: Airbase | null;
coalition?: Coalition;
}) {
@ -49,7 +48,7 @@ export function UnitSpawnMenu(props: {
/* When the menu is opened show the unit preview on the map as a cursor */
useEffect(() => {
if (props.spawnAtLocation && spawnRequestTable) {
if (!props.airbase && spawnRequestTable) {
/* Refresh the unique key identified */
const newKey = hash(JSON.stringify(spawnRequestTable));
setKey(newKey);
@ -67,7 +66,7 @@ export function UnitSpawnMenu(props: {
/* Callback and effect to update the quick access name in the input field */
const updateQuickAccessName = useCallback(() => {
if (props.spawnAtLocation) {
if (!props.airbase) {
/* If the spawn is starred, set the quick access name */
if (key in props.starredSpawns && props.starredSpawns[key].quickAccessName) setQuickAccessName(props.starredSpawns[key].quickAccessName);
else setQuickAccessName("No name");
@ -408,7 +407,7 @@ export function UnitSpawnMenu(props: {
</OlAccordion>
</div>
)}
{!props.spawnAtLocation && (
{props.airbase && (
<button
type="button"
className={`
@ -422,7 +421,7 @@ export function UnitSpawnMenu(props: {
if (spawnRequestTable)
getApp()
.getUnitsManager()
.spawnUnits(spawnRequestTable.category, [spawnRequestTable.unit], spawnRequestTable.coalition, false, props.airbase?.getName());
.spawnUnits(spawnRequestTable.category, Array(spawnRequestTable.amount).fill(spawnRequestTable.unit), spawnRequestTable.coalition, false, props.airbase?.getName());
}}
>
Spawn

View File

@ -28,7 +28,7 @@ import { AppStateChangedEvent, MapOptionsChangedEvent } from "../events";
import { GameMasterMenu } from "./panels/gamemastermenu";
import { InfoBar } from "./panels/infobar";
import { HotGroupBar } from "./panels/hotgroupsbar";
import { StarredSpawnContextMenu } from "./contextmenus/starredspawncontextmenu";
import { SpawnContextMenu } from "./contextmenus/SpawnContextmenu";
import { CoordinatesPanel } from "./panels/coordinatespanel";
export type OlympusUIState = {
@ -112,7 +112,7 @@ export function UI() {
<HotGroupBar />
<MapContextMenu />
<StarredSpawnContextMenu />
<SpawnContextMenu />
</div>
</div>
);

View File

@ -6,7 +6,10 @@ import { ContextActionTarget, ContextActionType } from "../constants/constants";
export interface ContextActionOptions {
executeImmediately?: boolean;
type: ContextActionType;
hotkey?: string;
code: string | null;
shiftKey?: boolean;
altKey?: boolean;
ctrlKey?: boolean;
}
export type ContextActionCallback = (units: Unit[], targetUnit: Unit | null, targetPosition: LatLng | null, originalEvent?: MouseEvent) => void;

View File

@ -159,7 +159,6 @@ export abstract class Unit extends CustomMarker {
/* Inputs timers */
#mouseCooldownTimer: number = 0;
#shortPressTimer: number = 0;
#longPressTimer: number = 0;
#isMouseOnCooldown: boolean = false;
#isMouseDown: boolean = false;
@ -345,10 +344,11 @@ export abstract class Unit extends CustomMarker {
/* Leaflet events listeners */
this.on("mousedown", (e) => this.#onMouseDown(e));
this.on("mouseup", (e) => this.#onMouseUp(e));
this.on("contextmenu", (e) => this.#onRightClick(e));
this.on("dblclick", (e) => this.#onDoubleClick(e));
this.on("mouseover", () => {
if (this.belongsToCommandedCoalition() && (getApp().getState() === OlympusState.IDLE || getApp().getState() === OlympusState.UNIT_CONTROL))
this.setHighlighted(true);
if (this.belongsToCommandedCoalition()) this.setHighlighted(true);
});
this.on("mouseout", () => this.setHighlighted(false));
getApp()
@ -1281,96 +1281,83 @@ export abstract class Unit extends CustomMarker {
}
/***********************************************/
#onMouseUp(e: any) {
if (getApp().getState() === OlympusState.IDLE || getApp().getState() === OlympusState.UNIT_CONTROL) {
this.#isMouseDown = false;
if (getApp().getMap().isSelecting()) return;
DomEvent.stop(e);
DomEvent.preventDefault(e);
e.originalEvent.stopImmediatePropagation();
e.originalEvent.stopPropagation();
window.clearTimeout(this.#longPressTimer);
this.#isMouseOnCooldown = true;
this.#mouseCooldownTimer = window.setTimeout(() => {
this.#isMouseOnCooldown = false;
}, 200);
}
}
#onMouseDown(e: any) {
if (getApp().getState() === OlympusState.IDLE || getApp().getState() === OlympusState.UNIT_CONTROL) {
this.#isMouseDown = true;
if (e.originalEvent.button === 2) return;
DomEvent.stop(e);
DomEvent.preventDefault(e);
e.originalEvent.stopImmediatePropagation();
this.#isMouseDown = true;
if (this.#isMouseOnCooldown) {
return;
}
DomEvent.stop(e);
DomEvent.preventDefault(e);
e.originalEvent.stopImmediatePropagation();
this.#shortPressTimer = window.setTimeout(() => {
/* If the mouse is no longer being pressed, execute the short press action */
if (!this.#isMouseDown) this.#onShortPress(e);
}, 200);
if (this.#isMouseOnCooldown) return;
this.#longPressTimer = window.setTimeout(() => {
/* If the mouse is still being pressed, execute the long press action */
if (this.#isMouseDown) this.#onLongPress(e);
}, 350);
}
this.#shortPressTimer = window.setTimeout(() => {
/* If the mouse is no longer being pressed, execute the short press action */
if (!this.#isMouseDown) this.#onLeftClick(e);
}, 200);
}
#onShortPress(e: LeafletMouseEvent) {
console.log(`Short press on ${this.getUnitName()}`);
#onMouseUp(e: any) {
if (e.originalEvent.button === 2) return;
if (getApp().getState() === OlympusState.IDLE) {
this.#isMouseDown = false;
if (getApp().getMap().isSelecting()) return;
DomEvent.stop(e);
DomEvent.preventDefault(e);
e.originalEvent.stopImmediatePropagation();
this.#isMouseOnCooldown = true;
this.#mouseCooldownTimer = window.setTimeout(() => {
this.#isMouseOnCooldown = false;
}, 200);
}
#onLeftClick(e: any) {
console.log(`Left click on ${this.getUnitName()}`);
if (getApp().getMap().getContextAction() === null) {
if (!e.originalEvent.ctrlKey) getApp().getUnitsManager().deselectAllUnits();
this.setSelected(!this.getSelected());
} else if (getApp().getState() === OlympusState.UNIT_CONTROL) {
if (getApp().getMap().getContextAction() && getApp().getMap().getContextAction()?.getTarget() === ContextActionTarget.UNIT) {
if (getApp().getMap().getContextAction()?.getTarget() === ContextActionTarget.UNIT) {
getApp().getMap().executeContextAction(this, null, e.originalEvent);
} else {
if (!e.originalEvent.ctrlKey) getApp().getUnitsManager().deselectAllUnits();
this.setSelected(!this.getSelected());
}
} else if (getApp().getState() === OlympusState.JTAC && getApp().getSubState() === JTACSubState.SELECT_TARGET) {
// TODO document.dispatchEvent(new CustomEvent("selectJTACTarget", { detail: { unit: this } }));
getApp().setState(OlympusState.IDLE);
}
}
#onLongPress(e: any) {
console.log(`Long press on ${this.getUnitName()}`);
#onRightClick(e: any) {
console.log(`Right click on ${this.getUnitName()}`);
if (getApp().getState() === OlympusState.UNIT_CONTROL && !getApp().getMap().getContextAction()) {
DomEvent.stop(e);
DomEvent.preventDefault(e);
e.originalEvent.stopImmediatePropagation();
if (getApp().getState() === OlympusState.UNIT_CONTROL && e.originalEvent.button === 2 && !getApp().getMap().getContextAction()) {
getApp().setState(OlympusState.UNIT_CONTROL, UnitControlSubState.UNIT_CONTEXT_MENU);
UnitContextMenuRequestEvent.dispatch(this);
}
}
#onDoubleClick(e: any) {
DomEvent.stop(e);
DomEvent.preventDefault(e);
console.log(`Double click on ${this.getUnitName()}`);
window.clearTimeout(this.#shortPressTimer);
/* Select all matching units in the viewport */
if (getApp().getState() === OlympusState.IDLE || getApp().getState() === OlympusState.UNIT_CONTROL) {
DomEvent.stop(e);
DomEvent.preventDefault(e);
console.log(`Double click on ${this.getUnitName()}`);
window.clearTimeout(this.#shortPressTimer);
window.clearTimeout(this.#longPressTimer);
/* Select all matching units in the viewport */
if (getApp().getState() === OlympusState.IDLE || getApp().getState() === OlympusState.UNIT_CONTROL) {
const unitsManager = getApp().getUnitsManager();
Object.values(unitsManager.getUnits()).forEach((unit: Unit) => {
if (unit.getAlive() === true && unit.getName() === this.getName() && unit.isInViewport()) unitsManager.selectUnit(unit.ID, false);
});
}
const unitsManager = getApp().getUnitsManager();
Object.values(unitsManager.getUnits()).forEach((unit: Unit) => {
if (unit.getAlive() === true && unit.getName() === this.getName() && unit.isInViewport()) unitsManager.selectUnit(unit.ID, false);
});
}
}

View File

@ -68,18 +68,6 @@ export class UnitsManager {
getApp()
.getShortcutManager()
.addShortcut("deselectAll", {
label: "Deselect all units",
keyUpCallback: (ev: KeyboardEvent) => {
getApp().getUnitsManager().deselectAllUnits();
},
code: "Escape",
})
.addShortcut("delete", {
label: "Delete selected units",
keyUpCallback: () => this.delete(),
code: "Delete",
})
.addShortcut("selectAll", {
label: "Select all units",
keyUpCallback: () => {