feat(frontend): primeira tela WBP_Boot (conecta ao ZeusCharServer)

Primeira tela concreta do front-end, cópia fiel do WBP_BootScreen gerado
pelo Zeus UMG Forge, adaptada aos componentes do projeto:

- UUIBootScreen_Base : UUIActivatableScreen_Base — observa o
  UZeusCharServerSubsystem; libera "Iniciar" no OnConnected; clique →
  UUIFrontEndFlowSubsystem::RequestEnterLogin → estado Login.
- UUISpinner_Base (componente compartilhado temável) + FUIStyleSpinner.
- FUIStyle.ScreenBackground (brush data-driven); fundo via DT_UI_Styles
  (T_Boot_Background em UI/Themes/Default/Boot), sem hard-ref no WBP (§5).
- ThemeKeys: Boot_Background/Logo/Music.
- Correção do Flow: pré-login agora conecta no UZeusCharServerSubsystem
  (WebSocket 7100), não mais no UDP do world server (que fica só p/
  EnteringWorld/InWorld); RequestEnterLogin() adicionado.
- WBP_Boot duplicado fiel + reparent UUIBootScreen_Base; swap manual no
  Designer (Btn_Start/Spinner_Connect/Text_Status); slots de layout
  (Container_Root/Stage/Content) ajustados p/ preencher/centralizar.
- WBP_PrimaryGameLayout: Stack_Modal corrigido p/ Fill/Fill.
- DA_FrontEndScreenSet: StateScreens[Boot] = WBP_Boot.
- ARQUITETURA.md (§2.1/§2.2/§4.8 + nota CharServer).

Validado em PIE: tela centralizada, fundo, conexão ao ZeusCharServer e
liberação do botão. Crash do plugin tratado em commit separado do server.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-05-19 05:15:17 -03:00
parent 2cd8b3b2bb
commit 1e501e52a8
16 changed files with 521 additions and 29 deletions

View File

@@ -210,7 +210,8 @@ Content/
│ ├── UI/ ← HUD, Login, CharacterSelect, Inventory,
│ │ │ Chat, Party, Guild, Trade, Quest, Map,
│ │ │ Shared, Icons
│ │ ├── Shared/ ← WBPs reutilizáveis (UI_Button_Master…)
│ │ ├── Shared/ ← WBPs reutilizáveis (UI_Button_Master,
│ │ │ UI_Panel_Master, UI_Spinner_Master)
│ │ ├── FrontEnd/ ← WBP_PrimaryGameLayout, DA_FrontEndScreenSet,
│ │ │ telas refinadas (Boot/Login/… vindas do Forge)
│ │ ├── Fonts/ ← Font_*/FF_* (Cinzel, Rajdhani)
@@ -268,9 +269,11 @@ Source/ZMMO/
│ ├── Network/ ← UZMMOWorldSubsystem
│ └── UI/ ← runtime de UI (não é contrato de dados)
│ ├── ZMMOThemeSubsystem.* ← resolução de tema
│ ├── Widgets/ ← UUIButton_Base, UUIPanel_Base
│ ├── Widgets/ ← UUIButton_Base, UUIPanel_Base,
│ │ UUISpinner_Base
│ └── FrontEnd/ ← infra de navegação CommonUI:
│ UUIActivatableScreen_Base,
│ UUIBootScreen_Base (1ª tela),
│ UUIPrimaryGameLayout_Base,
│ UUIManagerSubsystem (LocalPlayer),
│ UUIFrontEndFlowSubsystem (GameInstance),
@@ -622,6 +625,15 @@ Fluxo geral: **Boot → Connecting → Login → ServerSelect → Lobby → Ente
navegadas por um stack CommonUI **dentro** da tela do Lobby — não são estados
de topo do fluxo.
**Rede do pré-login:** o `UUIFrontEndFlowSubsystem` dirige a conexão ao
**`UZeusCharServerSubsystem`** (WebSocket, `CharServerUrl` de Project Settings,
default `ws://127.0.0.1:7100/zeus`) ao entrar em **Boot** — NÃO o
`UZeusNetworkSubsystem` (UDP, world server, usado só no travel
EnteringWorld→InWorld). A tela Boot (`UUIBootScreen_Base`) observa o
CharServer: libera o botão "Iniciar" no `OnConnected`; o clique chama
`UUIFrontEndFlowSubsystem::RequestEnterLogin()` → estado Login. Tela nunca
segura hard-ref de asset de tema (cores via `FUIStyle`; §5).
1. Prototipar a tela em HTML/CSS em `Tools/Templates/MMO_Widget/` (ou usar os
templates já existentes).
2. Rodar o Zeus UMG Forge (HTML→UMG). Saída crua: `Content/AutoCreated/<batch>/`.

