From 079d05c1175361af55a8e8ce24d58b1b49a2a713 Mon Sep 17 00:00:00 2001 From: Mateus Rodrigues Date: Tue, 26 May 2026 02:24:48 -0300 Subject: [PATCH] feat(ui/loading): tela de loading dinamica data-driven (etapas + dicas) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - UUILoadingScreen_Base (Game/UI/Common/) generica: Configure(Context), MarkStepRunning/Done/Failed, OnLoadingComplete delegate, progress bar reativa e rotacao automatica de dicas via timer. - Etapas configuradas por contexto em UZMMOLoadingProfilesDataAsset (DA_LoadingProfiles). Perfil inicial FrontEndEnteringWorld: Travel -> MapLoaded -> Spawn. - Dicas em DT_LoadingTips (FText localizavel + filtro por contexto + GameplayTag de categoria). 4 dicas seed. - UIFrontEndFlowSubsystem agora bloqueia a transicao InWorld ate o HandlePlayerSpawned (bIsLocal=true) — usuario nao ve mundo vazio. - WBP_Loading em /UI/Shared/Loading/ (neutro, reusavel pelo InGame futuro); DA_FrontEndScreenSet[EnteringWorld] aponta pra ele. - DefaultGame.ini wire LoadingProfilesAsset + LoadingTipsAsset. Co-Authored-By: Claude Opus 4.7 (1M context) --- Config/DefaultGame.ini | 6 + .../Data/UI/Loading/DA_LoadingProfiles.uasset | Bin 0 -> 2176 bytes .../Data/UI/Loading/DT_LoadingTips.uasset | Bin 0 -> 2765 bytes .../UI/FrontEnd/DA_FrontEndScreenSet.uasset | Bin 2192 -> 2345 bytes .../ZMMO/UI/Shared/Loading/WBP_Loading.uasset | Bin 0 -> 41173 bytes Source/ZMMO/Data/UI/LoadingTypes.h | 66 +++++ .../UI/Common/UILoadingProfilesDataAsset.cpp | 10 + .../UI/Common/UILoadingProfilesDataAsset.h | 29 +++ .../Game/UI/Common/UILoadingScreen_Base.cpp | 235 ++++++++++++++++++ .../Game/UI/Common/UILoadingScreen_Base.h | 124 +++++++++ Source/ZMMO/Game/UI/Common/UILoadingTipRow.h | 30 +++ .../UI/FrontEnd/UIFrontEndFlowSubsystem.cpp | 82 +++++- .../UI/FrontEnd/UIFrontEndFlowSubsystem.h | 50 ++++ 13 files changed, 627 insertions(+), 5 deletions(-) create mode 100644 Content/ZMMO/Data/UI/Loading/DA_LoadingProfiles.uasset create mode 100644 Content/ZMMO/Data/UI/Loading/DT_LoadingTips.uasset create mode 100644 Content/ZMMO/UI/Shared/Loading/WBP_Loading.uasset create mode 100644 Source/ZMMO/Data/UI/LoadingTypes.h create mode 100644 Source/ZMMO/Game/UI/Common/UILoadingProfilesDataAsset.cpp create mode 100644 Source/ZMMO/Game/UI/Common/UILoadingProfilesDataAsset.h create mode 100644 Source/ZMMO/Game/UI/Common/UILoadingScreen_Base.cpp create mode 100644 Source/ZMMO/Game/UI/Common/UILoadingScreen_Base.h create mode 100644 Source/ZMMO/Game/UI/Common/UILoadingTipRow.h diff --git a/Config/DefaultGame.ini b/Config/DefaultGame.ini index 11749f3..cd2c91a 100644 --- a/Config/DefaultGame.ini +++ b/Config/DefaultGame.ini @@ -35,6 +35,12 @@ ScreenSetAsset=/Game/ZMMO/UI/FrontEnd/DA_FrontEndScreenSet.DA_FrontEndScreenSet ; cliente resolve `mapId -> ClientLevel/spawns` via FZMMOMapDef rows. MapsTableAsset=/Game/ZMMO/Data/World/DT_Maps.DT_Maps +; Loading dinâmico (etapas + dicas). DA_LoadingProfiles tem 1 perfil por +; contexto (EZMMOLoadingContext); DT_LoadingTips rotaciona durante o load. +; Cliente local marca etapas via eventos (sem opcode novo). +LoadingProfilesAsset=/Game/ZMMO/Data/UI/Loading/DA_LoadingProfiles.DA_LoadingProfiles +LoadingTipsAsset=/Game/ZMMO/Data/UI/Loading/DT_LoadingTips.DT_LoadingTips + ; ----------------------------------------------------------------------------- ; UI in-game (PR 19+). Espelho do front-end pattern: ; - ScreenSetAsset mapeia EZMMOInGameUIState -> WBP (Playing -> WBP_HUD, diff --git a/Content/ZMMO/Data/UI/Loading/DA_LoadingProfiles.uasset b/Content/ZMMO/Data/UI/Loading/DA_LoadingProfiles.uasset new file mode 100644 index 0000000000000000000000000000000000000000..49a3c2a21ba8336f8e2f0d81a7ece74ec0295cea GIT binary patch literal 2176 zcmb7GU1%It6h3KEQyZHmv8@_ojnNuS%v82oKRo==s>*UcgJ ziTgHyiL3FhA#NIuoOS5T?vBl!m1VuXDT}p|MkdL+7GL9orj^am()?F595(ffeKJc& zMxLo(%hZ6#!?euMYbjZERhi?55IKJ65aJr-3h|jw{J7?R+{d74NLs3XXWb1<42$#u zF(v!Ww8-j@2}d^#9J2EwEvd;V9=#Gus)#Nect}Gmqj@o7P}iGt57dHkWWFy$8}@9v z*A+5p(~I*BudH{4)^ytOg9y_Lzaz3S&W6uC*Xzt9<72v$l|uz-S$am&k%evbFh6FA z0>AlO)t0uanG9sri;MP8J@w>eBE*!Clhjl)cK!cmIWcj55m%_{B5qo8d}!Z(nRe)x zzApDQ<5^+b7X%hhD#N&SD+6GwQHm ztf)<|DeKa;-ufq(`}N))1*4$!niV6k2KCS`0TEs25eTXkGk}41S`MncA-~*K@Jx$m zE67uO*!a(kEtCB=F7*~KKG<1Ct%F7CmT@uKzupcEq1OhvQ3|qL0m*=cK%Ho=w0*_4 z+ugzHeGb?p1D*gitdJ?CfW0?_-CBX3LNKEZ9utwHq5ZURun(^w|m-<$D^_)9Q@Q9WvA^|bB?BV)D}bRJbXP18tK z>atESIDkq{S|RYAAe^|cEIH-+$cemIkq1mMppu6+2z(Dn70-0bwQrcMxoLixpbvc1 wfM6!vw7DiAQSYl(@D};M#IrxWbWpqd+=o!ahct-7=Q~Df&M({^_L$KB3#rkj*8l(j literal 0 HcmV?d00001 diff --git a/Content/ZMMO/Data/UI/Loading/DT_LoadingTips.uasset b/Content/ZMMO/Data/UI/Loading/DT_LoadingTips.uasset new file mode 100644 index 0000000000000000000000000000000000000000..7d7ea6f9471662cd93e74b98a86f3b59a4db0cdd GIT binary patch literal 2765 zcmc&$Yiv|i5FThL%o1lSIYZR#SoxNwdZI_gPPIAv< zX3or@k4??dH*Kp?oe z+V88`Tv6?<^ZA376*UBXC?m9iJfbB$pTWRnF1;}iGEeTGWMspWRn%}-rXA2<0;c!tLbWF@4+Y4s7Zf^%C;BUWI#HV zP_7BJ_;k zXRqY%!YgRhk}6H*oL;q>H)-fHI@RYsoLtZBCM9}EiKz`*R7u32Qp~uPge@bbL{(aA z_V&Z0r6llayIg(>h_z_lt)|||*?4^@-+|m`I8BP#E_5r4OV`79S~26>L&vWIJWiKT z+uD8J7;y^rgz~k}J#G4;{43A+g)u48%36VW`rK7{= zwnAbyJYt6Jc?4&G5VVoM1skEzONhtYB~; ziwZEEU9PShg^BxQ`waldk3KH)m1$v}x=KjCG=_fSn%`8=WhG|3B+Jz7+~ZFI=(hAE zMszm%mm!lGo+rc&(aAvT(vv8{J8f%gDW+W*WnR&=B?-jjqZEEwGEZUdYqY;ggmQ;v z?sjX&&#$Brxe#XUy$imrVyFMVkIC0BA2dM0uQ~{KlptY`%t{jac%C4%evmyUC>Icn w;FDC0g{<3okV}Z?&SU(?bnvgYj<~jre}j-@vyxW3eynWH{p#~#kHvugf0+puMF0Q* literal 0 HcmV?d00001 diff --git a/Content/ZMMO/UI/FrontEnd/DA_FrontEndScreenSet.uasset b/Content/ZMMO/UI/FrontEnd/DA_FrontEndScreenSet.uasset index 5cdc89e190fabb4560fa922b86e74776060041a6..99508a4e775af5115265bc27bf1bb26f59e9d6a1 100644 GIT binary patch delta 753 zcmbOrxKe0>gqT*$iMyNEHtuP3D`sA`a8LUFB_+3wI40^P@hUPfFf3(cU|<8%((qunoabefIlGGxgn(+LhoD_z}hBK*BAo0nH%-_WLfaW4;jdx~< zJK`z3S)4_T(N+TH93TzyCJ2Z^C@=}~G6;x6C@{$aWlK&@WE0ktLXu&Hvc-_ZKunOy zf?%|q*&pbTM{0FZ`- z0fPuskRM8e0t{vsh|LKV2L%Dl@gO0fU2uSGunS1k6-0mnlMBQE0+4QAFbk3jK~V|< X%s|WoWY5S`7(ZK5XT!^9+NM8S_j1hG&9qK%DUWhWNmkFZckFqL+9Or@ZLaEK`^ zOd*KGI<>#R)>aD(G0wZ}$_?ziH?!|%X6Np6Zq_XoFS^Ma_kFK^chqWhwr^iD@$RFq zy~V>+m56*r6c?QFt`=(N!X&1&LlhD0;6|)2u|;SQK~VhP!ooy4FargJd`twps)^)3Q1_p1cQQ4hU3x3xTaIQ zsbl=+%&Rv4ILi|0;B4uY%!91WGiuDuJMl96`m}-8leS34kgQDz+8t2NBvS^lFxwy8 s{uGqA4}w%W|6925Dza;EXpsaF!`FBs3{{5$MidnsFdX_ceOV diff --git a/Content/ZMMO/UI/Shared/Loading/WBP_Loading.uasset b/Content/ZMMO/UI/Shared/Loading/WBP_Loading.uasset new file mode 100644 index 0000000000000000000000000000000000000000..e25bc98bf3009d456b176de26961f097f7e40b59 GIT binary patch literal 41173 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 Steps; + + /** Título da tela (vazio = usa default da classe). */ + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Loading") + FText TitleOverride; +}; diff --git a/Source/ZMMO/Game/UI/Common/UILoadingProfilesDataAsset.cpp b/Source/ZMMO/Game/UI/Common/UILoadingProfilesDataAsset.cpp new file mode 100644 index 0000000..87bf61a --- /dev/null +++ b/Source/ZMMO/Game/UI/Common/UILoadingProfilesDataAsset.cpp @@ -0,0 +1,10 @@ +#include "UILoadingProfilesDataAsset.h" + +FZMMOLoadingProfile UZMMOLoadingProfilesDataAsset::GetProfile(EZMMOLoadingContext Context) const +{ + if (const FZMMOLoadingProfile* Found = Profiles.Find(Context)) + { + return *Found; + } + return FZMMOLoadingProfile(); +} diff --git a/Source/ZMMO/Game/UI/Common/UILoadingProfilesDataAsset.h b/Source/ZMMO/Game/UI/Common/UILoadingProfilesDataAsset.h new file mode 100644 index 0000000..f3d83a2 --- /dev/null +++ b/Source/ZMMO/Game/UI/Common/UILoadingProfilesDataAsset.h @@ -0,0 +1,29 @@ +#pragma once + +#include "CoreMinimal.h" +#include "Engine/DataAsset.h" +#include "UI/LoadingTypes.h" +#include "UILoadingProfilesDataAsset.generated.h" + +/** + * Mapa data-driven contexto → perfil (etapas + título). Designer edita o + * asset; código consulta por EZMMOLoadingContext na hora de configurar a + * tela. Asset canônico: DA_LoadingProfiles em /Game/ZMMO/Data/UI/Loading/. + * + * Configurado em DefaultGame.ini: + * [/Script/ZMMO.UIFrontEndFlowSubsystem] + * LoadingProfilesAsset=/Game/ZMMO/Data/UI/Loading/DA_LoadingProfiles.DA_LoadingProfiles + */ +UCLASS(BlueprintType) +class ZMMO_API UZMMOLoadingProfilesDataAsset : public UDataAsset +{ + GENERATED_BODY() + +public: + UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "Loading") + TMap Profiles; + + /** Retorna o perfil do contexto ou um perfil vazio (sem etapas). */ + UFUNCTION(BlueprintCallable, Category = "Loading") + FZMMOLoadingProfile GetProfile(EZMMOLoadingContext Context) const; +}; diff --git a/Source/ZMMO/Game/UI/Common/UILoadingScreen_Base.cpp b/Source/ZMMO/Game/UI/Common/UILoadingScreen_Base.cpp new file mode 100644 index 0000000..cf3eb46 --- /dev/null +++ b/Source/ZMMO/Game/UI/Common/UILoadingScreen_Base.cpp @@ -0,0 +1,235 @@ +#include "UILoadingScreen_Base.h" + +#include "Components/ProgressBar.h" +#include "CommonTextBlock.h" +#include "Engine/DataTable.h" +#include "Engine/World.h" +#include "TimerManager.h" +#include "UILoadingProfilesDataAsset.h" +#include "UILoadingTipRow.h" + +void UUILoadingScreen_Base::Configure(EZMMOLoadingContext InContext, + UZMMOLoadingProfilesDataAsset* Profiles, + UDataTable* TipsTable) +{ + Context_ = InContext; + bCompleteFired_ = false; + + // Perfil (etapas + título). + Profile_ = (Profiles != nullptr) + ? Profiles->GetProfile(InContext) + : FZMMOLoadingProfile(); + + StepStatus_.Reset(); + for (const FZMMOLoadingStepDef& Step : Profile_.Steps) + { + StepStatus_.Add(Step.StepId, EZMMOLoadingStepStatus::Pending); + } + + // Pool de dicas filtrada por contexto (None = qualquer). + TipPool_.Reset(); + if (TipsTable != nullptr) + { + TipsTable->ForeachRow(TEXT("UUILoadingScreen_Base::Configure"), + [this](const FName& /*Key*/, const FZMMOLoadingTipRow& Row) + { + if (Row.Context == EZMMOLoadingContext::None || Row.Context == Context_) + { + if (!Row.Tip.IsEmpty()) + { + TipPool_.Add(Row.Tip); + } + } + }); + } + NextTipIndex_ = 0; + + RefreshUI(); + + if (IsActivated()) + { + StartTipTimer(); + } +} + +void UUILoadingScreen_Base::NativeOnActivated() +{ + Super::NativeOnActivated(); + RefreshUI(); + StartTipTimer(); +} + +void UUILoadingScreen_Base::NativeOnDeactivated() +{ + StopTipTimer(); + Super::NativeOnDeactivated(); +} + +void UUILoadingScreen_Base::MarkStepRunning(FName StepId) +{ + if (EZMMOLoadingStepStatus* Status = StepStatus_.Find(StepId)) + { + // Já Done não regride. + if (*Status != EZMMOLoadingStepStatus::Done && *Status != EZMMOLoadingStepStatus::Failed) + { + *Status = EZMMOLoadingStepStatus::Running; + RefreshUI(); + } + } +} + +void UUILoadingScreen_Base::MarkStepDone(FName StepId) +{ + if (EZMMOLoadingStepStatus* Status = StepStatus_.Find(StepId)) + { + *Status = EZMMOLoadingStepStatus::Done; + RefreshUI(); + + if (!bCompleteFired_ && AreAllStepsDone()) + { + bCompleteFired_ = true; + OnLoadingComplete.Broadcast(); + } + } +} + +void UUILoadingScreen_Base::MarkStepFailed(FName StepId, const FText& Reason) +{ + if (EZMMOLoadingStepStatus* Status = StepStatus_.Find(StepId)) + { + *Status = EZMMOLoadingStepStatus::Failed; + if (Text_Status && !Reason.IsEmpty()) + { + Text_Status->SetText(Reason); + } + RefreshUI(); + } +} + +bool UUILoadingScreen_Base::AreAllStepsDone() const +{ + if (Profile_.Steps.Num() == 0) + { + return false; + } + for (const FZMMOLoadingStepDef& Step : Profile_.Steps) + { + const EZMMOLoadingStepStatus* Status = StepStatus_.Find(Step.StepId); + if (Status == nullptr || *Status != EZMMOLoadingStepStatus::Done) + { + return false; + } + } + return true; +} + +int32 UUILoadingScreen_Base::FindStepIndex(FName StepId) const +{ + for (int32 i = 0; i < Profile_.Steps.Num(); ++i) + { + if (Profile_.Steps[i].StepId == StepId) + { + return i; + } + } + return INDEX_NONE; +} + +EZMMOLoadingStepStatus UUILoadingScreen_Base::GetStepStatus(int32 Index) const +{ + if (!Profile_.Steps.IsValidIndex(Index)) return EZMMOLoadingStepStatus::Pending; + const EZMMOLoadingStepStatus* Status = StepStatus_.Find(Profile_.Steps[Index].StepId); + return Status ? *Status : EZMMOLoadingStepStatus::Pending; +} + +FText UUILoadingScreen_Base::ComputeCurrentStatusText() const +{ + // Prioridade: Running > último Done > primeiro Pending > Status default. + int32 LastDone = INDEX_NONE; + for (int32 i = 0; i < Profile_.Steps.Num(); ++i) + { + const EZMMOLoadingStepStatus St = GetStepStatus(i); + if (St == EZMMOLoadingStepStatus::Running) + { + return Profile_.Steps[i].Label; + } + if (St == EZMMOLoadingStepStatus::Done) + { + LastDone = i; + } + } + if (LastDone != INDEX_NONE) + { + return Profile_.Steps[LastDone].Label; + } + if (Profile_.Steps.Num() > 0) + { + return Profile_.Steps[0].Label; + } + return StatusDefault; +} + +float UUILoadingScreen_Base::ComputeProgress01() const +{ + const int32 N = Profile_.Steps.Num(); + if (N == 0) return 0.f; + float Acc = 0.f; + for (int32 i = 0; i < N; ++i) + { + const EZMMOLoadingStepStatus St = GetStepStatus(i); + if (St == EZMMOLoadingStepStatus::Done) Acc += 1.f; + else if (St == EZMMOLoadingStepStatus::Running) Acc += 0.5f; + } + return FMath::Clamp(Acc / static_cast(N), 0.f, 1.f); +} + +void UUILoadingScreen_Base::RefreshUI() +{ + if (Text_Title) + { + const FText& Title = Profile_.TitleOverride.IsEmpty() ? TitleDefault : Profile_.TitleOverride; + Text_Title->SetText(Title); + } + if (Text_Status) + { + Text_Status->SetText(ComputeCurrentStatusText()); + } + if (ProgressBar_Progress) + { + ProgressBar_Progress->SetPercent(ComputeProgress01()); + } + if (Text_Tip && TipPool_.Num() > 0 && Text_Tip->GetText().IsEmpty()) + { + // Primeira dica na inicialização (timer cuida das próximas). + Text_Tip->SetText(TipPool_[0]); + NextTipIndex_ = TipPool_.Num() > 1 ? 1 : 0; + } +} + +void UUILoadingScreen_Base::RotateTip() +{ + if (!Text_Tip || TipPool_.Num() == 0) return; + Text_Tip->SetText(TipPool_[NextTipIndex_]); + NextTipIndex_ = (NextTipIndex_ + 1) % TipPool_.Num(); +} + +void UUILoadingScreen_Base::StartTipTimer() +{ + StopTipTimer(); + if (TipRotationIntervalSeconds <= 0.f || TipPool_.Num() <= 1) return; + if (UWorld* W = GetWorld()) + { + W->GetTimerManager().SetTimer(TipTimerHandle_, this, + &UUILoadingScreen_Base::RotateTip, + TipRotationIntervalSeconds, /*bLoop=*/true); + } +} + +void UUILoadingScreen_Base::StopTipTimer() +{ + if (UWorld* W = GetWorld()) + { + W->GetTimerManager().ClearTimer(TipTimerHandle_); + } + TipTimerHandle_.Invalidate(); +} diff --git a/Source/ZMMO/Game/UI/Common/UILoadingScreen_Base.h b/Source/ZMMO/Game/UI/Common/UILoadingScreen_Base.h new file mode 100644 index 0000000..401afd4 --- /dev/null +++ b/Source/ZMMO/Game/UI/Common/UILoadingScreen_Base.h @@ -0,0 +1,124 @@ +#pragma once + +#include "CoreMinimal.h" +#include "UIActivatableScreen_Base.h" +#include "UI/LoadingTypes.h" +#include "UILoadingScreen_Base.generated.h" + +class UCommonTextBlock; +class UProgressBar; +class UDataTable; +class UZMMOLoadingProfilesDataAsset; + +DECLARE_DYNAMIC_MULTICAST_DELEGATE(FZMMOOnLoadingComplete); + +/** + * Tela de loading genérica. Reusada pelo FrontEnd (handoff CharServer → + * WorldServer) e pelo InGame (entrada em instance/dungeon, respawn). Cada + * uso passa um EZMMOLoadingContext em Configure(), que resolve o perfil + * no UZMMOLoadingProfilesDataAsset (lista de etapas + título). + * + * Quem orquestra (UIFrontEndFlowSubsystem, futuro UIInGameFlowSubsystem) + * assina os próprios eventos (HandlePostLoadMap, OnPlayerSpawned, etc) e + * chama MarkStepDone(StepId) — server NÃO envia opcode de progresso (ver + * [[project_ui_loading_dynamic]]). + * + * Dismiss: a tela emite OnLoadingComplete quando todas as etapas estão + * Done. Caller decide quando popar (geralmente após pequeno fade-out). + * + * WBP concreto herda DIRETO desta classe. Subclasses C++ específicas só + * surgem quando houver necessidade real de estilo divergente. + */ +UCLASS(Abstract, Blueprintable) +class ZMMO_API UUILoadingScreen_Base : public UUIActivatableScreen_Base +{ + GENERATED_BODY() + +public: + /** + * Configura a tela: carrega o perfil do contexto, popula a pool de + * dicas filtrada e reseta estado das etapas. Pode ser chamado depois + * de NativeOnActivated (estado é reaplicado em RefreshUI). + */ + UFUNCTION(BlueprintCallable, Category = "Loading") + void Configure(EZMMOLoadingContext InContext, + UZMMOLoadingProfilesDataAsset* Profiles, + UDataTable* TipsTable); + + UFUNCTION(BlueprintCallable, Category = "Loading") + void MarkStepRunning(FName StepId); + + UFUNCTION(BlueprintCallable, Category = "Loading") + void MarkStepDone(FName StepId); + + UFUNCTION(BlueprintCallable, Category = "Loading") + void MarkStepFailed(FName StepId, const FText& Reason); + + UFUNCTION(BlueprintPure, Category = "Loading") + bool AreAllStepsDone() const; + + /** Disparado UMA vez quando todas as etapas viram Done. */ + UPROPERTY(BlueprintAssignable, Category = "Loading") + FZMMOOnLoadingComplete OnLoadingComplete; + +protected: + virtual void NativeOnActivated() override; + virtual void NativeOnDeactivated() override; + + /** Reaplica todo o estado aos widgets bindados (texto, progress, tip). */ + void RefreshUI(); + void RotateTip(); + void StartTipTimer(); + void StopTipTimer(); + + /** Acha índice da etapa por StepId; -1 se não existir. */ + int32 FindStepIndex(FName StepId) const; + + /** Status atual da etapa em [Steps_]; Pending se não rastreada. */ + EZMMOLoadingStepStatus GetStepStatus(int32 Index) const; + + /** Texto exibido em Text_Status — etapa Running atual ou última Done. */ + FText ComputeCurrentStatusText() const; + + /** Progresso 0..1: #Done / max(1,#Steps). Running conta como meio passo. */ + float ComputeProgress01() const; + + // ---- Bind widgets (BindWidgetOptional — WBP pode omitir qualquer) ---- + UPROPERTY(meta = (BindWidgetOptional)) + TObjectPtr Text_Title; + + UPROPERTY(meta = (BindWidgetOptional)) + TObjectPtr Text_Status; + + UPROPERTY(meta = (BindWidgetOptional)) + TObjectPtr ProgressBar_Progress; + + UPROPERTY(meta = (BindWidgetOptional)) + TObjectPtr Text_Tip; + + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Loading") + FText TitleDefault = FText::FromString(TEXT("Carregando")); + + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Loading") + FText StatusDefault = FText::FromString(TEXT("Entrando no mundo...")); + + /** Intervalo entre dicas. 0 = sem rotação (mostra a primeira dica fixa). */ + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Loading") + float TipRotationIntervalSeconds = 6.f; + +private: + EZMMOLoadingContext Context_ = EZMMOLoadingContext::None; + + UPROPERTY(Transient) + FZMMOLoadingProfile Profile_; + + UPROPERTY(Transient) + TMap StepStatus_; + + UPROPERTY(Transient) + TArray TipPool_; + + int32 NextTipIndex_ = 0; + bool bCompleteFired_ = false; + FTimerHandle TipTimerHandle_; +}; diff --git a/Source/ZMMO/Game/UI/Common/UILoadingTipRow.h b/Source/ZMMO/Game/UI/Common/UILoadingTipRow.h new file mode 100644 index 0000000..2889894 --- /dev/null +++ b/Source/ZMMO/Game/UI/Common/UILoadingTipRow.h @@ -0,0 +1,30 @@ +#pragma once + +#include "CoreMinimal.h" +#include "Engine/DataTable.h" +#include "GameplayTagContainer.h" +#include "UI/LoadingTypes.h" +#include "UILoadingTipRow.generated.h" + +/** + * Row do DT_LoadingTips. Cada row = uma dica mostrada (rotaciona durante o + * loading). FText é localizável (basta extrair pelo gather de tradução do + * UE). Filtra por contexto: Context=None significa "qualquer loading". + */ +USTRUCT(BlueprintType) +struct FZMMOLoadingTipRow : public FTableRowBase +{ + GENERATED_BODY() + + /** Texto da dica (localizável). */ + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Loading") + FText Tip; + + /** Restringe a dica a um contexto. None = todos. */ + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Loading") + EZMMOLoadingContext Context = EZMMOLoadingContext::None; + + /** Tag opcional pra categorizar (PvE/PvP/Crafting...) — futuro filtro. */ + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Loading") + FGameplayTag Category; +}; diff --git a/Source/ZMMO/Game/UI/FrontEnd/UIFrontEndFlowSubsystem.cpp b/Source/ZMMO/Game/UI/FrontEnd/UIFrontEndFlowSubsystem.cpp index cd2393e..31ba12b 100644 --- a/Source/ZMMO/Game/UI/FrontEnd/UIFrontEndFlowSubsystem.cpp +++ b/Source/ZMMO/Game/UI/FrontEnd/UIFrontEndFlowSubsystem.cpp @@ -5,6 +5,8 @@ #include "UIFrontEndScreenSet.h" #include "UIManagerSubsystem.h" #include "UIPrimaryGameLayout_Base.h" +#include "UI/Common/UILoadingProfilesDataAsset.h" +#include "UI/Common/UILoadingScreen_Base.h" #include "UI/UILayerTags.h" #include "ZMMOGameInstance.h" #include "ZMMOThemeSubsystem.h" @@ -212,13 +214,27 @@ void UUIFrontEndFlowSubsystem::ResolveAndPushScreen(EZMMOFrontEndState State) const FSoftObjectPath Path = Soft.ToSoftObjectPath(); UAssetManager::GetStreamableManager().RequestAsyncLoad( Path, - FStreamableDelegate::CreateWeakLambda(this, [this, Soft, Layer]() + FStreamableDelegate::CreateWeakLambda(this, [this, Soft, Layer, State]() { if (UUIManagerSubsystem* M = GetUIManager()) { if (UClass* Cls = Soft.Get()) { - M->PushScreenToLayer(Layer, Cls); + UCommonActivatableWidget* Pushed = M->PushScreenToLayer(Layer, Cls); + + // Loading: configura logo após o push e marca etapa "Travel" + // (chegamos aqui via HandleServerTravelRequested → SetState). + if (State == EZMMOFrontEndState::EnteringWorld) + { + if (UUILoadingScreen_Base* Loading = Cast(Pushed)) + { + ActiveLoadingScreen = Loading; + Loading->Configure(EZMMOLoadingContext::FrontEndEnteringWorld, + GetLoadingProfiles(), GetLoadingTips()); + Loading->OnLoadingComplete.AddDynamic(this, &UUIFrontEndFlowSubsystem::HandleLoadingComplete); + Loading->MarkStepDone(TEXT("Travel")); + } + } } } })); @@ -283,6 +299,7 @@ void UUIFrontEndFlowSubsystem::BindNetwork() if (UZeusNetworkSubsystem* Zeus = GetZeusNetwork()) { Zeus->OnServerTravelRequested.AddDynamic(this, &UUIFrontEndFlowSubsystem::HandleServerTravelRequested); + Zeus->OnPlayerSpawned.AddDynamic(this, &UUIFrontEndFlowSubsystem::HandlePlayerSpawned); } bNetBound = true; } @@ -302,6 +319,7 @@ void UUIFrontEndFlowSubsystem::UnbindNetwork() if (UZeusNetworkSubsystem* Zeus = GetZeusNetwork()) { Zeus->OnServerTravelRequested.RemoveDynamic(this, &UUIFrontEndFlowSubsystem::HandleServerTravelRequested); + Zeus->OnPlayerSpawned.RemoveDynamic(this, &UUIFrontEndFlowSubsystem::HandlePlayerSpawned); } bNetBound = false; } @@ -390,11 +408,65 @@ void UUIFrontEndFlowSubsystem::HandleServerTravelRequested(const FString& MapNam void UUIFrontEndFlowSubsystem::HandlePostLoadMap(UWorld* /*LoadedWorld*/) { - if (bTravelingToWorld) + if (!bTravelingToWorld) { - bTravelingToWorld = false; - SetState(EZMMOFrontEndState::InWorld); + return; } + + // Loading dinâmico: marca a etapa "MapLoaded" e espera o spawn antes de + // liberar o InWorld (a tela limpa via OnLoadingComplete). Fallback: se + // não há screen ativa (DA não configurado), comportamento legado — + // transita já pra InWorld pra não travar. + if (UUILoadingScreen_Base* Loading = ActiveLoadingScreen.Get()) + { + Loading->MarkStepDone(TEXT("MapLoaded")); + return; + } + + bTravelingToWorld = false; + SetState(EZMMOFrontEndState::InWorld); +} + +void UUIFrontEndFlowSubsystem::HandlePlayerSpawned(int32 /*EntityId*/, bool bIsLocal, + FVector /*PosCm*/, float /*YawDeg*/, + int64 /*ServerTimeMs*/) +{ + if (!bIsLocal) return; + if (UUILoadingScreen_Base* Loading = ActiveLoadingScreen.Get()) + { + Loading->MarkStepDone(TEXT("Spawn")); + } +} + +void UUIFrontEndFlowSubsystem::HandleLoadingComplete() +{ + if (UUILoadingScreen_Base* Loading = ActiveLoadingScreen.Get()) + { + Loading->OnLoadingComplete.RemoveDynamic(this, &UUIFrontEndFlowSubsystem::HandleLoadingComplete); + } + ActiveLoadingScreen.Reset(); + bTravelingToWorld = false; + SetState(EZMMOFrontEndState::InWorld); +} + +UZMMOLoadingProfilesDataAsset* UUIFrontEndFlowSubsystem::GetLoadingProfiles() +{ + if (LoadingProfiles) return LoadingProfiles; + if (!LoadingProfilesAsset.IsNull()) + { + LoadingProfiles = LoadingProfilesAsset.LoadSynchronous(); + } + return LoadingProfiles; +} + +UDataTable* UUIFrontEndFlowSubsystem::GetLoadingTips() +{ + if (LoadingTips) return LoadingTips; + if (!LoadingTipsAsset.IsNull()) + { + LoadingTips = LoadingTipsAsset.LoadSynchronous(); + } + return LoadingTips; } void UUIFrontEndFlowSubsystem::HandleServerHelloTheme(FName ThemeId) diff --git a/Source/ZMMO/Game/UI/FrontEnd/UIFrontEndFlowSubsystem.h b/Source/ZMMO/Game/UI/FrontEnd/UIFrontEndFlowSubsystem.h index 579939b..d1168de 100644 --- a/Source/ZMMO/Game/UI/FrontEnd/UIFrontEndFlowSubsystem.h +++ b/Source/ZMMO/Game/UI/FrontEnd/UIFrontEndFlowSubsystem.h @@ -8,6 +8,8 @@ class UUIFrontEndScreenSet; class UUIManagerSubsystem; +class UUILoadingScreen_Base; +class UZMMOLoadingProfilesDataAsset; class UZeusNetworkSubsystem; class UZeusCharServerSubsystem; struct FZMMOMapDef; @@ -147,6 +149,24 @@ protected: UPROPERTY(Config, EditDefaultsOnly, Category = "FrontEnd|Maps") TSoftObjectPtr MapsTableAsset; + /** + * DataAsset com os perfis de loading (etapas por contexto). Configure em + * DefaultGame.ini: + * [/Script/ZMMO.UIFrontEndFlowSubsystem] + * LoadingProfilesAsset=/Game/ZMMO/Data/UI/Loading/DA_LoadingProfiles.DA_LoadingProfiles + */ + UPROPERTY(Config, EditDefaultsOnly, Category = "FrontEnd|Loading") + TSoftObjectPtr LoadingProfilesAsset; + + /** + * DataTable com dicas (rows = FZMMOLoadingTipRow). Opcional — vazio + * significa "sem dicas". Configure em DefaultGame.ini: + * [/Script/ZMMO.UIFrontEndFlowSubsystem] + * LoadingTipsAsset=/Game/ZMMO/Data/UI/Loading/DT_LoadingTips.DT_LoadingTips + */ + UPROPERTY(Config, EditDefaultsOnly, Category = "FrontEnd|Loading") + TSoftObjectPtr LoadingTipsAsset; + private: void BindNetwork(); void UnbindNetwork(); @@ -170,8 +190,26 @@ private: UFUNCTION() void HandleServerTravelRequested(const FString& MapName, const FString& MapPath); + /** + * Spawn do player local — fim da última etapa do loading EnteringWorld. + * Assinatura espelha FZeusOnPlayerSpawned (FiveParams). + */ + UFUNCTION() + void HandlePlayerSpawned(int32 EntityId, bool bIsLocal, FVector PosCm, + float YawDeg, int64 ServerTimeMs); + + /** Disparado pela tela de loading quando todas as etapas viram Done. */ + UFUNCTION() + void HandleLoadingComplete(); + void HandlePostLoadMap(UWorld* LoadedWorld); + /** Resolve+carrega o DA de perfis (sync). */ + UZMMOLoadingProfilesDataAsset* GetLoadingProfiles(); + + /** Resolve+carrega o DT de dicas (sync). */ + UDataTable* GetLoadingTips(); + /** * Dívida técnica (D5): ServerHello traz ThemeId, mas o UZeusNetworkSubsystem * ainda não expõe um delegate/getter dedicado. Quando expuser, ligar aqui @@ -199,6 +237,18 @@ private: UPROPERTY(Transient) TObjectPtr ScreenSet; + UPROPERTY(Transient) + TObjectPtr LoadingProfiles; + + UPROPERTY(Transient) + TObjectPtr LoadingTips; + + /** + * Tela de loading atualmente em Modal. Weak porque o stack do CommonUI + * é dono — se popar por outro caminho não queremos dangling. + */ + TWeakObjectPtr ActiveLoadingScreen; + bool bNetBound = false; bool bTravelingToWorld = false; FDelegateHandle PostLoadMapHandle;