fix(ui/loading): memoiza steps quando widget em async load + draggable status

- UIFrontEndFlowSubsystem: HandlePostLoadMap/HandlePlayerSpawned agora
  registram o StepId em PendingLoadingSteps_ quando ActiveLoadingScreen
  ainda eh null (race com RequestAsyncLoad do WBP_Loading). Lambda do
  callback async drena o set apos criar a tela, fechando o loading
  retroativamente. Servidor rapido (TryFinalizeEnterWorld) deixava o
  cliente preso em "entrando no mundo" quando o async load atrasava.

- UIPlayerStatus_Window: herda de UIDraggableWindow_Base (drag + persist
  via UZMMOUISaveGame). Rename CloseBtn -> Button_Close, adiciona
  Background_Panel/Container_Header/Container_Stats, NativeOnKeyDown
  (Alt+A fecha), NativeGetDesiredFocusTarget=self. Remove bIsBackHandler
  pra parar o "Cannot create action binding" no log (projeto nao tem
  DefaultBackAction no CommonUISettings).

- UIInGameFlowSubsystem.SetState: skip re-push do HUD quando voltando de
  StatusWindow/Inventory/Menu pro Playing (Old != None && != Playing) pra
  evitar Construct novo do HUD e o flicker dos textos HP/SP.

- Novos: UIDraggableWindow_Base (drag generico + persistencia) e
  UIWindowSaveGame (TMap<FName, FVector2D> por WindowId).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-05-26 14:50:49 -03:00
parent 079d05c117
commit 7fa10b4cb9
13 changed files with 442 additions and 12 deletions

View File