Binary file not shown.

Binary file not shown.

View File

@@ -12,6 +12,11 @@ enum class EZMMOThemeKey : uint8
HUD_HealthBar UMETA(DisplayName = "HUD - Health Bar"),
HUD_ManaBar UMETA(DisplayName = "HUD - Mana Bar"),
// Boot
Boot_Background UMETA(DisplayName = "Boot - Background"),
Boot_Logo UMETA(DisplayName = "Boot - Logo"),
Boot_Music UMETA(DisplayName = "Boot - Music"),
// Login / Character Select
Login_Background UMETA(DisplayName = "Login - Background"),
Login_Logo UMETA(DisplayName = "Login - Logo"),

View File

@@ -42,6 +42,17 @@ struct ZMMO_API FUIStyle
UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "UI Style")
FUIStyleTooltip Tooltip;
UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "UI Style")
FUIStyleSpinner Spinner;
/**
* Fundo de tela cheia do front-end (ex.: Boot/Login). Brush data-driven:
* a tela aplica no seu Border_Bg via RefreshUIStyle — nunca hard-ref no
* WBP (ARQUITETURA.md §5). Preenchido em DT_UI_Styles.
*/
UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "UI Style")
FSlateBrush ScreenBackground;
};
/**

View File

@@ -444,3 +444,34 @@ struct ZMMO_API FUIStyleTooltip
UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "UI Style|Tooltip")
FLinearColor TextColor = FLinearColor(FColor(244, 240, 230)); // Text
};
/** Spinner/throbber indeterminado (ex.: "Conectando..." no Boot). */
USTRUCT(BlueprintType)
struct ZMMO_API FUIStyleSpinner
{
GENERATED_BODY()
/** Cor das peças do spinner. Default = Gold (Aurora Arcana). */
UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "UI Style|Spinner")
FLinearColor Color = FLinearColor(FColor(201, 167, 90)); // Gold
/** Cor do "trilho"/peças apagadas (UCircularThrobber não usa; reservado). */
UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "UI Style|Spinner")
FLinearColor TrackColor = FLinearColor(FColor(58, 75, 120, 140));
/** Raio do anel (px). */
UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "UI Style|Spinner")
float Radius = 28.f;
/** Espessura da peça (px). */
UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "UI Style|Spinner")
float Thickness = 3.f;
/** Número de peças do UCircularThrobber. */
UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "UI Style|Spinner", meta = (ClampMin = "1"))
int32 NumberOfPieces = 8;
/** Período de uma volta (s). */
UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "UI Style|Spinner", meta = (ClampMin = "0.05"))
float Period = 0.9f;
};

View File

