feat(ui-ingame): UI in-game system + PlayerState + Component Registry + HUD composite

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<EZMMOInGameUIState, TSoftClassPtr<...>>
- 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<TSubclassOf<UActorComponent>> 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 <noreply@anthropic.com>
This commit is contained in:
2026-05-23 05:27:41 -03:00
parent 7d83dd800f
commit 396223e2a8
25 changed files with 1029 additions and 242 deletions

View File

@@ -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<AActor> 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<UZMMOAttributeComponent>();
if (!PS) { continue; }
UZMMOAttributeComponent* Comp = PS->FindComponentByClass<UZMMOAttributeComponent>();
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<AActor> 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<UZMMOAttributeComponent>();
if (Comp && Comp->GetSnapshot().EntityId == 0)
for (APlayerState* PS : GS->PlayerArray)
{
break;
if (!PS) { continue; }
UZMMOAttributeComponent* Candidate = PS->FindComponentByClass<UZMMOAttributeComponent>();
if (Candidate && Candidate->GetSnapshot().EntityId == 0)
{
Comp = Candidate;
break;
}
}
Comp = nullptr;
}
}
if (Comp)