@@ -0,0 +1,131 @@
#include "UIDraggableWindow_Base.h"
#include "Components/Border.h"
#include "Engine/Engine.h"
#include "Engine/GameViewportClient.h"
#include "Input/Reply.h"
#include "UIWindowSaveGame.h"
void UUIDraggableWindow_Base::NativeOnActivated()
{
Super::NativeOnActivated();
ApplySavedPosition();
// Validacao usa translation + viewport size, ambos disponiveis ja' aqui
// (sem precisar do CachedGeometry/layout pass).
ValidatePositionInViewport();
}
void UUIDraggableWindow_Base::ApplySavedPosition()
{
const FName Id = GetWindowId();
if (Id.IsNone()) return;
UZMMOUISaveGame* Save = UZMMOUISaveGame::LoadOrCreate();
FVector2D Pos;
if (Save && Save->TryGetWindowPosition(Id, Pos))
{
SetRenderTranslation(Pos);
}
}
void UUIDraggableWindow_Base::PersistCurrentPosition()
{
const FName Id = GetWindowId();
if (Id.IsNone()) return;
UZMMOUISaveGame* Save = UZMMOUISaveGame::LoadOrCreate();
if (!Save) return;
Save->SetWindowPosition(Id, GetRenderTransform().Translation);
UZMMOUISaveGame::SaveNow(Save);
}
bool UUIDraggableWindow_Base::IsDragHandleHit(const FPointerEvent& MouseEvent) const
{
if (!Border_DragHandle) return false;
const FGeometry HandleGeom = Border_DragHandle->GetCachedGeometry();
return HandleGeom.IsUnderLocation(MouseEvent.GetScreenSpacePosition());
}
FReply UUIDraggableWindow_Base::NativeOnMouseButtonDown(const FGeometry& InGeometry,
const FPointerEvent& InMouseEvent)
{
if (InMouseEvent.GetEffectingButton() == EKeys::LeftMouseButton && IsDragHandleHit(InMouseEvent))
{
bMaybeDragging = true;
bDragging = false;
DragStartScreenPos = InMouseEvent.GetScreenSpacePosition();
WidgetStartTranslation = GetRenderTransform().Translation;
return FReply::Handled().CaptureMouse(TakeWidget());
}
return Super::NativeOnMouseButtonDown(InGeometry, InMouseEvent);
}
FReply UUIDraggableWindow_Base::NativeOnMouseMove(const FGeometry& InGeometry,
const FPointerEvent& InMouseEvent)
{
if (!bMaybeDragging && !bDragging)
{
return Super::NativeOnMouseMove(InGeometry, InMouseEvent);
}
const FVector2D Delta = InMouseEvent.GetScreenSpacePosition() - DragStartScreenPos;
if (!bDragging && Delta.SizeSquared() < DragStartThresholdPx * DragStartThresholdPx)
{
return FReply::Handled();
}
bDragging = true;
SetRenderTranslation(WidgetStartTranslation + Delta);
return FReply::Handled();
}
FReply UUIDraggableWindow_Base::NativeOnMouseButtonUp(const FGeometry& InGeometry,
const FPointerEvent& InMouseEvent)
{
if (InMouseEvent.GetEffectingButton() == EKeys::LeftMouseButton && (bMaybeDragging || bDragging))
{
const bool bWasRealDrag = bDragging;
bMaybeDragging = false;
bDragging = false;
if (bWasRealDrag)
{
PersistCurrentPosition();
}
return FReply::Handled().ReleaseMouseCapture();
}
return Super::NativeOnMouseButtonUp(InGeometry, InMouseEvent);
}
void UUIDraggableWindow_Base::NativeOnMouseLeave(const FPointerEvent& InMouseEvent)
{
Super::NativeOnMouseLeave(InMouseEvent);
}
void UUIDraggableWindow_Base::ValidatePositionInViewport()
{
if (!GEngine || !GEngine->GameViewport) return;
FVector2D ViewportSize;
GEngine->GameViewport->GetViewportSize(ViewportSize);
if (ViewportSize.IsNearlyZero()) return;
// RenderTransform.Translation esta em coordenadas de viewport (pixels),
// independente de DPI/offset do editor — comparacao direta funciona em
// PIE e em build standalone. Assume slot centralizado (default UMG
// stack); meio-viewport e' o quanto a translation pode crescer antes
// da janela sair completamente.
const FVector2D Tr = GetRenderTransform().Translation;
const float HalfX = ViewportSize.X * 0.5f;
const float HalfY = ViewportSize.Y * 0.5f;
const bool bOutOfBounds =
FMath::Abs(Tr.X) > HalfX - MinVisiblePixels ||
FMath::Abs(Tr.Y) > HalfY - MinVisiblePixels;
UE_LOG(LogTemp, Verbose, TEXT("DraggableWindow::Validate translation=(%.0f,%.0f) viewport=(%.0f,%.0f) outOfBounds=%d"),
Tr.X, Tr.Y, ViewportSize.X, ViewportSize.Y, bOutOfBounds ? 1 : 0);
if (bOutOfBounds)
{
SetRenderTranslation(FVector2D::ZeroVector);
PersistCurrentPosition();
}
}

View File