@@ -0,0 +1,175 @@
#include "UIBootScreen_Base.h"
#include "ZMMO.h"
#include "UIButton_Base.h"
#include "UISpinner_Base.h"
#include "UIFrontEndFlowSubsystem.h"
#include "ZMMOThemeSubsystem.h"
#include "ZeusCharServerSubsystem.h"
#include "CommonTextBlock.h"
#include "Components/Border.h"
#include "Engine/GameInstance.h"
#include "Engine/DataTable.h"
#include "UI/UIStyleRow.h"
namespace
{
// Runtime usa o tema ativo; design-time carrega a row "Default" de
// DT_UI_Styles para o Designer ver o fundo (mesmo padrão de UUIPanel_Base).
const FUIStyle& ResolveBootStyle(const UUserWidget* Widget, const FUIStyle& Fallback)
{
if (Widget)
{
if (const UGameInstance* GI = Widget->GetGameInstance())
{
if (const UZMMOThemeSubsystem* Theme = GI->GetSubsystem<UZMMOThemeSubsystem>())
{
return Theme->GetActiveUIStyle();
}
}
}
static FUIStyle DesignStyle;
static bool bLoaded = false;
if (!bLoaded)
{
if (const UDataTable* DT = LoadObject<UDataTable>(nullptr,
TEXT("/Game/ZMMO/Data/UI/DT_UI_Styles.DT_UI_Styles")))
{
if (const FUIStyleRow* Row = DT->FindRow<FUIStyleRow>(
FName(TEXT("Default")), TEXT("UIBootDesign"), false))
{
DesignStyle = Row->Style;
bLoaded = true;
}
}
}
return bLoaded ? DesignStyle : Fallback;
}
}
void UUIBootScreen_Base::NativePreConstruct()
{
Super::NativePreConstruct();
RefreshUIStyle(); // preview do fundo no Designer (DT_UI_Styles "Default")
}
void UUIBootScreen_Base::NativeOnActivated()
{
Super::NativeOnActivated(); // base: RefreshUIStyle + BP_OnScreenActivated
if (Btn_Start)
{
Btn_Start->OnClicked().AddUObject(this, &UUIBootScreen_Base::HandleStartClicked);
}
UZeusCharServerSubsystem* Char = GetCharServer();
const bool bConnected = Char && Char->IsConnected();
SetReady(bConnected);
if (Char && !bCharBound)
{
Char->OnConnected.AddDynamic(this, &UUIBootScreen_Base::HandleCharConnected);
Char->OnConnectionFailed.AddDynamic(this, &UUIBootScreen_Base::HandleCharConnectionFailed);
bCharBound = true;
}
// A conexão em si é disparada pelo UUIFrontEndFlowSubsystem (estado Boot).
}
void UUIBootScreen_Base::NativeOnDeactivated()
{
if (Btn_Start)
{
Btn_Start->OnClicked().RemoveAll(this);
}
if (bCharBound)
{
if (UZeusCharServerSubsystem* Char = GetCharServer())
{
Char->OnConnected.RemoveDynamic(this, &UUIBootScreen_Base::HandleCharConnected);
Char->OnConnectionFailed.RemoveDynamic(this, &UUIBootScreen_Base::HandleCharConnectionFailed);
}
bCharBound = false;
}
Super::NativeOnDeactivated();
}
void UUIBootScreen_Base::RefreshUIStyle_Implementation()
{
Super::RefreshUIStyle_Implementation();
const FUIStyle Fallback;
const FUIStyle& AS = ResolveBootStyle(this, Fallback);
if (Border_Bg)
{
// Fundo data-driven do DT_UI_Styles (T_Boot_Background) — sem
// hard-ref de textura no WBP (§5). Fallback p/ cor se vazio.
if (AS.ScreenBackground.GetResourceObject() != nullptr)
{
Border_Bg->SetBrush(AS.ScreenBackground);
}
else
{
Border_Bg->SetBrushColor(AS.Palette.Bg0);
}
}
if (Text_Status)
{
Text_Status->SetColorAndOpacity(FSlateColor(AS.Semantic.OnSurfaceMuted));
}
}
void UUIBootScreen_Base::SetReady(bool bReady)
{
if (Btn_Start)
{
Btn_Start->SetIsInteractionEnabled(bReady);
}
if (Spinner_Connect)
{
Spinner_Connect->SetVisibility(
bReady ? ESlateVisibility::Collapsed : ESlateVisibility::HitTestInvisible);
}
if (Text_Status)
{
Text_Status->SetText(bReady ? ReadyText : ConnectingText);
}
}
UZeusCharServerSubsystem* UUIBootScreen_Base::GetCharServer() const
{
const UGameInstance* GI = GetGameInstance();
return GI ? GI->GetSubsystem<UZeusCharServerSubsystem>() : nullptr;
}
void UUIBootScreen_Base::HandleStartClicked()
{
UZeusCharServerSubsystem* Char = GetCharServer();
if (!Char || !Char->IsConnected())
{
return; // botão só age quando conectado (defensivo)
}
if (const UGameInstance* GI = GetGameInstance())
{
if (UUIFrontEndFlowSubsystem* Flow = GI->GetSubsystem<UUIFrontEndFlowSubsystem>())
{
Flow->RequestEnterLogin();
}
}
}
void UUIBootScreen_Base::HandleCharConnected()
{
UE_LOG(LogZMMO, Log, TEXT("BootScreen: CharServer conectado — botão liberado."));
SetReady(true);
}
void UUIBootScreen_Base::HandleCharConnectionFailed(FString Reason)
{
UE_LOG(LogZMMO, Warning, TEXT("BootScreen: falha ao conectar ao CharServer: %s"), *Reason);
SetReady(false);
if (Text_Status)
{
Text_Status->SetText(FailedText);
}
}

