feat(ui/hud): painel de vitais (Nome/Job/Level + HP/SP/EXP) substituindo HpSpBar

UZMMOHudPlayerVitalsWidget (novo): sub-widget do HUD com 4 UUIProgressBar_Base
(HP/SP/BASE EXP/JOB EXP) + nome/classe/level. ApplySnapshot anima HP/SP via
SetTargetPrimaryLevel e formata os readouts; ApplyHpSp pro tick rapido.

UZMMOHudWidget propaga o snapshot pro PlayerVitals e resolve identidade: nome
do AZMMOPlayerState.CharName + job via UZMMOJobsLibrary.GetJobDisplayName(ClassId).
HpSpBar legado removido do WBP_HUD.

WBP_PlayerVitalsPanel: layout compacto (520w) com colunas alinhadas (label 28 /
bar FILL / readout 116 right-align) independente do tamanho do numero. Sem portrait.

MIs por barra: UI_M_ProgressBar_SP/BaseEXP/JobEXP/Loading (cores do
hud_layout_modal_New.html).

EXP/JOB readout mostra exp absoluto por ora — barras de EXP aguardam o servidor
enviar exp-to-next no snapshot pra calcular a porcentagem.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-05-28 00:34:03 -03:00
parent c9492a3910
commit e1fca1ae8d
10 changed files with 220 additions and 2 deletions

Binary file not shown.

Binary file not shown.

View File

@@ -0,0 +1,96 @@
#include "ZMMOHudPlayerVitalsWidget.h"
#include "Components/TextBlock.h"
#include "UI/Widgets/UIProgressBar_Base.h"
#include "Internationalization/Text.h"
namespace
{
/** Formata int com separador de milhar do locale (ex.: 9840 -> "9,840"). */
FText FormatNumber(int64 Value)
{
return FText::AsNumber(Value);
}
float SafeRatio(int32 Current, int32 Max)
{
return Max > 0 ? FMath::Clamp(static_cast<float>(Current) / static_cast<float>(Max), 0.f, 1.f) : 0.f;
}
}
void UZMMOHudPlayerVitalsWidget::ApplySnapshot(const FZMMOAttributesSnapshot& Snapshot)
{
LastSnapshot = Snapshot;
// HP / SP (pool efetivo — % real)
ApplyHpSp(Snapshot.Hp, Snapshot.MaxHp, Snapshot.Sp, Snapshot.MaxSp);
// Level
if (LvlText)
{
LvlText->SetText(FText::FromString(FString::Printf(TEXT("LVL %d"), Snapshot.BaseLevel)));
}
// BASE / JOB EXP — sem "exp-to-next" no snapshot. Readout mostra valor
// absoluto acumulado; as barras ficam pra quando a curva de exp existir.
if (ExpReadout)
{
ExpReadout->SetText(FormatNumber(Snapshot.BaseExp));
}
if (JobReadout)
{
JobReadout->SetText(FormatNumber(Snapshot.JobExp));
}
OnSnapshotApplied(Snapshot);
}
void UZMMOHudPlayerVitalsWidget::ApplyHpSp(int32 Hp, int32 MaxHp, int32 Sp, int32 MaxSp)
{
LastSnapshot.Hp = Hp;
LastSnapshot.MaxHp = MaxHp;
LastSnapshot.Sp = Sp;
LastSnapshot.MaxSp = MaxSp;
if (HpBar)
{
HpBar->SetTargetPrimaryLevel(SafeRatio(Hp, MaxHp));
}
if (SpBar)
{
SpBar->SetTargetPrimaryLevel(SafeRatio(Sp, MaxSp));
}
if (HpReadout)
{
HpReadout->SetText(FText::Format(
NSLOCTEXT("ZMMOHud", "HpSpReadout", "{0}/{1}"),
FormatNumber(Hp), FormatNumber(MaxHp)));
}
if (SpReadout)
{
SpReadout->SetText(FText::Format(
NSLOCTEXT("ZMMOHud", "HpSpReadout", "{0}/{1}"),
FormatNumber(Sp), FormatNumber(MaxSp)));
}
}
void UZMMOHudPlayerVitalsWidget::SetCharName(const FString& InName)
{
if (NameText)
{
NameText->SetText(FText::FromString(InName));
}
}
void UZMMOHudPlayerVitalsWidget::SetClassName(const FString& InClassName)
{
if (ClassText)
{
ClassText->SetText(FText::FromString(InClassName));
}
}
void UZMMOHudPlayerVitalsWidget::OnSnapshotApplied_Implementation(const FZMMOAttributesSnapshot& /*Snapshot*/)
{
// Default vazio — skin do WBP estende se quiser (low-HP flash, etc.).
}