@@ -0,0 +1,90 @@
#pragma once
#include "CoreMinimal.h"
#include "CommonActivatableWidget.h"
#include "UIDraggableWindow_Base.generated.h"
class UBorder;
class UZMMOUISaveGame;
/**
* Base de janelas in-game movel + persistente. Subclasse define WindowId
* (FName estavel) via override de GetWindowId(); a Base cuida do drag
* (RenderTransform.Translation) e da persistencia (UZMMOUISaveGame).
*
* Convencao do WBP:
* - Bind opcional `Border_DragHandle` cobrindo a area arrastavel (header).
* Sem ele, a janela nao captura drag (vira static).
* - `Border_DragHandle->Visibility = Visible` (precisa receber HitTest).
*
* Por que RenderTransform e nao Slot.Position?
* - Funciona em qualquer layout slot (Overlay, VBox, Canvas), nao so
* CanvasPanelSlot.
* - Nao polui o asset (translation e' transient/visual, nao layout-layout).
*
* Persistencia: na primeira ativacao, carrega a posicao salva do WindowId;
* ao soltar o drag, grava e salva o slot.
*/
UCLASS(Abstract, Blueprintable)
class ZMMO_API UUIDraggableWindow_Base : public UCommonActivatableWidget
{
GENERATED_BODY()
public:
/**
* Identificador estavel da janela (chave no save). Subclasse OBRIGADA
* a sobrescrever — default NAME_None desativa persistencia.
*/
virtual FName GetWindowId() const { return NAME_None; }
protected:
virtual void NativeOnActivated() override;
virtual FReply NativeOnMouseButtonDown(const FGeometry& InGeometry, const FPointerEvent& InMouseEvent) override;
virtual FReply NativeOnMouseMove(const FGeometry& InGeometry, const FPointerEvent& InMouseEvent) override;
virtual FReply NativeOnMouseButtonUp(const FGeometry& InGeometry, const FPointerEvent& InMouseEvent) override;
virtual void NativeOnMouseLeave(const FPointerEvent& InMouseEvent) override;
/**
* Define se o ponto sob o cursor pertence a area de drag. Default: usa
* Border_DragHandle (se bindado). Subclass pode override pra usar outro
* widget (ex.: Container_Header ja' existente) sem precisar adicionar
* um Border dedicado no WBP.
*/
virtual bool IsDragHandleHit(const FPointerEvent& MouseEvent) const;
/** Aplica a posicao salva (se houver) como RenderTransform.Translation. */
void ApplySavedPosition();
/** Grava a posicao atual no save e persiste no slot. */
void PersistCurrentPosition();
/**
* Se a janela ficou fora do viewport (ex.: usuario arrastou pra borda e
* depois a resolucao mudou ou ele fechou e reabriu), reseta translation
* pra (0,0). Chamado no proximo tick apos Activated — cached geometry
* so' tem valor depois do primeiro layout pass.
*/
UFUNCTION()
void ValidatePositionInViewport();
/** Quanto da janela precisa ficar visivel pra contar como "no viewport". */
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Drag")
float MinVisiblePixels = 50.f;
/**
* Bind opcional do WBP. Apenas drags iniciados com o cursor SOBRE
* este border sao aceitos — bloqueia drag por clicar nos rows/botoes.
*/
UPROPERTY(BlueprintReadOnly, meta = (BindWidgetOptional), Category = "Drag")
TObjectPtr<UBorder> Border_DragHandle;
/** Distancia minima do mouse pra confirmar "drag iniciado" (anti-jitter). */
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Drag")
float DragStartThresholdPx = 4.f;
private:
bool bMaybeDragging = false; ///< pressed mas ainda nao passou do threshold
bool bDragging = false; ///< drag confirmado
FVector2D DragStartScreenPos = FVector2D::ZeroVector;
FVector2D WidgetStartTranslation = FVector2D::ZeroVector;
};

View File

@@ -0,0 +1,43 @@
#include "UIWindowSaveGame.h"
#include "Kismet/GameplayStatics.h"
const FString UZMMOUISaveGame::SlotName = TEXT("UIPrefs");
UZMMOUISaveGame* UZMMOUISaveGame::LoadOrCreate()
{
if (UGameplayStatics::DoesSaveGameExist(SlotName, UserIndex))
{
if (UZMMOUISaveGame* Loaded = Cast<UZMMOUISaveGame>(
UGameplayStatics::LoadGameFromSlot(SlotName, UserIndex)))
{
return Loaded;
}
}
return Cast<UZMMOUISaveGame>(
UGameplayStatics::CreateSaveGameObject(UZMMOUISaveGame::StaticClass()));
}
void UZMMOUISaveGame::SaveNow(UZMMOUISaveGame* Save)
{
if (Save)
{
UGameplayStatics::SaveGameToSlot(Save, SlotName, UserIndex);
}
}
bool UZMMOUISaveGame::TryGetWindowPosition(FName WindowId, FVector2D& OutPos) const
{
if (const FVector2D* Found = WindowPositions.Find(WindowId))
{
OutPos = *Found;
return true;
}
OutPos = FVector2D::ZeroVector;
return false;
}
void UZMMOUISaveGame::SetWindowPosition(FName WindowId, FVector2D Pos)
{
WindowPositions.Add(WindowId, Pos);
}

