From 396223e2a859af0bdeb6d34ff0482403cf3ea22f Mon Sep 17 00:00:00 2001 From: Mateus Rodrigues Date: Sat, 23 May 2026 05:27:41 -0300 Subject: [PATCH] feat(ui-ingame): UI in-game system + PlayerState + Component Registry + HUD composite MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Espelho simétrico do front-end pattern (UUIFrontEndFlowSubsystem + DA_FrontEndScreenSet + UUIPrimaryGameLayout_Base) para a UI in-game. Pavimenta caminho pra Inventory, Skills, StatusWindow, EscapeMenu, etc. ## UI in-game system (Source/ZMMO/Game/UI/InGame/) - EZMMOInGameUIState enum (None, Playing, StatusWindow, Inventory, SkillTree, EscapeMenu, Loading) em Data/UI/InGameTypes.h - UUIInGameScreenSet DataAsset: TMap> - UUIInGameFlowSubsystem (GameInstanceSubsystem) com SetState/ToggleScreen - DA_InGameScreenSet em Content/ZMMO/UI/InGame/ mapeia Playing->WBP_HUD - Layer routing: Playing -> UI.Layer.Game | menus -> GameMenu | loading -> Modal ## AHUD pattern (idiomatic UE5) - AZMMOHUD : AHUD em Source/ZMMO/Game/Modes/ - AZMMOGameMode::HUDClass = AZMMOHUD::StaticClass() - AZMMOHUD::BeginPlay chama UIInGameFlowSubsystem::StartInGame - Pawn não cria mais HUD (separação de responsabilidades) ## PlayerState + Component Registry (Open-Closed) - AZMMOPlayerState : APlayerState em Source/ZMMO/Game/Modes/ - AZMMOGameMode::PlayerStateClass = AZMMOPlayerState::StaticClass() - UPROPERTY Config TArray> ComponentClasses - DefaultGame.ini: +ComponentClasses=/Script/ZMMOAttributes.ZMMOAttributeComponent - Ctor lê via GConfig (mais robusto que UPROPERTY Config + parsing) e instancia cada componente como CreateDefaultSubobject Por que componentes no PlayerState e não no Pawn: - Sobrevive ao despawn (morte/respawn) - Spectator-friendly - 1:1 com player (Pawn pode trocar: vehicle, mount, possession) - Padrão MMO clássico (rathena, TrinityCore) Adicionar novo módulo (Inventory, Skills, Guild) = 1 linha no DefaultGame.ini, sem editar AZMMOPlayerState.cpp. ## HUD composite - UZMMOHudWidget : UCommonActivatableWidget em ZMMO core (UI/InGame/) - WBP_HUD em Content/ZMMO/UI/HUD/ herda UZMMOHudWidget - HpSpBar (UZMMOHudHpSpWidget — sub-modulo ZMMOAttributes) embedded via BindWidgetOptional. Sub-widgets futuros: PlayerInfo, Minimap, QuickBar, Buffs, Chat, TargetInfo - UZMMOHudHpSpWidget volta a ser UUserWidget puro (sub-widget, não root) - UZMMOHudWidget::NativeOnActivated: - SetVisibility(HitTestInvisible) — mouse passa direto pro Pawn - BindToLocalPlayer: busca AttributeComponent no PC->PlayerState - Subscreve OnAttributesChanged/OnHpSpChanged/OnLevelUp - Propaga snapshots pra sub-widgets via HpSpBar->ApplySnapshot - GetDesiredInputConfig override: ECommonInputMode::Game (sem cursor, movimento livre). Menus override pra GameAndMenu/Menu. ## NetworkHandler refatorado (AttributeComponent agora no PlayerState) - ZMMOAttributeNetworkHandler::FindComponentForEntity: busca via GameState->PlayerArray (O(N_players)), não TActorIterator (O(N_actors)) - ZMMOAttributes.Build.cs += CommonUI, CommonInput ## EnsureRootLayout (fix crítico) - OpenLevel invalida widgets no viewport ("InvalidateAllWidgets") - RootLayout (criado pelo FrontEndFlow no boot) sobrevive como UObject (LocalPlayerSubsystem) mas fica órfão fora do viewport - Push do WBP_HUD no Stack_Game funcionava mas widget pai (RootLayout) estava fora do viewport → invisível - UUIFrontEndFlowSubsystem::EnsureRootLayout (público) recria via UIManagerSubsystem (idempotente) - UUIInGameFlowSubsystem::StartInGame chama EnsureRootLayout antes do push ## PlayerCharacter simplificado - Removido AttributeComponent (vai pro PlayerState via Registry) - Removido HudHpSpWidgetClass + spawn manual de HUD - HandleLocalSpawnReady só faz seed do EntityId via PlayerState - UI lifecycle agora é responsabilidade do AZMMOHUD::BeginPlay ## Verificação (smoke test) PIE: Login -> CharSelect -> Lobby -> EnterWorld ↓ LogZMMO: AZMMOPlayerState ctor: 1 componente(s) no registry LogZMMO: ZMMOPlayerState: componente registrado [0] ZMMOAttributeComponent LogZMMO: FrontEndFlow::EnsureRootLayout: RootLayout recriado LogZMMO: InGameFlow: state None -> Playing LogZMMO: InGameFlow: tela Playing pushed em UI.Layer.Game (widget=WBP_HUD_C_0) LogZMMO: ZMMOHudWidget activated (HpSpBar=WBP_HUD_HpSpBar_C_0) ↓ HUD aparece com Lv 5 / HP 10/200 / SP 5/20 (valores do DB) Regen funciona (HP/SP enchem a cada 6s/8s) Movimento normal (input flui pro Pawn) Co-Authored-By: Claude Opus 4.7 --- Config/DefaultGame.ini | 23 ++ Content/ZMMO/UI/HUD/WBP_HUD.uasset | Bin 0 -> 28757 bytes Content/ZMMO/UI/HUD/WBP_HUD_HpSpBar.uasset | Bin 33509 -> 38740 bytes .../ZMMO/UI/InGame/DA_InGameScreenSet.uasset | Bin 0 -> 1560 bytes Source/ZMMO/Data/UI/InGameTypes.h | 34 +++ .../ZMMO/Game/Entity/ZMMOPlayerCharacter.cpp | 82 ++----- Source/ZMMO/Game/Entity/ZMMOPlayerCharacter.h | 20 +- Source/ZMMO/Game/Modes/ZMMOGameMode.cpp | 4 + Source/ZMMO/Game/Modes/ZMMOHUD.cpp | 40 ++++ Source/ZMMO/Game/Modes/ZMMOHUD.h | 36 +++ Source/ZMMO/Game/Modes/ZMMOPlayerState.cpp | 82 +++++++ Source/ZMMO/Game/Modes/ZMMOPlayerState.h | 104 +++++++++ .../UI/FrontEnd/UIFrontEndFlowSubsystem.cpp | 33 ++- .../UI/FrontEnd/UIFrontEndFlowSubsystem.h | 12 + .../Game/UI/InGame/UIInGameFlowSubsystem.cpp | 210 ++++++++++++++++++ .../Game/UI/InGame/UIInGameFlowSubsystem.h | 97 ++++++++ .../ZMMO/Game/UI/InGame/UIInGameScreenSet.cpp | 7 + .../ZMMO/Game/UI/InGame/UIInGameScreenSet.h | 35 +++ Source/ZMMO/Game/UI/InGame/ZMMOHudWidget.cpp | 122 ++++++++++ Source/ZMMO/Game/UI/InGame/ZMMOHudWidget.h | 102 +++++++++ .../Private/ZMMOAttributeNetworkHandler.cpp | 55 +++-- .../Private/ZMMOHudHpSpWidget.cpp | 106 +++------ .../ZMMOAttributes/Public/ZMMOHudHpSpWidget.h | 63 +++--- Source/ZMMOAttributes/ZMMOAttributes.Build.cs | 2 + Source/ZMMOAttributes/module.json | 2 +- 25 files changed, 1029 insertions(+), 242 deletions(-) create mode 100644 Content/ZMMO/UI/HUD/WBP_HUD.uasset create mode 100644 Content/ZMMO/UI/InGame/DA_InGameScreenSet.uasset create mode 100644 Source/ZMMO/Data/UI/InGameTypes.h create mode 100644 Source/ZMMO/Game/Modes/ZMMOHUD.cpp create mode 100644 Source/ZMMO/Game/Modes/ZMMOHUD.h create mode 100644 Source/ZMMO/Game/Modes/ZMMOPlayerState.cpp create mode 100644 Source/ZMMO/Game/Modes/ZMMOPlayerState.h create mode 100644 Source/ZMMO/Game/UI/InGame/UIInGameFlowSubsystem.cpp create mode 100644 Source/ZMMO/Game/UI/InGame/UIInGameFlowSubsystem.h create mode 100644 Source/ZMMO/Game/UI/InGame/UIInGameScreenSet.cpp create mode 100644 Source/ZMMO/Game/UI/InGame/UIInGameScreenSet.h create mode 100644 Source/ZMMO/Game/UI/InGame/ZMMOHudWidget.cpp create mode 100644 Source/ZMMO/Game/UI/InGame/ZMMOHudWidget.h diff --git a/Config/DefaultGame.ini b/Config/DefaultGame.ini index a93670a..11749f3 100644 --- a/Config/DefaultGame.ini +++ b/Config/DefaultGame.ini @@ -34,3 +34,26 @@ ScreenSetAsset=/Game/ZMMO/UI/FrontEnd/DA_FrontEndScreenSet.DA_FrontEndScreenSet ; server (Server/ZeusServerEngine/Config/DataTables/maps_config.json) — o ; cliente resolve `mapId -> ClientLevel/spawns` via FZMMOMapDef rows. MapsTableAsset=/Game/ZMMO/Data/World/DT_Maps.DT_Maps + +; ----------------------------------------------------------------------------- +; UI in-game (PR 19+). Espelho do front-end pattern: +; - ScreenSetAsset mapeia EZMMOInGameUIState -> WBP (Playing -> WBP_HUD, +; StatusWindow -> WBP_StatusWindow, etc.) +; - Subsystem orquestra: AZMMOHUD::BeginPlay chama StartInGame que vai pra +; Playing. +; Asset criado via MCP em /Game/ZMMO/UI/InGame/DA_InGameScreenSet. +; ----------------------------------------------------------------------------- +[/Script/ZMMO.UIInGameFlowSubsystem] +ScreenSetAsset=/Game/ZMMO/UI/InGame/DA_InGameScreenSet.DA_InGameScreenSet + +; ----------------------------------------------------------------------------- +; PlayerState Component Registry (PR 19+). Cada módulo MMO registra seus +; UActorComponent aqui. O AZMMOPlayerState lê esta lista no construtor e +; instancia cada classe como subobject — pattern Open-Closed: adicionar +; Inventory/Skills/Guild = mais uma linha aqui, sem editar AZMMOPlayerState. +; +; Convenção: usa o path /Script/. (sem o "U" do prefixo +; C++; UClass resolve pelo nome curto). +; ----------------------------------------------------------------------------- +[/Script/ZMMO.ZMMOPlayerState] ++ComponentClasses=/Script/ZMMOAttributes.ZMMOAttributeComponent diff --git a/Content/ZMMO/UI/HUD/WBP_HUD.uasset b/Content/ZMMO/UI/HUD/WBP_HUD.uasset new file mode 100644 index 0000000000000000000000000000000000000000..b3bc8f539d6b63578573d5df1e471d1a7cacd0c5 GIT binary patch literal 28757 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 CSy(6l literal 0 HcmV?d00001 diff --git a/Content/ZMMO/UI/HUD/WBP_HUD_HpSpBar.uasset b/Content/ZMMO/UI/HUD/WBP_HUD_HpSpBar.uasset index f16ba4f9d38d8b05aff4ffb7d642a59bc2b4d198..eb05158c0d4bd41f9ae6627efc1c79b417f59173 100644 GIT binary patch literal 38740 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 b`+X-c@YRDuno@uB=vM0Rt`?3Ai1_~p<{Zmz delta 8547 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 UI.Layer.Game (HUD principal) + * - StatusWindow / Inventory / + * SkillTree / EscapeMenu -> UI.Layer.GameMenu (sobreposicoes) + * - Loading -> UI.Layer.Modal (overlay critico) + * + * `Playing` e' o estado base — quando in-game, sempre ha um HUD no + * Layer.Game. Abrir Status/Inventory NAO substitui o HUD; empilha em + * Layer.GameMenu por cima dele. + */ +UENUM(BlueprintType) +enum class EZMMOInGameUIState : uint8 +{ + None, ///< Antes do player local spawnar; UI in-game inativa. + Playing, ///< HUD principal visivel (HP/SP/level/etc). + StatusWindow, ///< Janela de atributos + botões de alocacao (Fase 4). + Inventory, ///< (futuro) Bag/equipamentos. + SkillTree, ///< (futuro) Arvore de skills. + EscapeMenu, ///< (futuro) Pausa, settings, logout. + Loading ///< Overlay de loading generico (transitorio). +}; diff --git a/Source/ZMMO/Game/Entity/ZMMOPlayerCharacter.cpp b/Source/ZMMO/Game/Entity/ZMMOPlayerCharacter.cpp index d6cd981..a4a2832 100644 --- a/Source/ZMMO/Game/Entity/ZMMOPlayerCharacter.cpp +++ b/Source/ZMMO/Game/Entity/ZMMOPlayerCharacter.cpp @@ -13,12 +13,11 @@ #include "GameFramework/SpringArmComponent.h" #include "InputAction.h" #include "InputActionValue.h" -#include "Blueprint/UserWidget.h" +#include "GameFramework/PlayerState.h" #include "Subsystems/WorldSubsystem.h" #include "UObject/ConstructorHelpers.h" #include "ZMMO.h" #include "ZMMOAttributeComponent.h" -#include "ZMMOHudHpSpWidget.h" #include "ZMMOWorldSubsystem.h" #include "ZeusNetworkSubsystem.h" #include "UI/FrontEnd/UIFrontEndFlowSubsystem.h" @@ -55,12 +54,8 @@ AZMMOPlayerCharacter::AZMMOPlayerCharacter() FollowCamera->SetupAttachment(CameraBoom, USpringArmComponent::SocketName); FollowCamera->bUsePawnControlRotation = false; - // AttributeSystem cliente (sub-modulo ZMMOAttributes). Estado authoritative - // vive no servidor (Game/MMO/Modules/AttributeSystem); este componente - // apenas armazena o ultimo snapshot recebido e expoe delegates pro HUD. - // Roteamento server -> componente via UZMMOAttributeNetworkHandler - // (UWorldSubsystem em ZMMOAttributes/Private/). - AttributeComponent = CreateDefaultSubobject(TEXT("AttributeComponent")); + // AttributeComponent migrou pro PlayerState (AZMMOPlayerState + Component + // Registry config-driven). Pawn fica leve — so' movement/input/camera. // Defaults visuais (mesh + AnimBP) — alinhados ao ZClientMMO. Permitem ao // motor spawnar AZMMOPlayerCharacter directamente como DefaultPawnClass do @@ -111,20 +106,6 @@ AZMMOPlayerCharacter::AZMMOPlayerCharacter() if (LookActionAsset.Succeeded()) { LookAction = LookActionAsset.Object; } if (MouseLookActionAsset.Succeeded()) { MouseLookAction = MouseLookActionAsset.Object; } if (JumpActionAsset.Succeeded()) { JumpAction = JumpActionAsset.Object; } - - // HUD default — WBP_HUD_HpSpBar herda de UZMMOHudHpSpWidget. BP filho do - // player character pode sobrescrever via EditAnywhere. - static ConstructorHelpers::FClassFinder HudClassFinder( - TEXT("/Game/ZMMO/UI/HUD/WBP_HUD_HpSpBar")); - if (HudClassFinder.Succeeded()) - { - HudHpSpWidgetClass = HudClassFinder.Class; - } - else - { - UE_LOG(LogZMMOPlayer, Warning, - TEXT("WBP_HUD_HpSpBar not found at /Game/ZMMO/UI/HUD/ — HUD desactivado")); - } } void AZMMOPlayerCharacter::BeginPlay() @@ -164,11 +145,6 @@ void AZMMOPlayerCharacter::EndPlay(const EEndPlayReason::Type EndPlayReason) { UnbindZeusSpawnDelegate(); ZeusNetwork = nullptr; - if (HudHpSpWidget) - { - HudHpSpWidget->RemoveFromParent(); - HudHpSpWidget = nullptr; - } Super::EndPlay(EndPlayReason); } @@ -373,49 +349,23 @@ void AZMMOPlayerCharacter::HandleLocalSpawnReady(const int32 InEntityId, const F } } - // Seed do EntityId no AttributeComponent: o S_ATTRIBUTE_SNAPSHOT_FULL ja - // pode estar em transito (server envia logo apos S_SPAWN_PLAYER) — sem - // este seed o NetworkHandler nao consegue achar o componente certo via - // lookup por EntityId. - if (AttributeComponent) + // Seed do EntityId no AttributeComponent do PlayerState. O componente + // vive no AZMMOPlayerState via Component Registry — busca via PlayerState + // (sobrevive ao despawn do Pawn). S_ATTRIBUTE_SNAPSHOT_FULL chega via + // UDP e e' rouado pelo ZMMOAttributeNetworkHandler via lookup por + // EntityId no PlayerArray do GameState. + if (APlayerState* PS = GetPlayerState()) { - AttributeComponent->SeedEntityId(InEntityId); - } - - // Spawn do HUD de atributos (HP/SP/level). Apenas player local (este - // metodo so' roda quando bIsLocal=true em HandleZeusPlayerSpawned). - UE_LOG(LogZMMOPlayer, Warning, - TEXT("HUD spawn check: HudClass=%s AttrComp=%s HudPrev=%s Controller=%s"), - HudHpSpWidgetClass ? *HudHpSpWidgetClass->GetName() : TEXT("NULL"), - AttributeComponent ? TEXT("OK") : TEXT("NULL"), - HudHpSpWidget ? TEXT("ALREADY_EXISTS") : TEXT("none"), - GetController() ? *GetController()->GetName() : TEXT("NULL")); - - if (HudHpSpWidgetClass && AttributeComponent && !HudHpSpWidget) - { - APlayerController* PC = Cast(GetController()); - if (!PC) + if (UZMMOAttributeComponent* AttrComp = PS->FindComponentByClass()) { - UE_LOG(LogZMMOPlayer, Warning, TEXT("HUD spawn: GetController() retornou null/nao-PC. Adiando.")); - } - else - { - HudHpSpWidget = CreateWidget(PC, HudHpSpWidgetClass); - if (!HudHpSpWidget) - { - UE_LOG(LogZMMOPlayer, Error, TEXT("HUD spawn: CreateWidget retornou NULL para classe %s"), - *HudHpSpWidgetClass->GetName()); - } - else - { - HudHpSpWidget->AddToViewport(/*ZOrder=*/10); - HudHpSpWidget->BindToAttributeComponent(AttributeComponent); - UE_LOG(LogZMMOPlayer, Warning, - TEXT("HUD spawn: widget criado e adicionado ao viewport (%s)"), - *HudHpSpWidget->GetName()); - } + AttrComp->SeedEntityId(InEntityId); } } + + // UI in-game e' responsabilidade do AZMMOHUD (GameMode.HUDClass), nao do + // Pawn. AHUD::BeginPlay chama UUIInGameFlowSubsystem::StartInGame, que + // pushea WBP_HUD em UI.Layer.Game. UZMMOHudWidget::NativeOnActivated auto-binda + // no UZMMOAttributeComponent via PlayerState. } void AZMMOPlayerCharacter::FlushInputAxisToServer(const float DeltaSeconds) diff --git a/Source/ZMMO/Game/Entity/ZMMOPlayerCharacter.h b/Source/ZMMO/Game/Entity/ZMMOPlayerCharacter.h index 35c28ca..dc6d166 100644 --- a/Source/ZMMO/Game/Entity/ZMMOPlayerCharacter.h +++ b/Source/ZMMO/Game/Entity/ZMMOPlayerCharacter.h @@ -11,8 +11,6 @@ class USpringArmComponent; class UCameraComponent; class UInputAction; class UZeusNetworkSubsystem; -class UZMMOAttributeComponent; -class UZMMOHudHpSpWidget; struct FInputActionValue; DECLARE_LOG_CATEGORY_EXTERN(LogZMMOPlayer, Log, All); @@ -49,10 +47,8 @@ class ZMMO_API AZMMOPlayerCharacter : public ACharacter, public IZMMOEntityInter UPROPERTY(VisibleAnywhere, BlueprintReadOnly, Category = "Components", meta = (AllowPrivateAccess = "true")) UCameraComponent* FollowCamera; - /** Atributos do MMO (HP/SP/STR/etc). Estado authoritative no server; - * cliente apenas exibe via UI bindando em `OnAttributesChanged`. */ - UPROPERTY(VisibleAnywhere, BlueprintReadOnly, Category = "Components", meta = (AllowPrivateAccess = "true")) - UZMMOAttributeComponent* AttributeComponent; + // AttributeComponent vive no PlayerState (AZMMOPlayerState + Component Registry). + // Acesso: GetPlayerState()->GetZMMOComponent(). protected: UPROPERTY(EditAnywhere, Category = "Input") @@ -80,12 +76,6 @@ protected: UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "ZMMO|Networking", meta = (ClampMin = "0.05", ClampMax = "5.0")) float HeartbeatIntervalSec = 0.2f; - /** Classe do HUD de HP/SP. Spawnada quando o pawn local recebe S_SPAWN_PLAYER. - * Default = WBP_HUD_HpSpBar que herda de UZMMOHudHpSpWidget (criado em Fase 1 - * via MCP em /Game/ZMMO/UI/HUD/). Deixar null desativa o HUD. */ - UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "ZMMO|UI") - TSubclassOf HudHpSpWidgetClass; - public: AZMMOPlayerCharacter(); @@ -102,7 +92,6 @@ public: FORCEINLINE USpringArmComponent* GetCameraBoom() const { return CameraBoom; } FORCEINLINE UCameraComponent* GetFollowCamera() const { return FollowCamera; } - FORCEINLINE UZMMOAttributeComponent* GetAttributeComponent() const { return AttributeComponent; } protected: virtual void BeginPlay() override; @@ -173,9 +162,4 @@ private: float SendAccumulatorSec = 0.0f; float TimeSinceLastSendSec = 0.0f; int32 InputSequence = 0; - - /** Instancia do HUD spawnada no spawn local. nullptr ate o spawn ou se - * HudHpSpWidgetClass nao foi setado. */ - UPROPERTY(Transient) - TObjectPtr HudHpSpWidget; }; diff --git a/Source/ZMMO/Game/Modes/ZMMOGameMode.cpp b/Source/ZMMO/Game/Modes/ZMMOGameMode.cpp index 48a5144..75821f3 100644 --- a/Source/ZMMO/Game/Modes/ZMMOGameMode.cpp +++ b/Source/ZMMO/Game/Modes/ZMMOGameMode.cpp @@ -1,10 +1,14 @@ #include "ZMMOGameMode.h" +#include "ZMMOHUD.h" #include "ZMMOPlayerCharacter.h" #include "ZMMOPlayerController.h" +#include "ZMMOPlayerState.h" AZMMOGameMode::AZMMOGameMode() { DefaultPawnClass = AZMMOPlayerCharacter::StaticClass(); PlayerControllerClass = AZMMOPlayerController::StaticClass(); + PlayerStateClass = AZMMOPlayerState::StaticClass(); + HUDClass = AZMMOHUD::StaticClass(); } diff --git a/Source/ZMMO/Game/Modes/ZMMOHUD.cpp b/Source/ZMMO/Game/Modes/ZMMOHUD.cpp new file mode 100644 index 0000000..c7c6087 --- /dev/null +++ b/Source/ZMMO/Game/Modes/ZMMOHUD.cpp @@ -0,0 +1,40 @@ +#include "ZMMOHUD.h" + +#include "Engine/GameInstance.h" +#include "ZMMO.h" +#include "UI/InGame/UIInGameFlowSubsystem.h" + +AZMMOHUD::AZMMOHUD() +{ + // AHUD nao precisa tick — toda atualizacao vem via UMG (delegates + + // CommonUI). PrimaryActorTick default e' off; mantemos. +} + +void AZMMOHUD::BeginPlay() +{ + Super::BeginPlay(); + + // UI in-game assume responsabilidade aqui. Subsystem orquestra: + // resolve DA_InGameScreenSet[Playing] -> WBP_HUD, pushea em UI.Layer.Game. + if (const UGameInstance* GI = GetGameInstance()) + { + if (UUIInGameFlowSubsystem* Flow = GI->GetSubsystem()) + { + Flow->StartInGame(); + } + else + { + UE_LOG(LogZMMO, Warning, + TEXT("AZMMOHUD: UUIInGameFlowSubsystem nao encontrado — HUD nao sera spawnado")); + } + } +} + +void AZMMOHUD::EndPlay(const EEndPlayReason::Type EndPlayReason) +{ + // UUIInGameFlowSubsystem persiste no GameInstance entre OpenLevel — nao + // limpamos UI in-game aqui (o front-end flow vai tomar conta na proxima + // transicao de estado). Cleanup do widget atual acontece no + // NativeOnDeactivated quando o stack for esvaziado pelo flow. + Super::EndPlay(EndPlayReason); +} diff --git a/Source/ZMMO/Game/Modes/ZMMOHUD.h b/Source/ZMMO/Game/Modes/ZMMOHUD.h new file mode 100644 index 0000000..44c95f7 --- /dev/null +++ b/Source/ZMMO/Game/Modes/ZMMOHUD.h @@ -0,0 +1,36 @@ +#pragma once + +#include "CoreMinimal.h" +#include "GameFramework/HUD.h" +#include "ZMMOHUD.generated.h" + +/** + * HUD do MMO. Pattern UE5 idiomatico: o GameMode atribui `HUDClass`, o + * engine cria 1 instancia por PlayerController automaticamente. + * + * Responsabilidade unica: orquestrar o lifecycle da UI in-game. Em + * BeginPlay, pede ao `UUIInGameFlowSubsystem` pra entrar em `Playing` + * (que pushea WBP_HUD em UI.Layer.Game). Em EndPlay/destrucao, o flow + * subsystem cuida do cleanup. + * + * Separacao de responsabilidades: + * - AZMMOPlayerCharacter (Pawn) — movimento, input, AttributeComponent + * - AZMMOPlayerController — input mapping, mouse + * - AZMMOHUD — UI lifecycle + * - AZMMOGameMode — wires tudo via class slots + * + * NAO desenha nada via canvas (AHUD::DrawHUD). UI inteira vive em UMG + + * CommonUI (UI.Layer.Game / GameMenu / Modal). + */ +UCLASS() +class ZMMO_API AZMMOHUD : public AHUD +{ + GENERATED_BODY() + +public: + AZMMOHUD(); + +protected: + virtual void BeginPlay() override; + virtual void EndPlay(const EEndPlayReason::Type EndPlayReason) override; +}; diff --git a/Source/ZMMO/Game/Modes/ZMMOPlayerState.cpp b/Source/ZMMO/Game/Modes/ZMMOPlayerState.cpp new file mode 100644 index 0000000..e7cfda9 --- /dev/null +++ b/Source/ZMMO/Game/Modes/ZMMOPlayerState.cpp @@ -0,0 +1,82 @@ +#include "ZMMOPlayerState.h" + +#include "Components/ActorComponent.h" +#include "Misc/ConfigCacheIni.h" +#include "ZMMO.h" + +AZMMOPlayerState::AZMMOPlayerState() +{ + // Component Registry: instancia cada classe declarada em + // DefaultGame.ini::[/Script/ZMMO.ZMMOPlayerState]+ComponentClasses=... + // + // Le manualmente via GConfig para evitar timing do UPROPERTY(Config), + // que requer LoadConfig() ja ter rodado no CDO antes da instance ser + // construida. Pra propriedades em arrays config com `+` syntax, lendo + // direto e mais previsivel. + if (ComponentClasses.Num() == 0 && GConfig) + { + TArray ClassPaths; + GConfig->GetArray(TEXT("/Script/ZMMO.ZMMOPlayerState"), + TEXT("ComponentClasses"), + ClassPaths, + GGameIni); + for (const FString& Path : ClassPaths) + { + if (UClass* C = LoadClass(nullptr, *Path)) + { + ComponentClasses.Add(C); + } + else + { + UE_LOG(LogZMMO, Warning, + TEXT("ZMMOPlayerState: ComponentClasses path nao resolveu: %s"), *Path); + } + } + } + + UE_LOG(LogZMMO, Log, + TEXT("AZMMOPlayerState ctor: %d componente(s) no registry (instance=%s)"), + ComponentClasses.Num(), *GetName()); + + int32 SubobjectIndex = 0; + for (const TSubclassOf& CompClassRef : ComponentClasses) + { + UClass* CompClass = CompClassRef.Get(); + if (!CompClass) + { + UE_LOG(LogZMMO, Warning, + TEXT("ZMMOPlayerState: ComponentClass null em ComponentClasses[%d]"), + SubobjectIndex); + ++SubobjectIndex; + continue; + } + const FName CompName = MakeUniqueObjectName(this, CompClass, + FName(*FString::Printf(TEXT("ZMMOComp_%d_%s"), SubobjectIndex, *CompClass->GetName()))); + // Overload runtime de CreateDefaultSubobject retorna UObject* (sem template) — + // precisa cast para UActorComponent. + UActorComponent* Comp = Cast(CreateDefaultSubobject(CompName, CompClass, CompClass, + /*bIsRequired*/ false, /*bIsTransient*/ false)); + if (Comp) + { + UE_LOG(LogZMMO, Log, + TEXT("ZMMOPlayerState: componente registrado [%d] %s"), + SubobjectIndex, *CompClass->GetName()); + } + else + { + UE_LOG(LogZMMO, Warning, + TEXT("ZMMOPlayerState: CreateDefaultSubobject falhou para %s"), + *CompClass->GetName()); + } + ++SubobjectIndex; + } +} + +void AZMMOPlayerState::SetPublicIdentity(int64 InEntityId, const FString& InCharId, + int32 InBaseLevel, int32 InClassId) +{ + ZMMOEntityId = InEntityId; + CharId = InCharId; + BaseLevel = InBaseLevel; + ClassId = InClassId; +} diff --git a/Source/ZMMO/Game/Modes/ZMMOPlayerState.h b/Source/ZMMO/Game/Modes/ZMMOPlayerState.h new file mode 100644 index 0000000..d7be27f --- /dev/null +++ b/Source/ZMMO/Game/Modes/ZMMOPlayerState.h @@ -0,0 +1,104 @@ +#pragma once + +#include "CoreMinimal.h" +#include "GameFramework/PlayerState.h" +#include "Templates/SubclassOf.h" +#include "ZMMOPlayerState.generated.h" + +class UActorComponent; + +/** + * PlayerState do MMO. Pattern UE5 idiomatico: o GameMode atribui + * `PlayerStateClass`, engine cria 1 instancia por PlayerController. + * + * Responsabilidade — estado **publico** do jogador (visivel pra outros): + * - Identidade: charId, entityId, name, guild + * - Progressao publica: baseLevel, classId + * - Estado: alive/dead, in-combat (futuro) + * + * Dados PRIVADOS (HP/SP atuais, stats brutos, inventory, hotbar) vivem + * em componentes anexados via Component Registry (vide abaixo). + * + * Por que componentes no PlayerState e nao no Pawn: + * - **Sobrevive ao despawn** (morte/respawn — Pawn destrui, PlayerState + * mantem). Padrao MMO classico (rathena, TrinityCore). + * - **Spectator-friendly**: dados acessiveis sem ter pawn. + * - **1:1 com player** (Pawn pode trocar — vehicle, mount, possession). + * + * ## Component Registry (Open-Closed) + * + * Cada modulo (ZMMOAttributes, ZMMOInventory, ZMMOSkills futuros) declara + * sua component class em DefaultGame.ini: + * + * [/Script/ZMMO.ZMMOPlayerState] + * +ComponentClasses=/Script/ZMMOAttributes.ZMMOAttributeComponent + * +ComponentClasses=/Script/ZMMOInventory.ZMMOInventoryComponent + * +ComponentClasses=/Script/ZMMOSkills.ZMMOSkillComponent + * + * O construtor de AZMMOPlayerState le essa lista (Config) e instancia + * cada classe como subobject. PlayerState fica fechado pra modificacao, + * modulos abertos pra extensao — adicionar Inventory NAO requer + * recompilar/editar AZMMOPlayerState. + * + * Acesso: `PlayerState->FindComponentByClass()` + * ou o helper `GetZMMOComponent()`. + */ +UCLASS(Config = Game, BlueprintType) +class ZMMO_API AZMMOPlayerState : public APlayerState +{ + GENERATED_BODY() + +public: + AZMMOPlayerState(); + + // === Identidade publica === + + /** EntityId autoritativo do server. 0 ate o spawn local confirmar. */ + UPROPERTY(VisibleInstanceOnly, BlueprintReadOnly, Category = "ZMMO|Identity") + int64 ZMMOEntityId = 0; + + /** CharId (BIGINT no DB; FString pra preservar 64 bits em Blueprint). */ + UPROPERTY(VisibleInstanceOnly, BlueprintReadOnly, Category = "ZMMO|Identity") + FString CharId; + + // === Progressao publica === + + UPROPERTY(VisibleInstanceOnly, BlueprintReadOnly, Category = "ZMMO|Progress") + int32 BaseLevel = 1; + + UPROPERTY(VisibleInstanceOnly, BlueprintReadOnly, Category = "ZMMO|Progress") + int32 ClassId = 0; + + // === Guild (futuro) === + + UPROPERTY(VisibleInstanceOnly, BlueprintReadOnly, Category = "ZMMO|Guild") + FString GuildName; + + // === Helpers === + + /// Atualizado quando S_ATTRIBUTE_SNAPSHOT_FULL chega — UZMMOHudWidget chama + /// isso pra refletir os dados publicos do snapshot no PlayerState. + UFUNCTION(BlueprintCallable, Category = "ZMMO|Identity") + void SetPublicIdentity(int64 InEntityId, const FString& InCharId, + int32 InBaseLevel, int32 InClassId); + + /// Template helper: PS->GetZMMOComponent() + template + T* GetZMMOComponent() const + { + return Cast(FindComponentByClass(T::StaticClass())); + } + +protected: + /** + * Component Registry — config-driven. Modulos registram suas classes + * via DefaultGame.ini com prefix `+`. Construtor instancia cada uma + * como CreateDefaultSubobject. + * + * TSubclassOf (nao TSoftClassPtr): config UE resolve classes nativas + * diretamente. Modulos C++ precisam estar carregados antes do CDO ser + * instanciado — garantido por LoadingPhase=PreDefault no .uproject. + */ + UPROPERTY(Config, EditDefaultsOnly, Category = "ZMMO|Components") + TArray> ComponentClasses; +}; diff --git a/Source/ZMMO/Game/UI/FrontEnd/UIFrontEndFlowSubsystem.cpp b/Source/ZMMO/Game/UI/FrontEnd/UIFrontEndFlowSubsystem.cpp index cdfd506..cd2393e 100644 --- a/Source/ZMMO/Game/UI/FrontEnd/UIFrontEndFlowSubsystem.cpp +++ b/Source/ZMMO/Game/UI/FrontEnd/UIFrontEndFlowSubsystem.cpp @@ -41,18 +41,7 @@ void UUIFrontEndFlowSubsystem::Deinitialize() void UUIFrontEndFlowSubsystem::StartFrontEnd() { - UUIFrontEndScreenSet* SS = GetScreenSet(); - - if (UUIManagerSubsystem* Mgr = GetUIManager()) - { - TSubclassOf LayoutClass; - if (SS && !SS->RootLayoutClass.IsNull()) - { - LayoutClass = SS->RootLayoutClass.LoadSynchronous(); - } - Mgr->CreateAndAddRootLayout(LayoutClass); - } - + EnsureRootLayout(); BindNetwork(); SetState(EZMMOFrontEndState::Boot); @@ -69,6 +58,26 @@ void UUIFrontEndFlowSubsystem::StartFrontEnd() } } +void UUIFrontEndFlowSubsystem::EnsureRootLayout() +{ + UUIManagerSubsystem* Mgr = GetUIManager(); + if (!Mgr) { return; } + if (Mgr->IsRootLayoutReady()) + { + return; // ja' no viewport + } + UUIFrontEndScreenSet* SS = GetScreenSet(); + if (!SS || SS->RootLayoutClass.IsNull()) + { + UE_LOG(LogZMMO, Warning, + TEXT("FrontEndFlow::EnsureRootLayout: RootLayoutClass ausente em ScreenSet")); + return; + } + TSubclassOf LayoutClass = SS->RootLayoutClass.LoadSynchronous(); + Mgr->CreateAndAddRootLayout(LayoutClass); + UE_LOG(LogZMMO, Log, TEXT("FrontEndFlow::EnsureRootLayout: RootLayout recriado")); +} + void UUIFrontEndFlowSubsystem::SetState(EZMMOFrontEndState NewState) { if (NewState == CurrentState) diff --git a/Source/ZMMO/Game/UI/FrontEnd/UIFrontEndFlowSubsystem.h b/Source/ZMMO/Game/UI/FrontEnd/UIFrontEndFlowSubsystem.h index 1f3b156..579939b 100644 --- a/Source/ZMMO/Game/UI/FrontEnd/UIFrontEndFlowSubsystem.h +++ b/Source/ZMMO/Game/UI/FrontEnd/UIFrontEndFlowSubsystem.h @@ -46,6 +46,18 @@ public: UFUNCTION(BlueprintCallable, Category = "FrontEnd") void StartFrontEnd(); + /** + * Garante que o root layout (UUIPrimaryGameLayout_Base) esta no viewport. + * Chamado pelo UIInGameFlowSubsystem::StartInGame apos OpenLevel — o + * `OpenLevel` invalida widgets no viewport ("InvalidateAllWidgets") e o + * RootLayout fica orfao. Idempotente: CreateAndAddRootLayout do + * UIManagerSubsystem checa `IsInViewport` antes de recriar. + * + * C++ puro (sem UFUNCTION) — Live Coding nao registra UFUNCTIONs novas; + * consumidores externos chamam direto via C++ (UIInGameFlowSubsystem). + */ + void EnsureRootLayout(); + UFUNCTION(BlueprintCallable, Category = "FrontEnd") void SetState(EZMMOFrontEndState NewState); diff --git a/Source/ZMMO/Game/UI/InGame/UIInGameFlowSubsystem.cpp b/Source/ZMMO/Game/UI/InGame/UIInGameFlowSubsystem.cpp new file mode 100644 index 0000000..cb16f33 --- /dev/null +++ b/Source/ZMMO/Game/UI/InGame/UIInGameFlowSubsystem.cpp @@ -0,0 +1,210 @@ +#include "UIInGameFlowSubsystem.h" + +#include "CommonActivatableWidget.h" // Necessario pra TSoftClassPtr::Get() +#include "Engine/AssetManager.h" +#include "Engine/GameInstance.h" +#include "Engine/StreamableManager.h" +#include "GameFramework/PlayerController.h" +#include "GameplayTagContainer.h" +#include "ZMMO.h" +#include "Data/UI/UILayerTags.h" +#include "FrontEnd/UIFrontEndFlowSubsystem.h" +#include "FrontEnd/UIManagerSubsystem.h" +#include "InGame/UIInGameScreenSet.h" + +void UUIInGameFlowSubsystem::Initialize(FSubsystemCollectionBase& Collection) +{ + Super::Initialize(Collection); + UE_LOG(LogZMMO, Log, TEXT("UIInGameFlowSubsystem initialized")); +} + +void UUIInGameFlowSubsystem::Deinitialize() +{ + ScreenSet = nullptr; + CurrentState = EZMMOInGameUIState::None; + Super::Deinitialize(); +} + +void UUIInGameFlowSubsystem::StartInGame() +{ + if (CurrentState != EZMMOInGameUIState::None) + { + // Idempotente: chamadas extras (ex.: respawn) sao no-op se ja in-world. + UE_LOG(LogZMMO, Verbose, + TEXT("InGameFlow: StartInGame ignorado (ja em estado %s)"), + *UEnum::GetValueAsString(CurrentState)); + return; + } + + // CRITICAL: OpenLevel invalida widgets no viewport ("InvalidateAllWidgets"), + // orfanando o RootLayout (UUIPrimaryGameLayout_Base) que foi criado pelo + // FrontEndFlow no boot. Sem RootLayout no viewport, PushScreenToLayer + // silenciosamente adiciona widgets ao stack do RootLayout — mas o stack + // nao e' visivel porque o parent (RootLayout) nao esta no viewport. + // + // Re-cria via EnsureRootLayout (idempotente) ANTES de qualquer push. + if (UGameInstance* GI = GetGameInstance()) + { + if (UUIFrontEndFlowSubsystem* FE = GI->GetSubsystem()) + { + FE->EnsureRootLayout(); + } + } + + // Limpa qualquer residuo do front-end (Menu/Modal podem ter "Loading" ou + // dialogs orfaos do handoff). Layer.Game vai receber o HUD novo. + if (UUIManagerSubsystem* Mgr = GetUIManager()) + { + Mgr->ClearLayer(ZMMOUITags::UI_Layer_Menu.GetTag()); + Mgr->ClearLayer(ZMMOUITags::UI_Layer_Modal.GetTag()); + Mgr->ClearLayer(ZMMOUITags::UI_Layer_GameMenu.GetTag()); + } + + SetState(EZMMOInGameUIState::Playing); +} + +void UUIInGameFlowSubsystem::SetState(EZMMOInGameUIState NewState) +{ + if (NewState == CurrentState) + { + return; + } + + const EZMMOInGameUIState OldState = CurrentState; + CurrentState = NewState; + + UE_LOG(LogZMMO, Log, TEXT("InGameFlow: state %s -> %s"), + *UEnum::GetValueAsString(OldState), + *UEnum::GetValueAsString(NewState)); + + // Decisao de stack: ao voltar pra `Playing`, fechamos quaisquer overlays + // (Status/Inventory/Menu) limpando o Layer.GameMenu — HUD em Layer.Game + // permanece. Para qualquer outro estado, mantemos HUD e empilhamos no + // layer correspondente. + if (UUIManagerSubsystem* Mgr = GetUIManager()) + { + const bool bGoingToPlaying = (NewState == EZMMOInGameUIState::Playing); + if (bGoingToPlaying) + { + Mgr->ClearLayer(ZMMOUITags::UI_Layer_GameMenu.GetTag()); + } + } + + ResolveAndPushScreen(NewState); + OnStateChanged.Broadcast(NewState); +} + +void UUIInGameFlowSubsystem::ToggleScreen(EZMMOInGameUIState Screen) +{ + if (Screen == EZMMOInGameUIState::Playing || Screen == EZMMOInGameUIState::None) + { + // Toggle para Playing nao faz sentido (estado base). Use SetState direto. + return; + } + if (CurrentState == Screen) + { + SetState(EZMMOInGameUIState::Playing); + } + else + { + SetState(Screen); + } +} + +UUIInGameScreenSet* UUIInGameFlowSubsystem::GetScreenSet() +{ + if (ScreenSet) { return ScreenSet; } + if (ScreenSetAsset.IsNull()) + { + UE_LOG(LogZMMO, Warning, + TEXT("InGameFlow: ScreenSetAsset nao configurado em DefaultGame.ini " + "[/Script/ZMMO.UIInGameFlowSubsystem] ScreenSetAsset=...")); + return nullptr; + } + // Sync load — DA e' pequeno (so' soft refs); PIE/dev OK. + ScreenSet = ScreenSetAsset.LoadSynchronous(); + return ScreenSet; +} + +UUIManagerSubsystem* UUIInGameFlowSubsystem::GetUIManager() const +{ + if (const UGameInstance* GI = GetGameInstance()) + { + if (ULocalPlayer* LP = GI->GetFirstGamePlayer()) + { + return LP->GetSubsystem(); + } + } + return nullptr; +} + +FGameplayTag UUIInGameFlowSubsystem::LayerForState(EZMMOInGameUIState State) const +{ + switch (State) + { + case EZMMOInGameUIState::Playing: + return ZMMOUITags::UI_Layer_Game.GetTag(); + case EZMMOInGameUIState::Loading: + return ZMMOUITags::UI_Layer_Modal.GetTag(); + case EZMMOInGameUIState::StatusWindow: + case EZMMOInGameUIState::Inventory: + case EZMMOInGameUIState::SkillTree: + case EZMMOInGameUIState::EscapeMenu: + default: + return ZMMOUITags::UI_Layer_GameMenu.GetTag(); + } +} + +void UUIInGameFlowSubsystem::ResolveAndPushScreen(EZMMOInGameUIState State) +{ + if (State == EZMMOInGameUIState::None) { return; } + + UUIInGameScreenSet* SS = GetScreenSet(); + if (!SS) + { + UE_LOG(LogZMMO, Warning, + TEXT("InGameFlow: ScreenSet ausente; tela de %s ignorada"), + *UEnum::GetValueAsString(State)); + return; + } + + const TSoftClassPtr Soft = SS->GetScreenForState(State); + if (Soft.IsNull()) + { + UE_LOG(LogZMMO, Warning, + TEXT("InGameFlow: Screen for state %s not configured em DA_InGameScreenSet (verificar StateScreens)"), + *UEnum::GetValueAsString(State)); + return; + } + + UUIManagerSubsystem* Mgr = GetUIManager(); + if (!Mgr) + { + UE_LOG(LogZMMO, Warning, TEXT("InGameFlow: UIManagerSubsystem ausente")); + return; + } + + const FGameplayTag Layer = LayerForState(State); + const FSoftObjectPath Path = Soft.ToSoftObjectPath(); + + UAssetManager::GetStreamableManager().RequestAsyncLoad( + Path, + FStreamableDelegate::CreateWeakLambda(this, [this, Soft, Layer, State]() + { + UUIManagerSubsystem* M = GetUIManager(); + if (!M) { return; } + UClass* Cls = Soft.Get(); + if (!Cls) + { + UE_LOG(LogZMMO, Warning, + TEXT("InGameFlow: async load falhou pra %s"), + *UEnum::GetValueAsString(State)); + return; + } + UCommonActivatableWidget* W = M->PushScreenToLayer(Layer, Cls); + UE_LOG(LogZMMO, Log, + TEXT("InGameFlow: tela %s pushed em %s (widget=%s)"), + *UEnum::GetValueAsString(State), *Layer.ToString(), + W ? *W->GetName() : TEXT("null")); + })); +} diff --git a/Source/ZMMO/Game/UI/InGame/UIInGameFlowSubsystem.h b/Source/ZMMO/Game/UI/InGame/UIInGameFlowSubsystem.h new file mode 100644 index 0000000..5037bde --- /dev/null +++ b/Source/ZMMO/Game/UI/InGame/UIInGameFlowSubsystem.h @@ -0,0 +1,97 @@ +#pragma once + +#include "CoreMinimal.h" +#include "Subsystems/GameInstanceSubsystem.h" +#include "UI/InGameTypes.h" +#include "UIInGameFlowSubsystem.generated.h" + +class UUIInGameScreenSet; +class UUIManagerSubsystem; +class UCommonActivatableWidget; +struct FGameplayTag; + +DECLARE_DYNAMIC_MULTICAST_DELEGATE_OneParam(FOnZMMOInGameStateChanged, EZMMOInGameUIState, NewState); + +/** + * Orquestrador da UI in-game. Espelho simetrico do UUIFrontEndFlowSubsystem + * (Game/UI/FrontEnd/UIFrontEndFlowSubsystem.h) — o front-end gerencia + * Boot/Login/ServerSelect/Lobby; este subsystem gerencia + * HUD/StatusWindow/Inventory/SkillTree/EscapeMenu in-world. + * + * UGameInstanceSubsystem pra persistir entre OpenLevel (futuro: trocar + * de mapa in-game preserva estado da UI). Inicializa em modo `None`; + * caller (`AZMMOPlayerCharacter::HandleLocalSpawnReady`) chama + * `StartInGame()` ao confirmar spawn local, que vai pra `Playing`. + * + * Convencao de layer (resolvida internamente): + * - Playing -> UI.Layer.Game + * - StatusWindow/Inventory/ + * SkillTree/EscapeMenu -> UI.Layer.GameMenu + * - Loading -> UI.Layer.Modal + * + * Adicionar nova tela in-game = 3 passos: + * 1. Adicionar entry em `EZMMOInGameUIState` + * 2. Adicionar entry no DA_InGameScreenSet (estado -> WBP) + * 3. (se for em layer custom) adicionar caso em `LayerForState` + * Subsystem nao precisa de mudancas. + */ +UCLASS(Config = Game) +class ZMMO_API UUIInGameFlowSubsystem : public UGameInstanceSubsystem +{ + GENERATED_BODY() + +public: + virtual void Initialize(FSubsystemCollectionBase& Collection) override; + virtual void Deinitialize() override; + + /** + * Ativa a UI in-game. Chamado pelo `AZMMOPlayerCharacter::HandleLocalSpawnReady` + * quando o servidor confirma o spawn local. Garante root layout, limpa + * layers do front-end (Menu/Modal residuais), e vai pra `Playing` (HUD). + */ + UFUNCTION(BlueprintCallable, Category = "InGame|UI") + void StartInGame(); + + /** Define o estado top-level. Resolve a tela do DA + push no layer correto. */ + UFUNCTION(BlueprintCallable, Category = "InGame|UI") + void SetState(EZMMOInGameUIState NewState); + + UFUNCTION(BlueprintPure, Category = "InGame|UI") + EZMMOInGameUIState GetCurrentState() const { return CurrentState; } + + /** + * Atalho UX: alterna entre `Playing` e a tela passada. Util pra teclas + * de toggle (Alt+A abre/fecha StatusWindow, I abre/fecha Inventory). + * Se ja' estiver em `Screen`, volta pra Playing; senao vai pra `Screen`. + */ + UFUNCTION(BlueprintCallable, Category = "InGame|UI") + void ToggleScreen(EZMMOInGameUIState Screen); + + /** Disparado a cada SetState bem-sucedido. */ + UPROPERTY(BlueprintAssignable, Category = "InGame|UI") + FOnZMMOInGameStateChanged OnStateChanged; + +protected: + /** DA com o mapa estado->tela. Config em DefaultGame.ini: + * [/Script/ZMMO.UIInGameFlowSubsystem] + * ScreenSetAsset=/Game/ZMMO/UI/InGame/DA_InGameScreenSet.DA_InGameScreenSet + */ + UPROPERTY(Config, EditDefaultsOnly, Category = "InGame|UI") + TSoftObjectPtr ScreenSetAsset; + +private: + UUIInGameScreenSet* GetScreenSet(); + UUIManagerSubsystem* GetUIManager() const; + + /** Retorna a layer tag onde a tela do estado deve ir. */ + FGameplayTag LayerForState(EZMMOInGameUIState State) const; + + /** Resolve a soft class + push no layer apropriado (async load). */ + void ResolveAndPushScreen(EZMMOInGameUIState State); + + UPROPERTY(Transient) + EZMMOInGameUIState CurrentState = EZMMOInGameUIState::None; + + UPROPERTY(Transient) + TObjectPtr ScreenSet; +}; diff --git a/Source/ZMMO/Game/UI/InGame/UIInGameScreenSet.cpp b/Source/ZMMO/Game/UI/InGame/UIInGameScreenSet.cpp new file mode 100644 index 0000000..9e14b95 --- /dev/null +++ b/Source/ZMMO/Game/UI/InGame/UIInGameScreenSet.cpp @@ -0,0 +1,7 @@ +#include "UIInGameScreenSet.h" + +TSoftClassPtr UUIInGameScreenSet::GetScreenForState(EZMMOInGameUIState State) const +{ + const TSoftClassPtr* Found = StateScreens.Find(State); + return Found ? *Found : TSoftClassPtr(); +} diff --git a/Source/ZMMO/Game/UI/InGame/UIInGameScreenSet.h b/Source/ZMMO/Game/UI/InGame/UIInGameScreenSet.h new file mode 100644 index 0000000..2a0bd06 --- /dev/null +++ b/Source/ZMMO/Game/UI/InGame/UIInGameScreenSet.h @@ -0,0 +1,35 @@ +#pragma once + +#include "CoreMinimal.h" +#include "Engine/DataAsset.h" +#include "Templates/SubclassOf.h" +#include "UI/InGameTypes.h" +#include "UIInGameScreenSet.generated.h" + +class UCommonActivatableWidget; + +/** + * Mapa data-driven estado -> tela in-game. Espelho de UUIFrontEndScreenSet + * (Game/UI/FrontEnd/UIFrontEndScreenSet.h) — mesma estrategia: as classes + * concretas das telas (WBP) vivem no DA, nao em codigo. Subsystem orquestra. + * + * Asset concreto: DA_InGameScreenSet em Content/ZMMO/UI/InGame/ + * (criado via MCP no editor). + * + * Adicionar nova tela = adicionar entry em EZMMOInGameUIState + entry no DA + * + WBP herdando UCommonActivatableWidget. Subsystem nao muda. + */ +UCLASS(BlueprintType) +class ZMMO_API UUIInGameScreenSet : public UDataAsset +{ + GENERATED_BODY() + +public: + /** Tela por estado in-game. */ + UPROPERTY(EditDefaultsOnly, BlueprintReadOnly, Category = "InGame") + TMap> StateScreens; + + /** Soft class da tela do estado (vazio se nao configurado). */ + UFUNCTION(BlueprintCallable, Category = "InGame") + TSoftClassPtr GetScreenForState(EZMMOInGameUIState State) const; +}; diff --git a/Source/ZMMO/Game/UI/InGame/ZMMOHudWidget.cpp b/Source/ZMMO/Game/UI/InGame/ZMMOHudWidget.cpp new file mode 100644 index 0000000..378ca3b --- /dev/null +++ b/Source/ZMMO/Game/UI/InGame/ZMMOHudWidget.cpp @@ -0,0 +1,122 @@ +#include "ZMMOHudWidget.h" + +#include "CommonInputTypeEnum.h" // ECommonInputMode +#include "Engine/World.h" +#include "GameFramework/PlayerController.h" +#include "GameFramework/PlayerState.h" +#include "ZMMO.h" +#include "ZMMOAttributeComponent.h" +#include "ZMMOHudHpSpWidget.h" + +UZMMOHudWidget::UZMMOHudWidget(const FObjectInitializer& ObjectInitializer) + : Super(ObjectInitializer) +{ + // HUD nao bloqueia "back" (Esc / B no controller). Menus reais (StatusWindow, + // EscapeMenu) sim — eles setam bIsBackHandler=true. + bIsBackHandler = false; + // HUD nao reativa stack ao virar topo (e' sempre fundo). Garante que o + // UCommonActivatableWidgetStack do Layer.Game nao force activate/deactivate. + bAutoActivate = true; +} + +TOptional UZMMOHudWidget::GetDesiredInputConfig() const +{ + // Modo Game: input flui pro PlayerController/Pawn, mouse capturado (3a pessoa). + // Sem isso, CommonUI assume Menu config (mostra cursor + bloqueia movimento). + return FUIInputConfig(ECommonInputMode::Game, EMouseCaptureMode::CapturePermanently); +} + +void UZMMOHudWidget::NativeOnActivated() +{ + Super::NativeOnActivated(); + // HUD nao intercepta cliques — mouse passa direto pro Pawn (camera, etc.). + // Sub-widgets que precisam de click (futuro: hotkeys) podem override. + SetVisibility(ESlateVisibility::HitTestInvisible); + BindToLocalPlayer(); + UE_LOG(LogZMMO, Log, TEXT("ZMMOHudWidget activated (HpSpBar=%s)"), + HpSpBar ? *HpSpBar->GetName() : TEXT("none")); +} + +void UZMMOHudWidget::NativeOnDeactivated() +{ + UnbindFromComponent(); + Super::NativeOnDeactivated(); +} + +void UZMMOHudWidget::NativeDestruct() +{ + UnbindFromComponent(); + Super::NativeDestruct(); +} + +void UZMMOHudWidget::BindToLocalPlayer() +{ + UWorld* World = GetWorld(); + if (!World) { return; } + + APlayerController* PC = World->GetFirstPlayerController(); + if (!PC) { return; } + APlayerState* PS = PC->PlayerState; + if (!PS) + { + // PlayerState pode nao existir ainda — race com GameMode setup. + // AZMMOPlayerCharacter::HandleLocalSpawnReady chama BindToAttributeComponent + // manualmente como fallback (seed do EntityId). + UE_LOG(LogZMMO, Log, TEXT("ZMMOHudWidget: PlayerState ausente no activate (race fix via HandleLocalSpawnReady esperado)")); + return; + } + UZMMOAttributeComponent* Comp = PS->FindComponentByClass(); + if (!Comp) { return; } + if (Comp == BoundComponent.Get()) { return; } // idempotente + + UnbindFromComponent(); + BoundComponent = Comp; + + Comp->OnAttributesChanged.AddDynamic(this, &UZMMOHudWidget::HandleAttributesChanged); + Comp->OnHpSpChanged.AddDynamic(this, &UZMMOHudWidget::HandleHpSpChanged); + Comp->OnLevelUp.AddDynamic(this, &UZMMOHudWidget::HandleLevelUp); + + // Refresh imediato — caso o snapshot ja' tenha chegado. + HandleAttributesChanged(Comp->GetSnapshot()); +} + +void UZMMOHudWidget::UnbindFromComponent() +{ + if (UZMMOAttributeComponent* Old = BoundComponent.Get()) + { + Old->OnAttributesChanged.RemoveDynamic(this, &UZMMOHudWidget::HandleAttributesChanged); + Old->OnHpSpChanged.RemoveDynamic(this, &UZMMOHudWidget::HandleHpSpChanged); + Old->OnLevelUp.RemoveDynamic(this, &UZMMOHudWidget::HandleLevelUp); + } + BoundComponent.Reset(); +} + +void UZMMOHudWidget::HandleAttributesChanged(const FZMMOAttributesSnapshot& Snapshot) +{ + // Propaga snapshot aos sub-widgets que ja existem. Em V1 so' HpSpBar. + if (HpSpBar) + { + HpSpBar->ApplySnapshot(Snapshot); + } + // Futuros: PlayerInfo->ApplySnapshot, Buffs->RefreshIcons, etc. +} + +void UZMMOHudWidget::HandleHpSpChanged(int32 Hp, int32 Sp) +{ + if (UZMMOAttributeComponent* Comp = BoundComponent.Get()) + { + const FZMMOAttributesSnapshot& Snap = Comp->GetSnapshot(); + if (HpSpBar) + { + HpSpBar->ApplyHpSp(Hp, Snap.MaxHp, Sp, Snap.MaxSp); + } + } +} + +void UZMMOHudWidget::HandleLevelUp(int32 NewBaseLevel, int32 StatusPointDelta) +{ + // Snapshot subsequente do AttributeSystem vai disparar HandleAttributesChanged + // e atualizar HpSpBar->LevelText. Aqui e' o ponto pra efeitos visuais + // instantaneos (toast "LEVEL UP", confetti, sound). V1 noop — UI pode + // hook via Blueprint depois. +} diff --git a/Source/ZMMO/Game/UI/InGame/ZMMOHudWidget.h b/Source/ZMMO/Game/UI/InGame/ZMMOHudWidget.h new file mode 100644 index 0000000..28d6de6 --- /dev/null +++ b/Source/ZMMO/Game/UI/InGame/ZMMOHudWidget.h @@ -0,0 +1,102 @@ +#pragma once + +#include "CommonActivatableWidget.h" +#include "CoreMinimal.h" +#include "ZMMOAttributeTypes.h" +#include "ZMMOHudWidget.generated.h" + +class UZMMOAttributeComponent; +class UZMMOHudHpSpWidget; + +/** + * Root da HUD de gameplay. UCommonActivatableWidget pushed pelo + * UUIInGameFlowSubsystem em UI.Layer.Game quando o estado vira `Playing`. + * + * **Vive em ZMMO core** (UI/InGame/) porque o HUD root e' generico pra + * qualquer GameType — sub-widgets vem dos modulos especializados + * (HpSpBar do sub-modulo ZMMOAttributes, Minimap de ZMMOMap futuro, + * Chat de ZMMOChat futuro, etc.). + * + * Padrao **composite**: o WBP_HUD (filho) declara as PECAS visuais como + * sub-widgets BindWidgetOptional, e este C++ orquestra: + * 1. Em NativeOnActivated: auto-binda no UZMMOAttributeComponent do + * player local (vive no PlayerState via Component Registry) + * 2. Subscreve aos delegates do componente (OnAttributesChanged, + * OnHpSpChanged, OnLevelUp) + * 3. Propaga os deltas pros sub-widgets via metodos publicos + * (HpSpBar->ApplySnapshot, HpSpBar->ApplyHpSp, etc.) + * + * Composicao prevista (sub-widgets — todos `BindWidgetOptional` pra + * permitir GameTypes sem modulos opcionais — ex.: Survival sem stats RO): + * - HpSpBar (UZMMOHudHpSpWidget — sub-modulo ZMMOAttributes) — impl + * - PlayerInfo (futuro — nome/job/level/exp bar) + * - Minimap (futuro) + * - QuickBar (futuro — hotkeys de skills) + * - Buffs (futuro — icones SC ativos) + * - Chat (futuro) + * - TargetInfo (futuro — HP/SP/nome do mob/player targetado) + * + * Adicionar novo sub-widget = adicionar campo BindWidgetOptional aqui + + * setup no NativeOnActivated/handlers + adicionar no WBP_HUD. + * + * Pra mudar o LAYOUT visual ou skin do HUD: edita o WBP_HUD (filho) + * no Designer — sem tocar este C++. + */ +UCLASS(Abstract, Blueprintable, BlueprintType) +class ZMMO_API UZMMOHudWidget : public UCommonActivatableWidget +{ + GENERATED_BODY() + +public: + UZMMOHudWidget(const FObjectInitializer& ObjectInitializer); + +protected: + virtual void NativeOnActivated() override; + virtual void NativeOnDeactivated() override; + virtual void NativeDestruct() override; + + /** + * HUD passa input direto pro Pawn — sem capturar mouse/teclado. Override + * retorna `TOptional(unset)`. Sem isso, UCommonActivatableWidget + * default usa Menu config (`SetInputMode_GameAndUI` + mouse cursor), o que + * desativa input de gameplay. + * + * Menus (StatusWindow, Inventory, etc.) override e' diferente — eles + * retornam GameAndMenu/Menu pra mostrar cursor + bloquear movement. + */ + virtual TOptional GetDesiredInputConfig() const override; + + /// Sub-widget HP/SP/level (sub-modulo ZMMOAttributes). BindWidgetOptional + /// — WBP_HUD pode omitir se o GameType nao usa Attributes (Survival, etc.). + UPROPERTY(BlueprintReadOnly, meta = (BindWidgetOptional), Category = "Zeus|HUD") + UZMMOHudHpSpWidget* HpSpBar = nullptr; + + // === Futuros sub-widgets === + // UPROPERTY(BlueprintReadOnly, meta = (BindWidgetOptional)) UZMMOHudPlayerInfoWidget* PlayerInfo = nullptr; + // UPROPERTY(BlueprintReadOnly, meta = (BindWidgetOptional)) UZMMOHudMinimapWidget* Minimap = nullptr; + // UPROPERTY(BlueprintReadOnly, meta = (BindWidgetOptional)) UZMMOHudQuickBarWidget* QuickBar = nullptr; + // UPROPERTY(BlueprintReadOnly, meta = (BindWidgetOptional)) UZMMOHudBuffsWidget* Buffs = nullptr; + // UPROPERTY(BlueprintReadOnly, meta = (BindWidgetOptional)) UZMMOHudChatWidget* Chat = nullptr; + // UPROPERTY(BlueprintReadOnly, meta = (BindWidgetOptional)) UZMMOHudTargetInfoWidget* TargetInfo = nullptr; + + UFUNCTION(BlueprintPure, Category = "Zeus|HUD") + UZMMOAttributeComponent* GetBoundComponent() const { return BoundComponent.Get(); } + +private: + /// Tenta achar o UZMMOAttributeComponent do player local + subscrever + /// nos delegates. Idempotente. + void BindToLocalPlayer(); + void UnbindFromComponent(); + + UFUNCTION() + void HandleAttributesChanged(const FZMMOAttributesSnapshot& Snapshot); + + UFUNCTION() + void HandleHpSpChanged(int32 Hp, int32 Sp); + + UFUNCTION() + void HandleLevelUp(int32 NewBaseLevel, int32 StatusPointDelta); + + UPROPERTY(Transient) + TWeakObjectPtr BoundComponent; +}; diff --git a/Source/ZMMOAttributes/Private/ZMMOAttributeNetworkHandler.cpp b/Source/ZMMOAttributes/Private/ZMMOAttributeNetworkHandler.cpp index 6b0543b..da21f17 100644 --- a/Source/ZMMOAttributes/Private/ZMMOAttributeNetworkHandler.cpp +++ b/Source/ZMMOAttributes/Private/ZMMOAttributeNetworkHandler.cpp @@ -2,8 +2,8 @@ #include "Engine/GameInstance.h" #include "Engine/World.h" -#include "EngineUtils.h" // TActorIterator -#include "GameFramework/Actor.h" +#include "GameFramework/GameStateBase.h" +#include "GameFramework/PlayerState.h" #include "ZMMOAttributeComponent.h" #include "ZMMOAttributeTypes.h" #include "ZeusNetworkSubsystem.h" @@ -45,25 +45,22 @@ namespace return S; } - // V1 — resolve por EntityId varrendo atores do mundo procurando um que - // tenha `UZMMOAttributeComponent`. Funciona para PIE com 1-2 players + - // proxies. Otimizacao futura: usar `UZMMOWorldSubsystem::GetActorByEntityId` - // (precisa expor) ou cache local de `EntityId -> UAttributeComponent*`. + // Resolve `EntityId -> UZMMOAttributeComponent` via PlayerArray do GameState. + // AttributeComponent agora vive no PlayerState (vide AZMMOPlayerState + + // Component Registry em DefaultGame.ini). Itera apenas players (1 por + // conexao), nao 1000+ atores — O(N_players). // - // Convencao: `IZMMOEntityInterface::GetZMMOEntityId()` retorna o EntityId - // autoritativo do servidor. Nao depender da interface aqui para manter - // ZMMOAttributes sem `#include` do modulo ZMMO core — varremos `Tags` - // alternativamente, mas o caminho mais simples e' a propria component - // keys de `EntityId` no proprio componente (TODO Fase 2: armazenar - // EntityId no componente quando snapshot chega). + // Cada UZMMOAttributeComponent carrega seu proprio EntityId (seed em + // AZMMOPlayerCharacter::HandleLocalSpawnReady ou no primeiro snapshot). UZMMOAttributeComponent* FindComponentForEntity(UWorld* World, int32 EntityId) { if (!World || EntityId == 0) { return nullptr; } - for (TActorIterator It(World); It; ++It) + const AGameStateBase* GS = World->GetGameState(); + if (!GS) { return nullptr; } + for (APlayerState* PS : GS->PlayerArray) { - AActor* Actor = *It; - if (!Actor) { continue; } - UZMMOAttributeComponent* Comp = Actor->FindComponentByClass(); + if (!PS) { continue; } + UZMMOAttributeComponent* Comp = PS->FindComponentByClass(); if (!Comp) { continue; } if (Comp->GetSnapshot().EntityId == EntityId) { @@ -136,21 +133,23 @@ void UZMMOAttributeNetworkHandler::HandleAttributeSnapshotFull(const FZeusAttrib UZMMOAttributeComponent* Comp = FindComponentForEntity(World, Payload.EntityId); if (!Comp) { - // V1 fallback: aplica ao primeiro UZMMOAttributeComponent encontrado - // (player local). Isso resolve o caso do primeiro snapshot chegar - // antes do `EntityId` estar registrado no componente (que so' acontece - // apos o primeiro snapshot — chicken-and-egg). Apos o primeiro apply, - // `Current.EntityId` ja' bate e o caminho normal funciona. - for (TActorIterator It(World); It; ++It) + // Fallback chicken-and-egg: primeiro snapshot chega antes do EntityId + // estar seeded no componente. Aplica ao primeiro componente do + // PlayerArray que ainda tem EntityId=0. Apos o primeiro apply, o + // caminho normal por lookup funciona. + const AGameStateBase* GS = World->GetGameState(); + if (GS) { - AActor* Actor = *It; - if (!Actor) { continue; } - Comp = Actor->FindComponentByClass(); - if (Comp && Comp->GetSnapshot().EntityId == 0) + for (APlayerState* PS : GS->PlayerArray) { - break; + if (!PS) { continue; } + UZMMOAttributeComponent* Candidate = PS->FindComponentByClass(); + if (Candidate && Candidate->GetSnapshot().EntityId == 0) + { + Comp = Candidate; + break; + } } - Comp = nullptr; } } if (Comp) diff --git a/Source/ZMMOAttributes/Private/ZMMOHudHpSpWidget.cpp b/Source/ZMMOAttributes/Private/ZMMOHudHpSpWidget.cpp index 2a67494..543ea80 100644 --- a/Source/ZMMOAttributes/Private/ZMMOHudHpSpWidget.cpp +++ b/Source/ZMMOAttributes/Private/ZMMOHudHpSpWidget.cpp @@ -3,99 +3,43 @@ #include "Components/ProgressBar.h" #include "Components/TextBlock.h" #include "Internationalization/Text.h" -#include "ZMMOAttributeComponent.h" -void UZMMOHudHpSpWidget::BindToAttributeComponent(UZMMOAttributeComponent* InComponent) -{ - if (UZMMOAttributeComponent* Old = BoundComponent.Get()) - { - Old->OnAttributesChanged.RemoveDynamic(this, &UZMMOHudHpSpWidget::HandleAttributesChanged); - Old->OnHpSpChanged.RemoveDynamic(this, &UZMMOHudHpSpWidget::HandleHpSpChanged); - } - - BoundComponent = InComponent; - - if (InComponent) - { - InComponent->OnAttributesChanged.AddDynamic(this, &UZMMOHudHpSpWidget::HandleAttributesChanged); - InComponent->OnHpSpChanged.AddDynamic(this, &UZMMOHudHpSpWidget::HandleHpSpChanged); - // Refresh imediato — caso o primeiro snapshot tenha chegado antes - // do widget existir. - HandleAttributesChanged(InComponent->GetSnapshot()); - } -} - -void UZMMOHudHpSpWidget::NativeDestruct() -{ - if (UZMMOAttributeComponent* Old = BoundComponent.Get()) - { - Old->OnAttributesChanged.RemoveDynamic(this, &UZMMOHudHpSpWidget::HandleAttributesChanged); - Old->OnHpSpChanged.RemoveDynamic(this, &UZMMOHudHpSpWidget::HandleHpSpChanged); - } - BoundComponent.Reset(); - Super::NativeDestruct(); -} - -void UZMMOHudHpSpWidget::HandleAttributesChanged(const FZMMOAttributesSnapshot& Snapshot) +void UZMMOHudHpSpWidget::ApplySnapshot(const FZMMOAttributesSnapshot& Snapshot) { + LastSnapshot = Snapshot; OnSnapshotApplied(Snapshot); } -void UZMMOHudHpSpWidget::HandleHpSpChanged(int32 Hp, int32 Sp) +void UZMMOHudHpSpWidget::ApplyHpSp(int32 Hp, int32 MaxHp, int32 Sp, int32 MaxSp) { - OnHpSpDelta(Hp, Sp); + LastSnapshot.Hp = Hp; + LastSnapshot.MaxHp = MaxHp; + LastSnapshot.Sp = Sp; + LastSnapshot.MaxSp = MaxSp; + + if (HpBar) + { + HpBar->SetPercent(MaxHp > 0 ? static_cast(Hp) / static_cast(MaxHp) : 0.f); + } + if (SpBar) + { + SpBar->SetPercent(MaxSp > 0 ? static_cast(Sp) / static_cast(MaxSp) : 0.f); + } + if (HpText) + { + HpText->SetText(FText::FromString(FString::Printf(TEXT("%d / %d"), Hp, MaxHp))); + } + if (SpText) + { + SpText->SetText(FText::FromString(FString::Printf(TEXT("%d / %d"), Sp, MaxSp))); + } } void UZMMOHudHpSpWidget::OnSnapshotApplied_Implementation(const FZMMOAttributesSnapshot& Snapshot) { - if (HpBar) - { - HpBar->SetPercent(Snapshot.MaxHp > 0 - ? static_cast(Snapshot.Hp) / static_cast(Snapshot.MaxHp) : 0.f); - } - if (SpBar) - { - SpBar->SetPercent(Snapshot.MaxSp > 0 - ? static_cast(Snapshot.Sp) / static_cast(Snapshot.MaxSp) : 0.f); - } - if (HpText) - { - HpText->SetText(FText::FromString(FString::Printf(TEXT("%d / %d"), Snapshot.Hp, Snapshot.MaxHp))); - } - if (SpText) - { - SpText->SetText(FText::FromString(FString::Printf(TEXT("%d / %d"), Snapshot.Sp, Snapshot.MaxSp))); - } + ApplyHpSp(Snapshot.Hp, Snapshot.MaxHp, Snapshot.Sp, Snapshot.MaxSp); if (LevelText) { LevelText->SetText(FText::FromString(FString::Printf(TEXT("Lv %d"), Snapshot.BaseLevel))); } } - -void UZMMOHudHpSpWidget::OnHpSpDelta_Implementation(int32 Hp, int32 Sp) -{ - // Atualiza apenas os campos vitais (evita refresh redundante de - // stats/level que nao mudaram). - if (UZMMOAttributeComponent* Comp = BoundComponent.Get()) - { - const FZMMOAttributesSnapshot& Snap = Comp->GetSnapshot(); - if (HpBar) - { - HpBar->SetPercent(Snap.MaxHp > 0 - ? static_cast(Hp) / static_cast(Snap.MaxHp) : 0.f); - } - if (SpBar) - { - SpBar->SetPercent(Snap.MaxSp > 0 - ? static_cast(Sp) / static_cast(Snap.MaxSp) : 0.f); - } - if (HpText) - { - HpText->SetText(FText::FromString(FString::Printf(TEXT("%d / %d"), Hp, Snap.MaxHp))); - } - if (SpText) - { - SpText->SetText(FText::FromString(FString::Printf(TEXT("%d / %d"), Sp, Snap.MaxSp))); - } - } -} diff --git a/Source/ZMMOAttributes/Public/ZMMOHudHpSpWidget.h b/Source/ZMMOAttributes/Public/ZMMOHudHpSpWidget.h index 735b186..2713625 100644 --- a/Source/ZMMOAttributes/Public/ZMMOHudHpSpWidget.h +++ b/Source/ZMMOAttributes/Public/ZMMOHudHpSpWidget.h @@ -7,21 +7,27 @@ class UProgressBar; class UTextBlock; -class UZMMOAttributeComponent; /** - * Widget base do HUD de atributos (HP/SP/level). Spawnada por - * `AZMMOPlayerCharacter::BeginPlay` quando o pawn local virou autoritativo. + * Sub-widget do HUD: barras de HP/SP + label de level. Sem auto-bind no + * AttributeComponent — o parent (`UZMMOHudWidget`) propaga snapshots via + * `ApplySnapshot` / `ApplyHpSpDelta` quando o player local recebe updates. * - * Layout (cor, textos, ancoras) vem do WBP filho via UMG. - * Comportamento (binding com AttributeComponent + refresh on delegate) - * vive aqui em C++ — permite que outros modulos consumam esta API - * sem precisar abrir o WBP. Sub-classe via Blueprint apenas para skin - * visual e disposicao dos sub-widgets. + * Composicao do HUD (ver `UZMMOHudWidget`): + * WBP_HUD (UZMMOHudWidget root) + * |-- HpSpBar (UZMMOHudHpSpWidget — este) + * |-- PlayerInfo (futuro — nome/job/level) + * |-- Minimap (futuro) + * |-- QuickBar (futuro — hotkeys) + * |-- Buffs (futuro — icones SC) + * |-- Chat (futuro) + * |-- TargetInfo (futuro — mob/player targetado) * - * Convencao `meta=(BindWidget)`: o WBP filho DEVE ter widgets com os - * mesmos nomes (HpBar, SpBar). `BindWidgetOptional` permite ao WBP - * omitir o widget (ex.: HUD compacto sem texto numerico). + * Cada sub-widget tem responsabilidade unica (SRP). O parent (UZMMOHudWidget) + * faz binding com AttributeComponent e propaga deltas. + * + * Convencao `meta=(BindWidget)`: o WBP filho deve ter widgets com nomes + * exatos (HpBar, SpBar). `BindWidgetOptional` permite omitir. */ UCLASS(Abstract, Blueprintable, BlueprintType) class ZMMOATTRIBUTES_API UZMMOHudHpSpWidget : public UUserWidget @@ -29,34 +35,24 @@ class ZMMOATTRIBUTES_API UZMMOHudHpSpWidget : public UUserWidget GENERATED_BODY() public: - /** - * Liga o widget a um `UZMMOAttributeComponent`. Subscreve aos delegates - * e faz um refresh imediato com o snapshot corrente. Idempotente — - * chamar com outro componente desliga do anterior. - * Se `InComponent` for nullptr, desliga sem religar. - */ + /** Atualiza todos os campos a partir do snapshot. Chamado pelo UZMMOHudWidget. */ UFUNCTION(BlueprintCallable, Category = "Zeus|HUD") - void BindToAttributeComponent(UZMMOAttributeComponent* InComponent); + void ApplySnapshot(const FZMMOAttributesSnapshot& Snapshot); - UFUNCTION(BlueprintPure, Category = "Zeus|HUD") - UZMMOAttributeComponent* GetBoundComponent() const { return BoundComponent.Get(); } + /** Atualiza apenas HP/SP (sem mexer no level/text). Otimizacao do tick. */ + UFUNCTION(BlueprintCallable, Category = "Zeus|HUD") + void ApplyHpSp(int32 Hp, int32 MaxHp, int32 Sp, int32 MaxSp); protected: - virtual void NativeDestruct() override; - /** * Hook BlueprintNativeEvent para skin visual reagir a um snapshot novo - * (cores, animacao de level up, etc.). Implementacao default em C++ - * apenas atualiza progress bars + textos via `BindWidget`/`BindWidgetOptional`. + * (cores, animacao de level up, etc.). Impl default em C++ apenas + * atualiza progress bars + textos via BindWidget/BindWidgetOptional. */ UFUNCTION(BlueprintNativeEvent, Category = "Zeus|HUD") void OnSnapshotApplied(const FZMMOAttributesSnapshot& Snapshot); virtual void OnSnapshotApplied_Implementation(const FZMMOAttributesSnapshot& Snapshot); - UFUNCTION(BlueprintNativeEvent, Category = "Zeus|HUD") - void OnHpSpDelta(int32 Hp, int32 Sp); - virtual void OnHpSpDelta_Implementation(int32 Hp, int32 Sp); - // === BindWidget — devem existir no WBP com EXATAMENTE estes nomes === UPROPERTY(BlueprintReadOnly, meta = (BindWidget), Category = "Zeus|HUD") UProgressBar* HpBar = nullptr; @@ -74,12 +70,7 @@ protected: UTextBlock* LevelText = nullptr; private: - UFUNCTION() - void HandleAttributesChanged(const FZMMOAttributesSnapshot& Snapshot); - - UFUNCTION() - void HandleHpSpChanged(int32 Hp, int32 Sp); - - UPROPERTY() - TWeakObjectPtr BoundComponent; + /// Ultimo snapshot recebido (cache pra ApplyHpSp manter MaxHp/MaxSp). */ + UPROPERTY(Transient) + FZMMOAttributesSnapshot LastSnapshot; }; diff --git a/Source/ZMMOAttributes/ZMMOAttributes.Build.cs b/Source/ZMMOAttributes/ZMMOAttributes.Build.cs index d42700a..85eba18 100644 --- a/Source/ZMMOAttributes/ZMMOAttributes.Build.cs +++ b/Source/ZMMOAttributes/ZMMOAttributes.Build.cs @@ -12,6 +12,8 @@ public class ZMMOAttributes : ModuleRules "Engine", "UMG", "Slate", + "CommonUI", + "CommonInput", "ZeusNetwork" }); diff --git a/Source/ZMMOAttributes/module.json b/Source/ZMMOAttributes/module.json index ad8b1a9..73f678f 100644 --- a/Source/ZMMOAttributes/module.json +++ b/Source/ZMMOAttributes/module.json @@ -4,7 +4,7 @@ "version": "0.1.0", "side": "client", "dependencies": { - "engine": ["Core", "CoreUObject", "Engine", "UMG", "Slate"], + "engine": ["Core", "CoreUObject", "Engine", "UMG", "Slate", "CommonUI", "CommonInput"], "plugins": ["ZeusNetwork"], "modules": [] },