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 0000000..b3bc8f5 Binary files /dev/null and b/Content/ZMMO/UI/HUD/WBP_HUD.uasset differ diff --git a/Content/ZMMO/UI/HUD/WBP_HUD_HpSpBar.uasset b/Content/ZMMO/UI/HUD/WBP_HUD_HpSpBar.uasset index f16ba4f..eb05158 100644 Binary files a/Content/ZMMO/UI/HUD/WBP_HUD_HpSpBar.uasset and b/Content/ZMMO/UI/HUD/WBP_HUD_HpSpBar.uasset differ diff --git a/Content/ZMMO/UI/InGame/DA_InGameScreenSet.uasset b/Content/ZMMO/UI/InGame/DA_InGameScreenSet.uasset new file mode 100644 index 0000000..16bcec6 Binary files /dev/null and b/Content/ZMMO/UI/InGame/DA_InGameScreenSet.uasset differ diff --git a/Source/ZMMO/Data/UI/InGameTypes.h b/Source/ZMMO/Data/UI/InGameTypes.h new file mode 100644 index 0000000..d5bc63c --- /dev/null +++ b/Source/ZMMO/Data/UI/InGameTypes.h @@ -0,0 +1,34 @@ +#pragma once + +#include "CoreMinimal.h" +#include "InGameTypes.generated.h" + +/** + * Estados da UI in-game. Dirigidos pelo UUIInGameFlowSubsystem; cada estado + * resolve uma tela (UCommonActivatableWidget) via DA_InGameScreenSet. + * + * Espelha simetricamente EZMMOFrontEndState (Data/UI/FrontEndTypes.h): + * - Front-end usa estados pra Login/ServerSelect/Lobby (UI.Layer.Menu) + * - In-game usa estados pra HUD/Status/Inventory/Skills (UI.Layer.Game + GameMenu) + * + * Convencao de camadas (resolvida pelo subsystem): + * - Playing -> 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": [] },