View File

@@ -0,0 +1,46 @@
#pragma once
#include "CoreMinimal.h"
#include "GameFramework/SaveGame.h"
#include "UIWindowSaveGame.generated.h"
/**
* Persistencia de prefs de UI por janela (posicao, e futuramente tamanho,
* estado expandido, etc). Usado por UUIDraggableWindow_Base — toda janela
* movel registra uma chave (FName WindowId) e grava aqui ao soltar o drag.
*
* Arquivo: [Project]/Saved/SaveGames/UIPrefs_0.sav (binary blob via
* UE serializer). Mesmo padrao do [[ZMMOLoginSaveGame]].
*
* Helpers static abaixo encapsulam load-or-create do slot e Get/Set/Save
* pra que a Base nao precise repetir a cerimonia.
*/
UCLASS()
class ZMMO_API UZMMOUISaveGame : public USaveGame
{
GENERATED_BODY()
public:
/** Posicao normalizada (translation absoluta em pixels da viewport) por janela. */
UPROPERTY()
TMap<FName, FVector2D> WindowPositions;
static const FString SlotName;
static constexpr int32 UserIndex = 0;
/**
* Carrega o save existente ou cria um novo (nao salva ainda).
* Sempre retorna instancia valida.
*/
static UZMMOUISaveGame* LoadOrCreate();
/** Salva o slot atual (sincrono — chamado on-mouse-up, custo baixo). */
static void SaveNow(UZMMOUISaveGame* Save);
/** Get com default — se WindowId nao foi gravado retorna FVector2D::ZeroVector. */
UFUNCTION(BlueprintCallable, Category = "UI|Prefs")
bool TryGetWindowPosition(FName WindowId, FVector2D& OutPos) const;
UFUNCTION(BlueprintCallable, Category = "UI|Prefs")
void SetWindowPosition(FName WindowId, FVector2D Pos);
};

View File

@@ -233,6 +233,16 @@ void UUIFrontEndFlowSubsystem::ResolveAndPushScreen(EZMMOFrontEndState State)
GetLoadingProfiles(), GetLoadingTips());
Loading->OnLoadingComplete.AddDynamic(this, &UUIFrontEndFlowSubsystem::HandleLoadingComplete);
Loading->MarkStepDone(TEXT("Travel"));
// Drena steps que dispararam ANTES do widget existir
// (HandlePostLoadMap/HandlePlayerSpawned podem ter
// rodado durante o RequestAsyncLoad). Sem isso o
// loading fica eterno se o servidor for rápido demais.
for (const FName StepId : PendingLoadingSteps_)
{
Loading->MarkStepDone(StepId);
}
PendingLoadingSteps_.Reset();
}
}
}
@@ -423,8 +433,10 @@ void UUIFrontEndFlowSubsystem::HandlePostLoadMap(UWorld* /*LoadedWorld*/)
return;
}
bTravelingToWorld = false;
SetState(EZMMOFrontEndState::InWorld);
// Race fix: widget ainda em RequestAsyncLoad; memoiza pra drenar quando
// a tela for criada (ver lambda em ResolveAndPushScreen). Não fallback
// pra InWorld — esperamos a tela existir e o Spawn chegar.
PendingLoadingSteps_.Add(TEXT("MapLoaded"));
}
void UUIFrontEndFlowSubsystem::HandlePlayerSpawned(int32 /*EntityId*/, bool bIsLocal,
@@ -435,6 +447,15 @@ void UUIFrontEndFlowSubsystem::HandlePlayerSpawned(int32 /*EntityId*/, bool bIsL
if (UUILoadingScreen_Base* Loading = ActiveLoadingScreen.Get())
{
Loading->MarkStepDone(TEXT("Spawn"));
return;
}
// Race fix: spawn pode chegar ANTES do widget existir se o servidor for
// rápido (TryFinalizeEnterWorld) e o RequestAsyncLoad lento. Memoiza
// pra drenar no callback do async load.
if (bTravelingToWorld)
{
PendingLoadingSteps_.Add(TEXT("Spawn"));
}
}
@@ -445,6 +466,7 @@ void UUIFrontEndFlowSubsystem::HandleLoadingComplete()
Loading->OnLoadingComplete.RemoveDynamic(this, &UUIFrontEndFlowSubsystem::HandleLoadingComplete);
}
ActiveLoadingScreen.Reset();
PendingLoadingSteps_.Reset();
bTravelingToWorld = false;
SetState(EZMMOFrontEndState::InWorld);
}

