// RadialMenuController.h // Local-only radial weapon selector for the Naked Desire project. // No replication, no RPCs: this is single-player UI logic. #pragma once #include "CoreMinimal.h" #include "Components/ActorComponent.h" #include "RadialMenuController.generated.h" class URadialMenuWidget; /** * One entry in the radial menu. Swap this out for your real item type later * (e.g. a soft pointer to a UPrimaryDataAsset) — the controller only ever * reads DisplayName + Icon to build slices, so the rest of the menu is agnostic. */ USTRUCT(BlueprintType) struct FRadialMenuEntry { GENERATED_BODY() UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Radial Menu") FText DisplayName; UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Radial Menu") TObjectPtr Icon = nullptr; // Optional payload tag so the owning code knows what to equip on confirm. UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Radial Menu") FName ItemId = NAME_None; // Set false to draw the slice greyed-out (e.g. weapon not yet unlocked). UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Radial Menu") bool bEnabled = true; }; // Fires when the player confirms a slice. Index is into the Entries array; -1 = cancelled. DECLARE_DYNAMIC_MULTICAST_DELEGATE_OneParam(FOnRadialSelectionConfirmed, int32, SelectedIndex); UCLASS(ClassGroup = (UI), meta = (BlueprintSpawnableComponent)) class URadialMenuController : public UActorComponent { GENERATED_BODY() public: URadialMenuController(); // --- Public API --------------------------------------------------------- // Call on "hold" pressed. Opens the menu, dilates time, shows the cursor. UFUNCTION(BlueprintCallable, Category = "Radial Menu") void OpenMenu(); // Call on "hold" released. Confirms the hovered slice and closes. UFUNCTION(BlueprintCallable, Category = "Radial Menu") void CloseAndConfirm(); // Close without selecting anything (e.g. pressed a cancel key). UFUNCTION(BlueprintCallable, Category = "Radial Menu") void CloseAndCancel(); UFUNCTION(BlueprintPure, Category = "Radial Menu") bool IsOpen() const { return bIsOpen; } UFUNCTION(BlueprintPure, Category = "Radial Menu") int32 GetHoveredIndex() const { return HoveredIndex; } // The data the menu draws. Populate from wherever you like (data table, // inventory component, hardcoded list) before calling OpenMenu(). UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Radial Menu") TArray Entries; UPROPERTY(BlueprintAssignable, Category = "Radial Menu") FOnRadialSelectionConfirmed OnSelectionConfirmed; // --- Tunables ----------------------------------------------------------- // How long after opening (real seconds) confirms are ignored, so the tap // that opens the wheel can't immediately select. ~0.15s is imperceptible // but covers the same-frame / next-frame case. Set 0 to disable. UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Radial Menu|Tuning", meta = (ClampMin = "0.0")) float OpenInputGuardSeconds = 0.15f; // Time scale while the menu is open. GTA sits around 0.2; 0.0 = full pause. UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Radial Menu|Tuning", meta = (ClampMin = "0.0", ClampMax = "1.0")) float OpenTimeDilation = 0.2f; // Dead zone (gamepad) / radius in px (mouse) below which we keep the last // hovered slice instead of snapping to a new one. Stops jitter near center. UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Radial Menu|Tuning") float SelectionDeadZone = 0.25f; UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Radial Menu|Tuning") TSubclassOf WidgetClass; protected: virtual void TickComponent(float DeltaTime, ELevelTick TickType, FActorComponentTickFunction* ThisTickFunction) override; private: // Converts a screen-space or stick direction into a slice index. // Angle is measured clockwise from straight up (12 o'clock), matching GTA. int32 AngleToSegment(float AngleDegrees) const; // Reads current input (mouse delta or right stick) and updates HoveredIndex. void UpdateHoverFromInput(); void SetTimeDilation(float Scale); UPROPERTY(Transient) TObjectPtr ActiveWidget = nullptr; bool bIsOpen = false; int32 HoveredIndex = -1; // Real-time timestamp when the menu opened. CloseAndConfirm ignores confirms // that arrive within OpenInputGuardSeconds, so the tap that opens the wheel // can't also register as a selection on the same/next frame. double OpenRealTimeSeconds = 0.0; // Cached so we can restore exactly what the game had before we opened. float CachedTimeDilation = 1.0f; };