View File

@@ -0,0 +1,92 @@
#pragma once
#include "CommonUserWidget.h"
#include "CoreMinimal.h"
#include "ZMMOAttributeTypes.h"
#include "ZMMOHudPlayerVitalsWidget.generated.h"
class UUIProgressBar_Base;
class UTextBlock;
/**
* Sub-widget do HUD: painel de vitais do player (portrait + nome/job/level +
* barras HP/SP/BASE EXP/JOB EXP). Sem auto-bind no AttributeComponent — o
* parent (UZMMOHudWidget) propaga snapshots via ApplySnapshot/ApplyHpSp.
*
* Barras são UUIProgressBar_Base (material custom Primary/Secondary). HP/SP
* têm pool efetivo no snapshot → % real. BASE/JOB EXP só têm exp acumulado
* (sem "exp-to-next") — barras de EXP ficam pra quando a curva existir;
* por ora o readout mostra o valor absoluto.
*
* WBP filho (WBP_PlayerVitalsPanel) deve ter widgets com EXATAMENTE estes
* nomes: HpBar, SpBar (obrigatórios); ExpBar, JobBar, NameText, ClassText,
* LvlText, HpReadout, SpReadout, ExpReadout, JobReadout (opcionais).
*/
UCLASS(Abstract, Blueprintable, BlueprintType)
class ZMMO_API UZMMOHudPlayerVitalsWidget : public UCommonUserWidget
{
GENERATED_BODY()
public:
/** Atualiza todos os campos a partir do snapshot. Chamado pelo UZMMOHudWidget. */
UFUNCTION(BlueprintCallable, Category = "Zeus|HUD")
void ApplySnapshot(const FZMMOAttributesSnapshot& Snapshot);
/** Atualiza apenas HP/SP (otimização do tick — sem mexer em level/exp). */
UFUNCTION(BlueprintCallable, Category = "Zeus|HUD")
void ApplyHpSp(int32 Hp, int32 MaxHp, int32 Sp, int32 MaxSp);
/** Nome do char — vem do PlayerState (não está no snapshot). */
UFUNCTION(BlueprintCallable, Category = "Zeus|HUD")
void SetCharName(const FString& InName);
/** Nome do job/classe — vem do JobsDatabase via ClassId (não é string no snapshot). */
UFUNCTION(BlueprintCallable, Category = "Zeus|HUD")
void SetClassName(const FString& InClassName);
protected:
/** Hook BP pra skin reagir (cores low-HP, animações, etc.). */
UFUNCTION(BlueprintNativeEvent, Category = "Zeus|HUD")
void OnSnapshotApplied(const FZMMOAttributesSnapshot& Snapshot);
virtual void OnSnapshotApplied_Implementation(const FZMMOAttributesSnapshot& Snapshot);
// === BindWidget — barras (HP/SP obrigatórias) ===
UPROPERTY(BlueprintReadOnly, meta = (BindWidget), Category = "Zeus|HUD")
TObjectPtr<UUIProgressBar_Base> HpBar;
UPROPERTY(BlueprintReadOnly, meta = (BindWidget), Category = "Zeus|HUD")
TObjectPtr<UUIProgressBar_Base> SpBar;
UPROPERTY(BlueprintReadOnly, meta = (BindWidgetOptional), Category = "Zeus|HUD")
TObjectPtr<UUIProgressBar_Base> ExpBar;
UPROPERTY(BlueprintReadOnly, meta = (BindWidgetOptional), Category = "Zeus|HUD")
TObjectPtr<UUIProgressBar_Base> JobBar;
// === BindWidget — textos (todos opcionais) ===
UPROPERTY(BlueprintReadOnly, meta = (BindWidgetOptional), Category = "Zeus|HUD")
TObjectPtr<UTextBlock> NameText;
UPROPERTY(BlueprintReadOnly, meta = (BindWidgetOptional), Category = "Zeus|HUD")
TObjectPtr<UTextBlock> ClassText;
UPROPERTY(BlueprintReadOnly, meta = (BindWidgetOptional), Category = "Zeus|HUD")
TObjectPtr<UTextBlock> LvlText;
UPROPERTY(BlueprintReadOnly, meta = (BindWidgetOptional), Category = "Zeus|HUD")
TObjectPtr<UTextBlock> HpReadout;
UPROPERTY(BlueprintReadOnly, meta = (BindWidgetOptional), Category = "Zeus|HUD")
TObjectPtr<UTextBlock> SpReadout;
UPROPERTY(BlueprintReadOnly, meta = (BindWidgetOptional), Category = "Zeus|HUD")
TObjectPtr<UTextBlock> ExpReadout;
UPROPERTY(BlueprintReadOnly, meta = (BindWidgetOptional), Category = "Zeus|HUD")
TObjectPtr<UTextBlock> JobReadout;
private:
/** Último snapshot recebido (cache pra ApplyHpSp manter MaxHp/MaxSp). */
UPROPERTY(Transient)
FZMMOAttributesSnapshot LastSnapshot;
};