View File

@@ -0,0 +1,73 @@
#pragma once
#include "CoreMinimal.h"
#include "UIActivatableScreen_Base.h"
#include "UIBootScreen_Base.generated.h"
class UUIButton_Base;
class UUISpinner_Base;
class UCommonTextBlock;
class UBorder;
class UZeusCharServerSubsystem;
/**
* Tela de Boot — primeira tela do front-end. Reconstruída a partir do mock
* do Zeus UMG Forge com os widgets compartilhados (UI_Button_Master,
* UI_Spinner_Master). Camada Abstract; o WBP concreto (WBP_Boot) herda
* DIRETO desta classe (ARQUITETURA.md §3.3).
*
* O UUIFrontEndFlowSubsystem dispara a conexão ao ZeusCharServer (WebSocket)
* ao entrar no estado Boot. Esta tela OBSERVA o UZeusCharServerSubsystem:
* enquanto não conecta, mostra o spinner e o botão desabilitado; ao conectar,
* libera o botão "Iniciar" (clique → Flow::RequestEnterLogin → Login).
* Sem hard-ref de asset de tema (§5): cores vêm do FUIStyle.
*/
UCLASS(Abstract, Blueprintable)
class ZMMO_API UUIBootScreen_Base : public UUIActivatableScreen_Base
{
GENERATED_BODY()
protected:
virtual void NativePreConstruct() override;
virtual void NativeOnActivated() override;
virtual void NativeOnDeactivated() override;
virtual void RefreshUIStyle_Implementation() override;
/** Texto exibido enquanto conecta / quando pronto / em erro. */
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Boot")
FText ConnectingText = FText::FromString(TEXT("Conectando ao Gateway"));
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Boot")
FText ReadyText = FText::FromString(TEXT("Pronto"));
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Boot")
FText FailedText = FText::FromString(TEXT("Falha ao conectar — tentando novamente"));
// ---- Widgets do WBP (nomes esperados) ----
UPROPERTY(meta = (BindWidgetOptional))
TObjectPtr<UUIButton_Base> Btn_Start;
UPROPERTY(meta = (BindWidgetOptional))
TObjectPtr<UUISpinner_Base> Spinner_Connect;
UPROPERTY(meta = (BindWidgetOptional))
TObjectPtr<UCommonTextBlock> Text_Status;
UPROPERTY(meta = (BindWidgetOptional))
TObjectPtr<UBorder> Border_Bg;
private:
void SetReady(bool bReady);
UZeusCharServerSubsystem* GetCharServer() const;
UFUNCTION()
void HandleStartClicked();
UFUNCTION()
void HandleCharConnected();
UFUNCTION()
void HandleCharConnectionFailed(FString Reason);
bool bCharBound = false;
};

View File