View File

@@ -249,6 +249,15 @@ private:
*/
TWeakObjectPtr<UUILoadingScreen_Base> ActiveLoadingScreen;
/**
* Race fix: o widget de loading é criado via RequestAsyncLoad (assíncrono).
* Se o servidor for rápido (TryFinalizeEnterWorld), HandlePostLoadMap e/ou
* HandlePlayerSpawned podem disparar ANTES de ActiveLoadingScreen existir
* — nesse caso o MarkStepDone vira no-op e o loading fica eterno.
* Memoizamos os StepIds aqui e drenamos quando a tela for criada.
*/
TSet<FName> PendingLoadingSteps_;
bool bNetBound = false;
bool bTravelingToWorld = false;
FDelegateHandle PostLoadMapHandle;

View File

@@ -81,16 +81,26 @@ void UUIInGameFlowSubsystem::SetState(EZMMOInGameUIState NewState)
// (Status/Inventory/Menu) limpando o Layer.GameMenu — HUD em Layer.Game
// permanece. Para qualquer outro estado, mantemos HUD e empilhamos no
// layer correspondente.
const bool bGoingToPlaying = (NewState == EZMMOInGameUIState::Playing);
if (UUIManagerSubsystem* Mgr = GetUIManager())
{
const bool bGoingToPlaying = (NewState == EZMMOInGameUIState::Playing);
if (bGoingToPlaying)
{
Mgr->ClearLayer(ZMMOUITags::UI_Layer_GameMenu.GetTag());
}
}
ResolveAndPushScreen(NewState);
// Skip re-push do HUD quando ja' estavamos in-game (Old != None significa
// que StartInGame ja' rodou e o HUD esta no Layer.Game). Senao, cada vez
// que o usuario fechar StatusWindow/Inventory/etc, o HUD seria recriado
// — flicker em todos os textos (HP/SP/etc) por causa do Construct novo.
const bool bSkipRepush = bGoingToPlaying
&& OldState != EZMMOInGameUIState::None
&& OldState != EZMMOInGameUIState::Playing;
if (!bSkipRepush)
{
ResolveAndPushScreen(NewState);
}
OnStateChanged.Broadcast(NewState);
}

View File