View File

@@ -10,6 +10,9 @@
#include "ZMMO.h"
#include "ZMMOAttributeComponent.h"
#include "ZMMOHudHpSpWidget.h"
#include "ZMMOHudPlayerVitalsWidget.h"
#include "ZMMOJobsLibrary.h"
#include "ZMMOPlayerState.h"
UZMMOHudWidget::UZMMOHudWidget(const FObjectInitializer& ObjectInitializer)
: Super(ObjectInitializer)
@@ -98,12 +101,28 @@ void UZMMOHudWidget::UnbindFromComponent()
void UZMMOHudWidget::HandleAttributesChanged(const FZMMOAttributesSnapshot& Snapshot)
{
// Propaga snapshot aos sub-widgets que ja existem. Em V1 so' HpSpBar.
// Propaga snapshot aos sub-widgets que ja existem.
if (HpSpBar)
{
HpSpBar->ApplySnapshot(Snapshot);
}
// Futuros: PlayerInfo->ApplySnapshot, Buffs->RefreshIcons, etc.
if (PlayerVitals)
{
PlayerVitals->ApplySnapshot(Snapshot);
// Identidade: nome vem do PlayerState (não está no snapshot); job vem
// do JobsSubsystem resolvendo ClassId -> display name.
if (UZMMOAttributeComponent* Comp = BoundComponent.Get())
{
if (const AZMMOPlayerState* PS = Cast<AZMMOPlayerState>(Comp->GetOwner()))
{
PlayerVitals->SetCharName(PS->CharName);
}
}
PlayerVitals->SetClassName(
UZMMOJobsLibrary::GetJobDisplayName(this, Snapshot.ClassId).ToString());
}
// Futuros: Buffs->RefreshIcons, etc.
}
void UZMMOHudWidget::HandleHpSpChanged(int32 Hp, int32 Sp)
@@ -115,6 +134,10 @@ void UZMMOHudWidget::HandleHpSpChanged(int32 Hp, int32 Sp)
{
HpSpBar->ApplyHpSp(Hp, Snap.MaxHp, Sp, Snap.MaxSp);
}
if (PlayerVitals)
{
PlayerVitals->ApplyHpSp(Hp, Snap.MaxHp, Sp, Snap.MaxSp);
}
}
}

View File

@@ -10,6 +10,7 @@ class UTextBlock;
class UWidget;
class UZMMOAttributeComponent;
class UZMMOHudHpSpWidget;
class UZMMOHudPlayerVitalsWidget;
/**
* Root da HUD de gameplay. UCommonActivatableWidget pushed pelo
@@ -74,6 +75,12 @@ protected:
UPROPERTY(BlueprintReadOnly, meta = (BindWidgetOptional), Category = "Zeus|HUD")
UZMMOHudHpSpWidget* HpSpBar = nullptr;
/// Painel de vitais novo (portrait + nome/job/level + HP/SP/EXP/JOB bars).
/// Substitui o HpSpBar legado. BindWidgetOptional — coexiste durante a
/// migração; o WBP_HUD pode ter um ou outro.
UPROPERTY(BlueprintReadOnly, meta = (BindWidgetOptional), Category = "Zeus|HUD")
UZMMOHudPlayerVitalsWidget* PlayerVitals = nullptr;
/// Container do toast "LEVEL UP!" (Fase 3). Hidden por default; em
/// `HandleLevelUp` vira visivel por `LevelUpToastDurationSec` segundos
/// (ver `Show/HideLevelUpToast`). WBP_HUD coloca um Border + Image + TextBlock