diff --git a/backend/core/include/datatypes.h b/backend/core/include/datatypes.h index c0e1c86e..fec3cb5b 100644 --- a/backend/core/include/datatypes.h +++ b/backend/core/include/datatypes.h @@ -13,6 +13,7 @@ namespace DataIndex { country, name, unitName, + callsign, groupName, state, task, diff --git a/backend/core/include/unit.h b/backend/core/include/unit.h index 5ba64e7c..83f39c4b 100644 --- a/backend/core/include/unit.h +++ b/backend/core/include/unit.h @@ -19,12 +19,12 @@ public: ~Unit(); /********** Methods **********/ - void initialize(json::value json); + virtual void initialize(json::value json) final; virtual void setDefaults(bool force = false); void runAILoop(); - void update(json::value json, double dt); + virtual void update(json::value json, double dt) final; void refreshLeaderData(unsigned long long time); unsigned int getID() { return ID; } @@ -71,6 +71,7 @@ public: virtual void setCountry(unsigned char newValue) { updateValue(country, newValue, DataIndex::country); } virtual void setName(string newValue) { updateValue(name, newValue, DataIndex::name); } virtual void setUnitName(string newValue) { updateValue(unitName, newValue, DataIndex::unitName); } + virtual void setCallsign(string newValue) { updateValue(callsign, newValue, DataIndex::callsign); } virtual void setGroupName(string newValue) { updateValue(groupName, newValue, DataIndex::groupName); } virtual void setState(unsigned char newValue) { updateValue(state, newValue, DataIndex::state); }; virtual void setTask(string newValue) { updateValue(task, newValue, DataIndex::task); } @@ -117,6 +118,7 @@ public: virtual unsigned char getCoalition() { return coalition; } virtual unsigned char getCountry() { return country; } virtual string getName() { return name; } + virtual string getCallsign() { return callsign; } virtual string getUnitName() { return unitName; } virtual string getGroupName() { return groupName; } virtual unsigned char getState() { return state; } @@ -167,6 +169,7 @@ protected: unsigned char country = NULL; string name = ""; string unitName = ""; + string callsign = ""; string groupName = ""; unsigned char state = State::NONE; string task = ""; diff --git a/backend/core/src/unit.cpp b/backend/core/src/unit.cpp index f35c5c1f..0ad51970 100644 --- a/backend/core/src/unit.cpp +++ b/backend/core/src/unit.cpp @@ -27,6 +27,13 @@ Unit::~Unit() } void Unit::initialize(json::value json) +{ + update(json, 0); + setDefaults(); +} + + +void Unit::update(json::value json, double dt) { if (json.has_string_field(L"name")) setName(to_string(json[L"name"])); @@ -37,23 +44,18 @@ void Unit::initialize(json::value json) if (json.has_string_field(L"groupName")) setGroupName(to_string(json[L"groupName"])); + if (json.has_string_field(L"callsign")) + setCallsign(to_string(json[L"callsign"])); + if (json.has_number_field(L"coalitionID")) setCoalition(json[L"coalitionID"].as_number().to_int32()); - //if (json.has_number_field(L"Country")) // setCountry(json[L"Country"].as_number().to_int32()); - + /* All units which contain the name "Olympus" are automatically under AI control */ if (getUnitName().find("Olympus") != string::npos) setControlled(true); - update(json, 0); - setDefaults(); -} - - -void Unit::update(json::value json, double dt) -{ if (json.has_object_field(L"position")) { setPosition({ @@ -255,6 +257,7 @@ void Unit::getData(stringstream& ss, unsigned long long time) case DataIndex::country: appendNumeric(ss, datumIndex, country); break; case DataIndex::name: appendString(ss, datumIndex, name); break; case DataIndex::unitName: appendString(ss, datumIndex, unitName); break; + case DataIndex::callsign: appendString(ss, datumIndex, callsign); break; case DataIndex::groupName: appendString(ss, datumIndex, groupName); break; case DataIndex::state: appendNumeric(ss, datumIndex, state); break; case DataIndex::task: appendString(ss, datumIndex, task); break; diff --git a/databases/units/groundunitdatabase.json b/databases/units/groundunitdatabase.json index 9aa77084..43537996 100644 --- a/databases/units/groundunitdatabase.json +++ b/databases/units/groundunitdatabase.json @@ -2267,7 +2267,7 @@ "coalition": "red", "era": "Mid Cold War", "category": "groundunit", - "label": "AK-74", + "label": "AK-74 (Type 1)", "shortLabel": "AK-74", "filename": "", "type": "Infantry", @@ -5558,7 +5558,7 @@ "coalition": "red", "era": "Early Cold War", "category": "groundunit", - "label": "AK-74", + "label": "AK-74 (Type 4)", "shortLabel": "AK-74", "filename": "", "type": "Infantry", @@ -7722,7 +7722,7 @@ "coalition": "red", "era": "Early Cold War", "category": "groundunit", - "label": "Insurgent AK-74", + "label": "AK-74 (Insurgent)", "shortLabel": "AK-74 (Ins)", "type": "Infantry", "enabled": true, @@ -7880,7 +7880,7 @@ "coalition": "red", "era": "Late Cold War", "category": "groundunit", - "label": "AK-74", + "label": "AK-74 (Type 2)", "shortLabel": "AK-74", "type": "Infantry", "enabled": true, @@ -7974,7 +7974,7 @@ "coalition": "red", "era": "Late Cold War", "category": "groundunit", - "label": "AK-74", + "label": "AK-74 (Type 3)", "shortLabel": "AK-74", "type": "Infantry", "enabled": true, diff --git a/frontend/react/src/constants/constants.ts b/frontend/react/src/constants/constants.ts index 93b11d36..978b7fed 100644 --- a/frontend/react/src/constants/constants.ts +++ b/frontend/react/src/constants/constants.ts @@ -513,7 +513,7 @@ export namespace ContextActions { .getUnitsManager() .addDestination(targetPosition, getApp().getMap().getKeepRelativePositions(), getApp().getMap().getDestinationRotation(), units); }, - { type: ContextActionType.MOVE, code: "ControlLeft", shiftKey: false } + { type: ContextActionType.MOVE, code: null} ); export const DELETE = new ContextAction( diff --git a/frontend/react/src/map/boxselect.ts b/frontend/react/src/map/boxselect.ts index d4db058c..edfa9635 100644 --- a/frontend/react/src/map/boxselect.ts +++ b/frontend/react/src/map/boxselect.ts @@ -12,7 +12,6 @@ export var BoxSelect = Handler.extend({ this._map = map; this._container = map.getContainer(); this._pane = map.getPanes().overlayPane; - this._resetStateTimeout = 0; map.on("unload", this._destroy, this); }, @@ -34,23 +33,12 @@ export var BoxSelect = Handler.extend({ }, _resetState: function () { - this._resetStateTimeout = 0; this._moved = false; }, - _clearDeferredResetState: function () { - if (this._resetStateTimeout !== 0) { - clearTimeout(this._resetStateTimeout); - this._resetStateTimeout = 0; - } - }, - _onMouseDown: function (e: any) { if (this._map.getSelectionEnabled() && e.button == 0) { - // Clear the deferred resetState if it hasn't executed yet, otherwise it - // will interrupt the interaction and orphan a box element in the container. - this._clearDeferredResetState(); - this._resetState(); + if (this._moved) this._finish(); DomUtil.disableImageDrag(); this._map.dragging.disable(); @@ -66,7 +54,7 @@ export var BoxSelect = Handler.extend({ touchmove: this._onMouseMove, touchend: this._onMouseUp, mousemove: this._onMouseMove, - mouseup: this._onMouseUp + mouseup: this._onMouseUp, }, this ); @@ -76,20 +64,10 @@ export var BoxSelect = Handler.extend({ }, _onMouseUp: function (e: any) { - if (e.button !== 0) { - return; - } - - this._finish(); - - if (!this._moved) { - return; - } - // Postpone to next JS tick so internal click event handling - // still see it as "moved". - window.setTimeout(Util.bind(this._resetState, this), 0); + if (e.button !== 0) return; + window.setTimeout(Util.bind(this._finish, this), 0); + if (!this._moved) return; var bounds = new LatLngBounds(this._map.containerPointToLatLng(this._startPoint), this._map.containerPointToLatLng(this._point)); - this._map.fire("selectionend", { selectionBounds: bounds }); }, @@ -141,5 +119,7 @@ export var BoxSelect = Handler.extend({ }, this ); + + this._resetState(); }, }); diff --git a/frontend/react/src/map/map.ts b/frontend/react/src/map/map.ts index 3c9411da..e63e6059 100644 --- a/frontend/react/src/map/map.ts +++ b/frontend/react/src/map/map.ts @@ -297,6 +297,11 @@ export class Map extends L.Map { ContextActionChangedEvent.on((contextAction) => this.#updateDestinationPreviewMarkers()); MapOptionsChangedEvent.on((mapOptions) => this.#moveDestinationPreviewMarkers()); + window.addEventListener("blur", () => { + this.setSelectionEnabled(false); + this.setPasteEnabled(false); + }) + /* Pan interval */ this.#panInterval = window.setInterval(() => { if (this.#panUp || this.#panDown || this.#panRight || this.#panLeft) @@ -806,6 +811,10 @@ export class Map extends L.Map { return this.#contextAction; } + getDefaultContextAction() { + return this.#contextActionSet?.getDefaultContextAction(); + } + executeDefaultContextAction(targetUnit: Unit | null, targetPosition: L.LatLng | null, originalEvent?: MouseEvent) { this.#contextActionSet?.getDefaultContextAction()?.executeCallback(targetUnit, targetPosition, originalEvent); } diff --git a/frontend/react/src/map/markers/custommarker.ts b/frontend/react/src/map/markers/custommarker.ts index 8c68779a..2e3a0f00 100644 --- a/frontend/react/src/map/markers/custommarker.ts +++ b/frontend/react/src/map/markers/custommarker.ts @@ -1,8 +1,16 @@ import { DivIcon, Map, Marker, MarkerOptions, LatLngExpression } from "leaflet"; +import { SelectionEnabledChangedEvent } from "../../events"; export class CustomMarker extends Marker { constructor(latlng: LatLngExpression, options?: MarkerOptions) { super(latlng, options); + + SelectionEnabledChangedEvent.on((enabled) => { + const el = this.getElement(); + if (el === undefined) return; + if (enabled) el.classList.add("disable-pointer-events"); + else el.classList.remove("disable-pointer-events"); + }); } onAdd(map: Map): this { diff --git a/frontend/react/src/map/markers/stylesheets/airbase.css b/frontend/react/src/map/markers/stylesheets/airbase.css index 6b1c30c6..1fa991a9 100644 --- a/frontend/react/src/map/markers/stylesheets/airbase.css +++ b/frontend/react/src/map/markers/stylesheets/airbase.css @@ -32,3 +32,7 @@ [data-awacs-mode] .airbase-icon svg * { fill: transparent !important; } + +#map-container .leaflet-airbase-marker.airbase-disable-pointer-events { + pointer-events: none; +} \ No newline at end of file diff --git a/frontend/react/src/map/stylesheets/map.css b/frontend/react/src/map/stylesheets/map.css index 12e7e2e4..c7b07fa6 100644 --- a/frontend/react/src/map/stylesheets/map.css +++ b/frontend/react/src/map/stylesheets/map.css @@ -234,3 +234,7 @@ path.leaflet-interactive:focus { .smoke-orange-cursor { cursor: url("/images/cursors/smoke-orange.svg"), auto !important; } + +#map-container .disable-pointer-events { + pointer-events: none; +} \ No newline at end of file diff --git a/frontend/react/src/mission/airbase.ts b/frontend/react/src/mission/airbase.ts index a526d042..0135fd05 100644 --- a/frontend/react/src/mission/airbase.ts +++ b/frontend/react/src/mission/airbase.ts @@ -28,12 +28,14 @@ export class Airbase extends CustomMarker { 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"; + const el = this.getElement(); + if (el === undefined) return; + if (state === OlympusState.IDLE || state === OlympusState.AIRBASE) el.classList.remove("airbase-disable-pointer-events"); + else el.classList.add("airbase-disable-pointer-events"); }); AirbaseSelectedEvent.on((airbase) => { - this.#selected = (airbase === this); + this.#selected = airbase === this; if (this.getElement()?.querySelector(".airbase-icon")) (this.getElement()?.querySelector(".airbase-icon") as HTMLElement).dataset.selected = `${this.#selected}`; }); diff --git a/frontend/react/src/shortcut/shortcut.ts b/frontend/react/src/shortcut/shortcut.ts index 8f915653..b947ef3e 100644 --- a/frontend/react/src/shortcut/shortcut.ts +++ b/frontend/react/src/shortcut/shortcut.ts @@ -20,7 +20,17 @@ export class Shortcut { document.addEventListener("keyup", (ev: any) => { if (this.#modal) return; if (this.#keydown && this.getOptions().code === ev.code) { - console.log(`Keydown up for shortcut ${this.#id}`) + console.log(`Keyup for shortcut ${this.#id}`) + ev.preventDefault(); + this.#keydown = false; + this.getOptions().keyUpCallback(ev); + } + }); + + /* Forced keyup, in case the window loses focus */ + document.addEventListener("blur", (ev: any) => { + if (this.#keydown) { + console.log(`Keyup (forced by blur) for shortcut ${this.#id}`) ev.preventDefault(); this.#keydown = false; this.getOptions().keyUpCallback(ev); diff --git a/frontend/react/src/ui/components/olstatebutton.tsx b/frontend/react/src/ui/components/olstatebutton.tsx index 42bd329a..2ed67987 100644 --- a/frontend/react/src/ui/components/olstatebutton.tsx +++ b/frontend/react/src/ui/components/olstatebutton.tsx @@ -9,7 +9,7 @@ export function OlStateButton(props: { buttonColor?: string | null; checked: boolean; icon?: IconProp; - tooltip?: string | JSX.Element | JSX.Element[]; + tooltip?: string | (() => JSX.Element | JSX.Element[]); tooltipPosition?: string; onClick: () => void; onMouseUp?: () => void; @@ -64,7 +64,7 @@ export function OlStateButton(props: { {props.children} - {hover && props.tooltip && } + {hover && props.tooltip && } ); } diff --git a/frontend/react/src/ui/components/oltooltip.tsx b/frontend/react/src/ui/components/oltooltip.tsx index 1038065d..bbe26976 100644 --- a/frontend/react/src/ui/components/oltooltip.tsx +++ b/frontend/react/src/ui/components/oltooltip.tsx @@ -72,6 +72,12 @@ export function OlTooltip(props: { content: string | JSX.Element | JSX.Element[] const button = props.buttonRef.current as HTMLButtonElement; setPosition(content, button); + + const resizeObserver = new ResizeObserver(() => { + setPosition(content, button); + }); + resizeObserver.observe(content); + return () => resizeObserver.disconnect(); // clean up } }); diff --git a/frontend/react/src/ui/contextmenus/mapcontextmenu.tsx b/frontend/react/src/ui/contextmenus/mapcontextmenu.tsx index 1934063c..d729fedd 100644 --- a/frontend/react/src/ui/contextmenus/mapcontextmenu.tsx +++ b/frontend/react/src/ui/contextmenus/mapcontextmenu.tsx @@ -26,6 +26,7 @@ export function MapContextMenu(props: {}) { const [latLng, setLatLng] = useState(null as null | LatLng); const [unit, setUnit] = useState(null as null | Unit); const [selectedUnits, setSelectedUnits] = useState([] as Unit[]); + const [height, setHeight] = useState(0); var contentRef = useRef(null); @@ -68,6 +69,14 @@ export function MapContextMenu(props: {}) { content.style.left = `${newXPosition}px`; content.style.top = `${newYposition}px`; + + setHeight(content.clientHeight); + + const resizeObserver = new ResizeObserver(() => { + setHeight(content.clientHeight); + }); + resizeObserver.observe(content); + return () => resizeObserver.disconnect(); // clean up } }); diff --git a/frontend/react/src/ui/contextmenus/spawncontextmenu.tsx b/frontend/react/src/ui/contextmenus/spawncontextmenu.tsx index c08f62bf..fe00e69c 100644 --- a/frontend/react/src/ui/contextmenus/spawncontextmenu.tsx +++ b/frontend/react/src/ui/contextmenus/spawncontextmenu.tsx @@ -62,6 +62,7 @@ export function SpawnContextMenu(props: {}) { const [showCost, setShowCost] = useState(false); const [spawnCoalition, setSpawnCoalition] = useState("blue" as Coalition); const [showMore, setShowMore] = useState(false); + const [height, setHeight] = useState(0); useEffect(() => { if (selectedRole) setBlueprints(getApp()?.getUnitsManager().getDatabase().getByRole(selectedRole)); @@ -152,17 +153,24 @@ export function SpawnContextMenu(props: {}) { content.style.left = `${newXPosition}px`; content.style.top = `${newYposition}px`; + + const resizeObserver = new ResizeObserver(() => { + setHeight(content.clientHeight); + }); + resizeObserver.observe(content); + return () => resizeObserver.disconnect(); // clean up } }); + // TODO fix button being moved if overflowing return ( <>
diff --git a/frontend/react/src/ui/panels/maptoolbar.tsx b/frontend/react/src/ui/panels/maptoolbar.tsx index 5061b0ed..40e288c4 100644 --- a/frontend/react/src/ui/panels/maptoolbar.tsx +++ b/frontend/react/src/ui/panels/maptoolbar.tsx @@ -152,26 +152,28 @@ export function MapToolBar(props: {}) { key={"select"} checked={selectionEnabled} icon={faObjectGroup} - tooltip={ -
- {shortcutCombination(shortcuts["toggleSelectionEnabled"]?.getOptions())} -
Box selection
+ tooltip={() => ( +
+
+ {shortcutCombination(shortcuts["toggleSelectionEnabled"]?.getOptions())} +
Box selection
+
- } + )} tooltipPosition="side" onClick={() => { getApp().getMap().setSelectionEnabled(!selectionEnabled); if (!selectionEnabled) { - getApp() - .getMap() - .getContainer() - .addEventListener( - "mouseup", - () => { - getApp().getMap().setSelectionEnabled(false); - }, - { once: true, signal: controller.signal } - ); + window.addEventListener( + "mouseup", + () => { + getApp().getMap().setSelectionEnabled(false); + }, + { once: true, signal: controller.signal } + ); } else { controller.abort(); } @@ -184,12 +186,12 @@ export function MapToolBar(props: {}) { key={"copy"} checked={false} icon={faCopy} - tooltip={ + tooltip={() => (
{shortcutCombination(shortcuts["copyUnits"]?.getOptions())}
Copy selected units
- } + )} tooltipPosition="side" onClick={() => { getApp().getUnitsManager().copy(selectedUnits); @@ -199,14 +201,21 @@ export function MapToolBar(props: {}) { )} {copiedUnitsData.length > 0 && (
- (
{shortcutCombination(shortcuts["pasteUnits"]?.getOptions())}
Paste copied units
- } tooltipPosition="side" onClick={() => { - getApp().getMap().setPasteEnabled(!pasteEnabled) - }} /> + )} + tooltipPosition="side" + onClick={() => { + getApp().getMap().setPasteEnabled(!pasteEnabled); + }} + />
)} @@ -218,12 +227,18 @@ export function MapToolBar(props: {}) { key={contextActionIt.getId()} checked={contextActionIt === contextAction} icon={contextActionIt.getIcon()} - tooltip={ + tooltip={() => ( +
{shortcutCombination(contextActionIt.getOptions())}
{contextActionIt.getLabel()}
- } +
{contextActionIt.getDescription()}
+
+ )} tooltipPosition="side" buttonColor={CONTEXT_ACTION_COLORS[contextActionIt.getOptions().type ?? 0]} onClick={() => { diff --git a/frontend/react/src/ui/ui.css b/frontend/react/src/ui/ui.css index 62f01669..95147ad0 100644 --- a/frontend/react/src/ui/ui.css +++ b/frontend/react/src/ui/ui.css @@ -103,4 +103,22 @@ input[type="range"]:focus::-moz-range-thumb { .no-scrollbar { -ms-overflow-style: none; /* IE and Edge */ scrollbar-width: none; /* Firefox */ +} + +@keyframes tooltipFadeInHeight { + 99% { + height: 25px; + } + 100% { + height: fit-content; + } +} + +@keyframes tooltipFadeInWidth { + 99% { + width: 0px; + } + 100% { + width: 200px; + } } \ No newline at end of file diff --git a/frontend/react/src/unit/unit.ts b/frontend/react/src/unit/unit.ts index 60fa5731..cc42c5e2 100644 --- a/frontend/react/src/unit/unit.ts +++ b/frontend/react/src/unit/unit.ts @@ -1372,6 +1372,14 @@ export abstract class Unit extends CustomMarker { #onRightShortClick(e: any) { console.log(`Right short click on ${this.getUnitName()}`); + + window.clearTimeout(this.#rightMouseDownTimeout); + if ( + getApp().getState() === OlympusState.UNIT_CONTROL && + getApp().getMap().getDefaultContextAction() && + getApp().getMap().getDefaultContextAction()?.getTarget() === ContextActionTarget.POINT + ) + getApp().getMap().executeDefaultContextAction(null, this.getPosition(), e.originalEvent); } #onRightLongClick(e: any) { diff --git a/scripts/lua/backend/OlympusCommand.lua b/scripts/lua/backend/OlympusCommand.lua index 78fa426d..4479dcfd 100644 --- a/scripts/lua/backend/OlympusCommand.lua +++ b/scripts/lua/backend/OlympusCommand.lua @@ -1099,6 +1099,8 @@ function Olympus.setUnitsData(arg, time) else table["unitName"] = unit:getName() end + -- In case of AI units the callSign and the unitName will be the same + table["callsign"] = unit:getName() table["groupName"] = group:getName() table["isHuman"] = (unit:getPlayerName() ~= nil) table["hasTask"] = controller:hasTask()