@@ -2,7 +2,9 @@
#include "CommonInputTypeEnum.h"
#include "Components/Button.h"
#include "Components/HorizontalBox.h"
#include "Components/TextBlock.h"
#include "InputCoreTypes.h"
#include "Engine/World.h"
#include "GameFramework/PlayerController.h"
#include "GameFramework/PlayerState.h"
@@ -20,8 +22,18 @@
UUIPlayerStatus_Window::UUIPlayerStatus_Window(const FObjectInitializer& ObjectInitializer)
: Super(ObjectInitializer)
{
bIsBackHandler = true;
// bIsBackHandler exige DefaultBackAction configurada no CommonUISettings;
// projeto ainda não tem DT de input actions, então registrar o handler
// poluía o log com "Cannot create action binding". Fechamento da janela
// vai por Alt+A (NativeOnKeyDown) e Button_Close.
bAutoActivate = true;
// Focusable + DesiredFocusTarget=self → recebe NativeOnKeyDown (Alt+A).
SetIsFocusable(true);
}
UWidget* UUIPlayerStatus_Window::NativeGetDesiredFocusTarget() const
{
return const_cast<UUIPlayerStatus_Window*>(this);
}
TOptional<FUIInputConfig> UUIPlayerStatus_Window::GetDesiredInputConfig() const
@@ -51,7 +63,14 @@ void UUIPlayerStatus_Window::NativeOnActivated()
if (TitleText) TitleText->SetText(FText::FromString(TEXT("Status")));
if (CloseBtn) CloseBtn->OnClicked.AddDynamic(this, &UUIPlayerStatus_Window::OnCloseClicked);
if (Button_Close) Button_Close->OnClicked.AddDynamic(this, &UUIPlayerStatus_Window::HandleCloseClicked);
// Background_Panel default é SELF_HIT_TEST_INVISIBLE; precisa Visible
// pra capturar clicks em areas vazias do modal e bubbla pra Window.
if (Background_Panel)
{
Background_Panel->SetVisibility(ESlateVisibility::Visible);
}
BindToLocalPlayer();
@@ -69,7 +88,7 @@ void UUIPlayerStatus_Window::NativeOnDeactivated()
UnbindRow(Row_Str); UnbindRow(Row_Agi); UnbindRow(Row_Vit);
UnbindRow(Row_Int); UnbindRow(Row_Dex); UnbindRow(Row_Luk);
if (CloseBtn) CloseBtn->OnClicked.RemoveDynamic(this, &UUIPlayerStatus_Window::OnCloseClicked);
if (Button_Close) Button_Close->OnClicked.RemoveDynamic(this, &UUIPlayerStatus_Window::HandleCloseClicked);
Super::NativeOnDeactivated();
}
@@ -219,7 +238,7 @@ void UUIPlayerStatus_Window::RefreshFromSnapshot(const FZMMOAttributesSnapshot&
SetAllPrimaryRowsPlusEnabled(bCanAlloc);
}
void UUIPlayerStatus_Window::OnCloseClicked()
void UUIPlayerStatus_Window::HandleCloseClicked()
{
if (UGameInstance* GI = GetGameInstance())
{
@@ -229,3 +248,33 @@ void UUIPlayerStatus_Window::OnCloseClicked()
}
}
}
FReply UUIPlayerStatus_Window::NativeOnKeyDown(const FGeometry& Geom, const FKeyEvent& KeyEvent)
{
// Alt+A em modo Menu — PlayerController nao recebe (CommonUI absorve).
// Handle aqui pra completar o toggle.
if (KeyEvent.GetKey() == EKeys::A && KeyEvent.IsAltDown())
{
if (UGameInstance* GI = GetGameInstance())
{
if (UUIInGameFlowSubsystem* Flow = GI->GetSubsystem<UUIInGameFlowSubsystem>())
{
Flow->SetState(EZMMOInGameUIState::Playing);
return FReply::Handled();
}
}
}
return Super::NativeOnKeyDown(Geom, KeyEvent);
}
bool UUIPlayerStatus_Window::IsDragHandleHit(const FPointerEvent& MouseEvent) const
{
// Drag em qualquer parte do modal EXCETO Container_Stats (rows + botões +STR).
// Botão close consome o evento antes (UButton.OnClicked) e não bubbla aqui.
const FVector2D Pos = MouseEvent.GetScreenSpacePosition();
if (Container_Stats && Container_Stats->GetCachedGeometry().IsUnderLocation(Pos))
{
return false;
}
return true;
}

View File

