Files
ZMMO/Source/ZMMOAttributes/Private/ZMMOHudHpSpWidget.cpp
Mateus Rodrigues 7d83dd800f feat(attributes): cliente Fase 1 — sub-modulo ZMMOAttributes + HUD HP/SP
Espelho do AttributeSystem do servidor (Game/MMO/Modules/AttributeSystem).
Sub-modulo C++ Source/ZMMOAttributes/ (Runtime, LoadingPhase=PreDefault)
isolando o cliente do AttributeSystem em pasta propria — pavimentando o
caminho para futuros Inventory/Skills/Combat virem como sub-modulos peer.

Componentes:
  - FZMMOAttributesSnapshot (USTRUCT Blueprint, espelha
    FZeusAttributesPayload do plugin ZeusNetwork)
  - UZMMOAttributeComponent (UActorComponent ligado ao player):
    ApplySnapshot / ApplyHpSpUpdate / NotifyLevelUp + 3 delegates
    dinamicos (OnAttributesChanged / OnHpSpChanged / OnLevelUp) +
    helpers GetHpRatio / GetSpRatio
  - UZMMOAttributeNetworkHandler (UWorldSubsystem): ponte
    UZeusNetworkSubsystem -> componente via lookup por EntityId
  - UZMMOHudHpSpWidget (UUserWidget base): BindWidget (HpBar, SpBar) +
    BindWidgetOptional (HpText, SpText, LevelText). NativeConstruct
    binda nos delegates do componente; NativeDestruct desliga.
    OnSnapshotApplied / OnHpSpDelta (BlueprintNativeEvent) com impl
    C++ atualizando progress bars e textos.

Integracao no PlayerCharacter:
  - CreateDefaultSubobject<UZMMOAttributeComponent> no ctor
  - HudHpSpWidgetClass (TSubclassOf) resolvido via
    ConstructorHelpers::FClassFinder em /Game/ZMMO/UI/HUD/WBP_HUD_HpSpBar
  - HandleLocalSpawnReady seed do EntityId no componente + spawn do HUD
    (CreateWidget + AddToViewport + BindToAttributeComponent)
  - EndPlay remove widget do parent

WBP_HUD_HpSpBar (UMG, criado via MCP):
  - Overlay root + SizeBox (320x120) + VerticalBox
  - 5 widgets BindWidget: LevelText, HpBar (vermelho), HpText,
    SpBar (azul), SpText
  - Herda de UZMMOHudHpSpWidget

Verificacao (smoke test):
  - PIE login -> server envia S_ATTRIBUTE_SNAPSHOT_FULL
  - Cliente recebe via OnAttributeSnapshotFull -> handler roteia pra
    componente do entity -> ApplySnapshot dispara OnAttributesChanged ->
    widget atualiza ProgressBars + TextBlocks
  - HUD aparece no canto inferior esquerdo da tela com valores
    correspondentes ao DB

Refs:
  - Plano: C:/Users/mateu/.claude/plans/temos-uma-lab-input-nested-diffie.md
  - Server companion commit: feat(attributes): AttributeSystem modular

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-23 04:00:32 -03:00

102 lines
2.9 KiB
C++

#include "ZMMOHudHpSpWidget.h"
#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)
{
OnSnapshotApplied(Snapshot);
}
void UZMMOHudHpSpWidget::HandleHpSpChanged(int32 Hp, int32 Sp)
{
OnHpSpDelta(Hp, Sp);
}
void UZMMOHudHpSpWidget::OnSnapshotApplied_Implementation(const FZMMOAttributesSnapshot& Snapshot)
{
if (HpBar)
{
HpBar->SetPercent(Snapshot.MaxHp > 0
? static_cast<float>(Snapshot.Hp) / static_cast<float>(Snapshot.MaxHp) : 0.f);
}
if (SpBar)
{
SpBar->SetPercent(Snapshot.MaxSp > 0
? static_cast<float>(Snapshot.Sp) / static_cast<float>(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)));
}
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<float>(Hp) / static_cast<float>(Snap.MaxHp) : 0.f);
}
if (SpBar)
{
SpBar->SetPercent(Snap.MaxSp > 0
? static_cast<float>(Sp) / static_cast<float>(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)));
}
}
}