@@ -8,6 +8,7 @@
#include "ZMMOGameInstance.h"
#include "ZMMOThemeSubsystem.h"
#include "ZeusNetworkSubsystem.h"
#include "ZeusCharServerSubsystem.h"
#include "CommonActivatableWidget.h"
#include "Engine/GameInstance.h"
#include "Engine/LocalPlayer.h"
@@ -54,22 +55,15 @@ void UUIFrontEndFlowSubsystem::StartFrontEnd()
SetState(EZMMOFrontEndState::Boot);
// Substitui o auto-connect do GameInstance: o fluxo dirige a conexão.
if (UZeusNetworkSubsystem* Zeus = GetZeusNetwork())
// Pré-login fala com o ZeusCharServer (WebSocket), NÃO com o world server
// UDP. A tela Boot observa o CharServer e libera o botão ao conectar.
if (UZeusCharServerSubsystem* Char = GetCharServer())
{
FString Host = TEXT("127.0.0.1");
int32 Port = 27777;
if (const UZMMOGameInstance* GI = Cast<UZMMOGameInstance>(GetGameInstance()))
{
Host = GI->ZeusServerHost;
Port = GI->ZeusServerPort;
}
SetState(EZMMOFrontEndState::Connecting);
Zeus->ConnectToZeusServer(Host, Port);
Char->ConnectToDefaultCharServer(); // lê CharServerUrl de Project Settings
}
else
{
UE_LOG(LogZMMO, Warning, TEXT("FrontEndFlow: ZeusNetworkSubsystem indisponível; sem conexão."));
UE_LOG(LogZMMO, Warning, TEXT("FrontEndFlow: ZeusCharServerSubsystem indisponível; sem conexão de Boot."));
}
}
@@ -221,6 +215,12 @@ UZeusNetworkSubsystem* UUIFrontEndFlowSubsystem::GetZeusNetwork() const
return GI ? GI->GetSubsystem<UZeusNetworkSubsystem>() : nullptr;
}
UZeusCharServerSubsystem* UUIFrontEndFlowSubsystem::GetCharServer() const
{
const UGameInstance* GI = GetGameInstance();
return GI ? GI->GetSubsystem<UZeusCharServerSubsystem>() : nullptr;
}
UUIManagerSubsystem* UUIFrontEndFlowSubsystem::GetUIManager() const
{
if (const UGameInstance* GI = GetGameInstance())
@@ -257,14 +257,19 @@ void UUIFrontEndFlowSubsystem::BindNetwork()
{
return;
}
// CharServer (WebSocket) dirige o pré-login: Boot→Login.
if (UZeusCharServerSubsystem* Char = GetCharServer())
{
Char->OnConnected.AddDynamic(this, &UUIFrontEndFlowSubsystem::HandleConnected);
Char->OnConnectionFailed.AddDynamic(this, &UUIFrontEndFlowSubsystem::HandleConnectionFailed);
Char->OnDisconnected.AddDynamic(this, &UUIFrontEndFlowSubsystem::HandleCharDisconnected);
}
// UDP (world server) só interessa para o handoff de travel.
if (UZeusNetworkSubsystem* Zeus = GetZeusNetwork())
{
Zeus->OnConnected.AddDynamic(this, &UUIFrontEndFlowSubsystem::HandleConnected);
Zeus->OnConnectionFailed.AddDynamic(this, &UUIFrontEndFlowSubsystem::HandleConnectionFailed);
Zeus->OnDisconnected.AddDynamic(this, &UUIFrontEndFlowSubsystem::HandleDisconnected);
Zeus->OnServerTravelRequested.AddDynamic(this, &UUIFrontEndFlowSubsystem::HandleServerTravelRequested);
bNetBound = true;
}
bNetBound = true;
}
void UUIFrontEndFlowSubsystem::UnbindNetwork()
@@ -273,11 +278,14 @@ void UUIFrontEndFlowSubsystem::UnbindNetwork()
{
return;
}
if (UZeusCharServerSubsystem* Char = GetCharServer())
{
Char->OnConnected.RemoveDynamic(this, &UUIFrontEndFlowSubsystem::HandleConnected);
Char->OnConnectionFailed.RemoveDynamic(this, &UUIFrontEndFlowSubsystem::HandleConnectionFailed);
Char->OnDisconnected.RemoveDynamic(this, &UUIFrontEndFlowSubsystem::HandleCharDisconnected);
}
if (UZeusNetworkSubsystem* Zeus = GetZeusNetwork())
{
Zeus->OnConnected.RemoveDynamic(this, &UUIFrontEndFlowSubsystem::HandleConnected);
Zeus->OnConnectionFailed.RemoveDynamic(this, &UUIFrontEndFlowSubsystem::HandleConnectionFailed);
Zeus->OnDisconnected.RemoveDynamic(this, &UUIFrontEndFlowSubsystem::HandleDisconnected);
Zeus->OnServerTravelRequested.RemoveDynamic(this, &UUIFrontEndFlowSubsystem::HandleServerTravelRequested);
}
bNetBound = false;
@@ -285,20 +293,38 @@ void UUIFrontEndFlowSubsystem::UnbindNetwork()
void UUIFrontEndFlowSubsystem::HandleConnected()
{
UE_LOG(LogZMMO, Log, TEXT("FrontEndFlow: conectado → Login."));
SetState(EZMMOFrontEndState::Login);
// NÃO auto-avança: a tela Boot libera o botão "Iniciar"; o usuário clica
// (→ RequestEnterLogin). Mantém o estado Boot.
UE_LOG(LogZMMO, Log, TEXT("FrontEndFlow: CharServer conectado (estado %s)."),
*UEnum::GetValueAsString(CurrentState));
}
void UUIFrontEndFlowSubsystem::HandleConnectionFailed(FString Reason)
{
UE_LOG(LogZMMO, Warning, TEXT("FrontEndFlow: conexão falhou (%s) → volta a Login."), *Reason);
SetState(EZMMOFrontEndState::Login);
// Permanece em Boot; a tela mostra erro e o CharServer pode re-tentar.
UE_LOG(LogZMMO, Warning, TEXT("FrontEndFlow: conexão ao CharServer falhou (%s)."), *Reason);
}
void UUIFrontEndFlowSubsystem::HandleDisconnected()
void UUIFrontEndFlowSubsystem::HandleCharDisconnected(int32 StatusCode, FString Reason)
{
UE_LOG(LogZMMO, Log, TEXT("FrontEndFlow: desconectado."));
if (CurrentState != EZMMOFrontEndState::InWorld)
UE_LOG(LogZMMO, Log, TEXT("FrontEndFlow: CharServer desconectou (code=%d, %s)."),
StatusCode, *Reason);
// Se caiu antes de entrar no mundo, volta a Boot para reconectar.
if (CurrentState != EZMMOFrontEndState::InWorld &&
CurrentState != EZMMOFrontEndState::EnteringWorld &&
CurrentState != EZMMOFrontEndState::Boot)
{
SetState(EZMMOFrontEndState::Boot);
if (UZeusCharServerSubsystem* Char = GetCharServer())
{
Char->ConnectToDefaultCharServer();
}
}
}
void UUIFrontEndFlowSubsystem::RequestEnterLogin()
{
if (CurrentState == EZMMOFrontEndState::Boot)
{
SetState(EZMMOFrontEndState::Login);
}

View File

@@ -8,6 +8,7 @@
class UUIFrontEndScreenSet;
class UUIManagerSubsystem;
class UZeusNetworkSubsystem;
class UZeusCharServerSubsystem;
DECLARE_DYNAMIC_MULTICAST_DELEGATE_OneParam(FOnZMMOFrontEndStateChanged, EZMMOFrontEndState, NewState);
@@ -57,6 +58,10 @@ public:
UFUNCTION(BlueprintCallable, Category = "FrontEnd")
bool RequestBack();
/** Chamado pela tela Boot quando o usuário clica "Iniciar" (já conectado). */
UFUNCTION(BlueprintCallable, Category = "FrontEnd")
void RequestEnterLogin();
UPROPERTY(BlueprintAssignable, Category = "FrontEnd")
FOnZMMOFrontEndStateChanged OnStateChanged;
@@ -69,6 +74,7 @@ private:
void BindNetwork();
void UnbindNetwork();
UZeusNetworkSubsystem* GetZeusNetwork() const;
UZeusCharServerSubsystem* GetCharServer() const;
UUIManagerSubsystem* GetUIManager() const;
UUIFrontEndScreenSet* GetScreenSet();
@@ -82,7 +88,7 @@ private:
void HandleConnectionFailed(FString Reason);
UFUNCTION()
void HandleDisconnected();
void HandleCharDisconnected(int32 StatusCode, FString Reason);
UFUNCTION()
void HandleServerTravelRequested(const FString& MapName, const FString& MapPath);

View File

@@ -0,0 +1,104 @@
#include "UISpinner_Base.h"
#include "ZMMOThemeSubsystem.h"
#include "Engine/GameInstance.h"
#include "Engine/DataTable.h"
#include "Components/CircularThrobber.h"
#include "UI/UIStyleRow.h"
namespace
{
// Mesmo padrão de UUIPanel_Base: runtime usa o tema ativo; design-time
// carrega a row "Default" de DT_UI_Styles para o Designer ver igual.
const FUIStyle& ResolveSpinnerStyle(const UUserWidget* Widget, const FUIStyle& Fallback)
{
if (Widget)
{
if (const UGameInstance* GI = Widget->GetGameInstance())
{
if (const UZMMOThemeSubsystem* Theme = GI->GetSubsystem<UZMMOThemeSubsystem>())
{
return Theme->GetActiveUIStyle();
}
}
}
static FUIStyle DesignStyle;
static bool bDesignLoaded = false;
if (!bDesignLoaded)
{
if (const UDataTable* DT = LoadObject<UDataTable>(nullptr,
TEXT("/Game/ZMMO/Data/UI/DT_UI_Styles.DT_UI_Styles")))
{
if (const FUIStyleRow* Row = DT->FindRow<FUIStyleRow>(
FName(TEXT("Default")), TEXT("UISpinnerDesign"), false))
{
DesignStyle = Row->Style;
bDesignLoaded = true;
}
}
}
return bDesignLoaded ? DesignStyle : Fallback;
}
}
void UUISpinner_Base::RefreshUIStyle()
{
const FUIStyle Fallback;
const FUIStyle& AS = ResolveSpinnerStyle(this, Fallback);
const FUIStyleSpinner& S = AS.Spinner;
if (Throbber)
{
Throbber->SetNumberOfPieces(FMath::Max(1, S.NumberOfPieces));
Throbber->SetPeriod(FMath::Max(0.05f, S.Period));
Throbber->SetRadius(S.Radius);
}
// Cor/brush: o WBP aplica no throbber (mesma divisão de UUIPanel_Base).
BP_ApplySpinnerStyle(S);
}
void UUISpinner_Base::NativePreConstruct()
{
Super::NativePreConstruct();
RefreshUIStyle();
}
void UUISpinner_Base::NativeConstruct()
{
Super::NativeConstruct();
if (!bThemeBound)
{
if (const UGameInstance* GI = GetGameInstance())
{
if (UZMMOThemeSubsystem* Theme = GI->GetSubsystem<UZMMOThemeSubsystem>())
{
Theme->OnThemeChanged.AddDynamic(this, &UUISpinner_Base::HandleThemeChanged);
bThemeBound = true;
}
}
}
RefreshUIStyle();
}
void UUISpinner_Base::NativeDestruct()
{
if (bThemeBound)
{
if (const UGameInstance* GI = GetGameInstance())
{
if (UZMMOThemeSubsystem* Theme = GI->GetSubsystem<UZMMOThemeSubsystem>())
{
Theme->OnThemeChanged.RemoveDynamic(this, &UUISpinner_Base::HandleThemeChanged);
}
}
bThemeBound = false;
}
Super::NativeDestruct();
}
void UUISpinner_Base::HandleThemeChanged(FName /*NewThemeId*/)
{
RefreshUIStyle();
}

View File

@@ -0,0 +1,49 @@
#pragma once
#include "CoreMinimal.h"
#include "CommonUserWidget.h"
#include "UI/UIStyleTokens.h"
#include "UISpinner_Base.generated.h"
class UCircularThrobber;
/**
* Spinner/throbber indeterminado do ZMMO — mesmo padrão de UUIPanel_Base.
* Fundação CommonUI (UCommonUserWidget). Camada Abstract; o WBP concreto
* (UI_Spinner_Master) herda DIRETO desta classe C++ (UMG não encadeia
* árvores — ARQUITETURA.md §3.3).
*
* Visual data-driven por FUIStyle.Spinner (UZMMOThemeSubsystem). C++ aplica
* NumberOfPieces/Period/Radius no UCircularThrobber; cor/brush vão pelo hook
* BP_ApplySpinnerStyle (o WBP tinge a imagem do throbber).
*/
UCLASS(Abstract, Blueprintable)
class ZMMO_API UUISpinner_Base : public UCommonUserWidget
{
GENERATED_BODY()
public:
/** Re-resolve os tokens do tema ativo e reaplica no throbber. */
UFUNCTION(BlueprintCallable, Category = "UI Style")
void RefreshUIStyle();
protected:
virtual void NativePreConstruct() override;
virtual void NativeConstruct() override;
virtual void NativeDestruct() override;
/** Hook opcional: o WBP aplica cor/brush do spinner (FUIStyleSpinner). */
UFUNCTION(BlueprintImplementableEvent, Category = "UI Style",
meta = (DisplayName = "Apply Spinner Style"))
void BP_ApplySpinnerStyle(const FUIStyleSpinner& Spinner);
/** Throbber circular (nome esperado no WBP: "Throbber"). */
UPROPERTY(meta = (BindWidgetOptional))
TObjectPtr<UCircularThrobber> Throbber;
private:
UFUNCTION()
void HandleThemeChanged(FName NewThemeId);
bool bThemeBound = false;
};