@@ -1,12 +1,13 @@
#pragma once
#include "CoreMinimal.h"
#include "CommonActivatableWidget.h"
#include "UI/Common/UIDraggableWindow_Base.h"
#include "ZMMOAttributeTypes.h"
#include "UIPlayerStatus_Window.generated.h"
class UButton;
class UTextBlock;
class UHorizontalBox;
class UUIPlayerStatus_BonusRow;
class UUIPlayerStatus_StatRow;
class UZMMOAttributeComponent;
@@ -22,18 +23,46 @@ class UZMMOAttributeComponent;
* pra suportar variantes (ex.: HUD compacto sem CRIT/ASPD).
*/
UCLASS(Abstract, Blueprintable, BlueprintType)
class ZMMO_API UUIPlayerStatus_Window : public UCommonActivatableWidget
class ZMMO_API UUIPlayerStatus_Window : public UUIDraggableWindow_Base
{
GENERATED_BODY()
public:
UUIPlayerStatus_Window(const FObjectInitializer& ObjectInitializer);
/** Chave de persistencia no UZMMOUISaveGame. Estavel — nao renomear. */
virtual FName GetWindowId() const override { return TEXT("PlayerStatus"); }
protected:
virtual void NativeOnActivated() override;
virtual void NativeOnDeactivated() override;
virtual TOptional<FUIInputConfig> GetDesiredInputConfig() const override;
/** Diz ao stack pra mover foco pra ca' (precisa pra NativeOnKeyDown ser chamado). */
virtual UWidget* NativeGetDesiredFocusTarget() const override;
/** Atalho Alt+A — fecha a janela em modo Menu (PC controller nao recebe). */
virtual FReply NativeOnKeyDown(const FGeometry& Geom, const FKeyEvent& KeyEvent) override;
/** Drag area = Container_Header (header inteiro). Header_DragHandle no WBP nao necessario. */
virtual bool IsDragHandleHit(const FPointerEvent& MouseEvent) const override;
/** Header do WBP (HorizontalBox raiz da identidade) — usado como drag area. */
UPROPERTY(BlueprintReadOnly, meta = (BindWidgetOptional), Category = "Zeus|Status|Header") TObjectPtr<UHorizontalBox> Container_Header;
/**
* Zona EXCLUÍDA do drag — é onde ficam os rows interativos (+STR etc.).
* Clicar aqui não deve iniciar drag mesmo em area "vazia" entre rows.
*/
UPROPERTY(BlueprintReadOnly, meta = (BindWidgetOptional), Category = "Zeus|Status") TObjectPtr<UWidget> Container_Stats;
/**
* Panel de fundo (UIPanel_Base). Setado pra Visible em NativeOnActivated
* pra capturar clicks em areas vazias do modal — sem isso, drag só funciona
* onde algum filho é Visible (header/footer).
*/
UPROPERTY(BlueprintReadOnly, meta = (BindWidgetOptional), Category = "Zeus|Status") TObjectPtr<UWidget> Background_Panel;
UPROPERTY(BlueprintReadOnly, meta = (BindWidgetOptional), Category = "Zeus|Status") TObjectPtr<UTextBlock> TitleText;
UPROPERTY(BlueprintReadOnly, meta = (BindWidgetOptional), Category = "Zeus|Status") TObjectPtr<UTextBlock> StatusPointText;
@@ -59,7 +88,8 @@ protected:
UPROPERTY(BlueprintReadOnly, meta = (BindWidgetOptional), Category = "Zeus|Status|Derived") TObjectPtr<UUIPlayerStatus_BonusRow> Row_Crit;
UPROPERTY(BlueprintReadOnly, meta = (BindWidgetOptional), Category = "Zeus|Status|Derived") TObjectPtr<UUIPlayerStatus_BonusRow> Row_Aspd;
UPROPERTY(BlueprintReadOnly, meta = (BindWidgetOptional), Category = "Zeus|Status") TObjectPtr<UButton> CloseBtn;
/** Botao de fechar na header — UButton simples (placeholder, estilizar depois). */
UPROPERTY(BlueprintReadOnly, meta = (BindWidgetOptional), Category = "Zeus|Status") TObjectPtr<UButton> Button_Close;
private:
void BindToLocalPlayer();
@@ -71,7 +101,7 @@ private:
UFUNCTION() void HandleAttributesChanged(const FZMMOAttributesSnapshot& Snapshot);
UFUNCTION() void HandleStatAllocReply(bool bAccepted, int32 Reason);
UFUNCTION() void HandleRowPlusClicked(int32 StatId);
UFUNCTION() void OnCloseClicked();
UFUNCTION() void HandleCloseClicked();
UPROPERTY(Transient)
TWeakObjectPtr<UZMMOAttributeComponent> BoundComponent;