132 lines
5.2 KiB
C++
132 lines
5.2 KiB
C++
// 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<UTexture2D> 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; }
|
|
|
|
// Update the hovered slice from a direction vector (X right, Y up), measured
|
|
// in the same frame as the menu. Used by both the mouse path (controller
|
|
// tick) and the gamepad path (widget forwards the right-stick vector here,
|
|
// since CommonUI consumes the stick before the controller can read it).
|
|
// Magnitude below the dead zone leaves the current hover unchanged.
|
|
void UpdateHoverFromDirection(float DirX, float DirY, float Magnitude);
|
|
|
|
// 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<FRadialMenuEntry> 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<URadialMenuWidget> WidgetClass;
|
|
|
|
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<URadialMenuWidget> 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;
|
|
}; |