2 Commits

Author SHA1 Message Date
ede49c1777 feat(ui/hud): barras EXP/JOB em % via exp-to-next do snapshot
FZMMOAttributesSnapshot ganha BaseExpToNext/JobExpToNext espelhando o wire do
server. ApplySnapshot liga ExpBar/JobBar pelo ratio (exp / exp-to-next) e os
readouts mostram a porcentagem (ou "MAX" no cap).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-28 03:58:33 -03:00
484c8d5d4f feat(ui/hud): readouts HP/SP + level multi-cor via RichText (DT_UI_VitalsReadout)
Readouts e LvlText do PlayerVitals viram RichTextBlock com markup inline,
estilizados pelo DT_UI_VitalsReadout (fonte JetBrains Mono Bold):
- HP/SP: atual (branco) + "/" (sep, #3a3830) + max (#6c6860)
- LVL: "LVL " (#b8b3a8) + numero (num, gold-bright #ffd888, maior)

Cores espelham hud_layout_modal_New.html (text-1/2/3/4 + gold-bright).
UZMMOHudPlayerVitalsWidget: HpReadout/SpReadout/ExpReadout/JobReadout/LvlText
agora URichTextBlock; ApplyHpSp/ApplySnapshot montam o markup.

EXP/JOB readout segue exp absoluto (sem %) ate o server enviar exp-to-next.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-28 03:06:44 -03:00
6 changed files with 58 additions and 25 deletions

Binary file not shown.

View File

@@ -1,21 +1,33 @@
#include "ZMMOHudPlayerVitalsWidget.h" #include "ZMMOHudPlayerVitalsWidget.h"
#include "Components/RichTextBlock.h"
#include "Components/TextBlock.h" #include "Components/TextBlock.h"
#include "UI/Widgets/UIProgressBar_Base.h" #include "UI/Widgets/UIProgressBar_Base.h"
#include "Internationalization/Text.h" #include "Internationalization/Text.h"
namespace 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) 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; return Max > 0 ? FMath::Clamp(static_cast<float>(Current) / static_cast<float>(Max), 0.f, 1.f) : 0.f;
} }
/** Ratio 0..1 pras barras de EXP/JOB (int64). ToNext<=0 = cap -> barra cheia. */
float ExpRatio(int64 Cur, int64 ToNext)
{
return ToNext > 0
? FMath::Clamp(static_cast<float>(static_cast<double>(Cur) / static_cast<double>(ToNext)), 0.f, 1.f)
: 1.f;
}
/** Readout de progressao em "%": numero (Default) + "%" (unit/cinza).
* Cap (ToNext<=0) -> "MAX". Markup resolve no DT_UI_VitalsReadout. */
FText ExpReadoutText(int64 Cur, int64 ToNext)
{
if (ToNext <= 0) { return FText::FromString(TEXT("MAX")); }
const double Pct = static_cast<double>(Cur) / static_cast<double>(ToNext) * 100.0;
return FText::FromString(FString::Printf(TEXT("%.2f<unit>%%</>"), Pct));
}
} }
void UZMMOHudPlayerVitalsWidget::ApplySnapshot(const FZMMOAttributesSnapshot& Snapshot) void UZMMOHudPlayerVitalsWidget::ApplySnapshot(const FZMMOAttributesSnapshot& Snapshot)
@@ -25,21 +37,31 @@ void UZMMOHudPlayerVitalsWidget::ApplySnapshot(const FZMMOAttributesSnapshot& Sn
// HP / SP (pool efetivo — % real) // HP / SP (pool efetivo — % real)
ApplyHpSp(Snapshot.Hp, Snapshot.MaxHp, Snapshot.Sp, Snapshot.MaxSp); ApplyHpSp(Snapshot.Hp, Snapshot.MaxHp, Snapshot.Sp, Snapshot.MaxSp);
// Level // Level — markup: "LVL " (pct/cinza) + número (num/gold-bright maior)
if (LvlText) if (LvlText)
{ {
LvlText->SetText(FText::FromString(FString::Printf(TEXT("LVL %d"), Snapshot.BaseLevel))); LvlText->SetText(FText::FromString(
FString::Printf(TEXT("<pct>LVL </><num>%d</>"), Snapshot.BaseLevel)));
} }
// BASE / JOB EXP — sem "exp-to-next" no snapshot. Readout mostra valor // BASE / JOB EXP — barras em % (exp atual / exp-to-next, resolvido pelo
// absoluto acumulado; as barras ficam pra quando a curva de exp existir. // server via JobsDatabase). Em cap (ToNext=0) a barra enche e o readout
// vira "MAX". Readout exibe a porcentagem (numero + "%" cinza).
if (ExpBar)
{
ExpBar->SetTargetPrimaryLevel(ExpRatio(Snapshot.BaseExp, Snapshot.BaseExpToNext));
}
if (JobBar)
{
JobBar->SetTargetPrimaryLevel(ExpRatio(Snapshot.JobExp, Snapshot.JobExpToNext));
}
if (ExpReadout) if (ExpReadout)
{ {
ExpReadout->SetText(FormatNumber(Snapshot.BaseExp)); ExpReadout->SetText(ExpReadoutText(Snapshot.BaseExp, Snapshot.BaseExpToNext));
} }
if (JobReadout) if (JobReadout)
{ {
JobReadout->SetText(FormatNumber(Snapshot.JobExp)); JobReadout->SetText(ExpReadoutText(Snapshot.JobExp, Snapshot.JobExpToNext));
} }
OnSnapshotApplied(Snapshot); OnSnapshotApplied(Snapshot);
@@ -60,17 +82,20 @@ void UZMMOHudPlayerVitalsWidget::ApplyHpSp(int32 Hp, int32 MaxHp, int32 Sp, int3
{ {
SpBar->SetTargetPrimaryLevel(SafeRatio(Sp, MaxSp)); SpBar->SetTargetPrimaryLevel(SafeRatio(Sp, MaxSp));
} }
// Markup RichText: atual (Default/branco) + "/" (sep/escuro) + max (max/cinza).
// Tags resolvem no DT_UI_VitalsReadout (TextStyleSet do RichTextBlock).
auto MakeReadout = [](int32 Cur, int32 Max) -> FText
{
return FText::FromString(FString::Printf(TEXT("%s<sep>/</><max>%s</>"),
*FText::AsNumber(Cur).ToString(), *FText::AsNumber(Max).ToString()));
};
if (HpReadout) if (HpReadout)
{ {
HpReadout->SetText(FText::Format( HpReadout->SetText(MakeReadout(Hp, MaxHp));
NSLOCTEXT("ZMMOHud", "HpSpReadout", "{0}/{1}"),
FormatNumber(Hp), FormatNumber(MaxHp)));
} }
if (SpReadout) if (SpReadout)
{ {
SpReadout->SetText(FText::Format( SpReadout->SetText(MakeReadout(Sp, MaxSp));
NSLOCTEXT("ZMMOHud", "HpSpReadout", "{0}/{1}"),
FormatNumber(Sp), FormatNumber(MaxSp)));
} }
} }

View File

@@ -7,6 +7,7 @@
class UUIProgressBar_Base; class UUIProgressBar_Base;
class UTextBlock; class UTextBlock;
class URichTextBlock;
/** /**
* Sub-widget do HUD: painel de vitais do player (portrait + nome/job/level + * Sub-widget do HUD: painel de vitais do player (portrait + nome/job/level +
@@ -71,19 +72,19 @@ protected:
TObjectPtr<UTextBlock> ClassText; TObjectPtr<UTextBlock> ClassText;
UPROPERTY(BlueprintReadOnly, meta = (BindWidgetOptional), Category = "Zeus|HUD") UPROPERTY(BlueprintReadOnly, meta = (BindWidgetOptional), Category = "Zeus|HUD")
TObjectPtr<UTextBlock> LvlText; TObjectPtr<URichTextBlock> LvlText;
UPROPERTY(BlueprintReadOnly, meta = (BindWidgetOptional), Category = "Zeus|HUD") UPROPERTY(BlueprintReadOnly, meta = (BindWidgetOptional), Category = "Zeus|HUD")
TObjectPtr<UTextBlock> HpReadout; TObjectPtr<URichTextBlock> HpReadout;
UPROPERTY(BlueprintReadOnly, meta = (BindWidgetOptional), Category = "Zeus|HUD") UPROPERTY(BlueprintReadOnly, meta = (BindWidgetOptional), Category = "Zeus|HUD")
TObjectPtr<UTextBlock> SpReadout; TObjectPtr<URichTextBlock> SpReadout;
UPROPERTY(BlueprintReadOnly, meta = (BindWidgetOptional), Category = "Zeus|HUD") UPROPERTY(BlueprintReadOnly, meta = (BindWidgetOptional), Category = "Zeus|HUD")
TObjectPtr<UTextBlock> ExpReadout; TObjectPtr<URichTextBlock> ExpReadout;
UPROPERTY(BlueprintReadOnly, meta = (BindWidgetOptional), Category = "Zeus|HUD") UPROPERTY(BlueprintReadOnly, meta = (BindWidgetOptional), Category = "Zeus|HUD")
TObjectPtr<UTextBlock> JobReadout; TObjectPtr<URichTextBlock> JobReadout;
private: private:
/** Último snapshot recebido (cache pra ApplyHpSp manter MaxHp/MaxSp). */ /** Último snapshot recebido (cache pra ApplyHpSp manter MaxHp/MaxSp). */

View File

@@ -19,9 +19,11 @@ namespace
S.EntityId = P.EntityId; S.EntityId = P.EntityId;
S.ClassId = P.ClassId; S.ClassId = P.ClassId;
S.BaseLevel = P.BaseLevel; S.BaseLevel = P.BaseLevel;
S.BaseExp = P.BaseExp; S.BaseExp = P.BaseExp;
S.BaseExpToNext = P.BaseExpToNext;
S.JobLevel = P.JobLevel; S.JobLevel = P.JobLevel;
S.JobExp = P.JobExp; S.JobExp = P.JobExp;
S.JobExpToNext = P.JobExpToNext;
S.Str = P.Str; S.Str = P.Str;
S.Agi = P.Agi; S.Agi = P.Agi;
S.Vit = P.Vit; S.Vit = P.Vit;

View File

@@ -26,8 +26,13 @@ struct ZMMOATTRIBUTES_API FZMMOAttributesSnapshot
// === Progressao === // === Progressao ===
UPROPERTY(BlueprintReadOnly, Category = "Zeus|Attributes") int32 BaseLevel = 1; UPROPERTY(BlueprintReadOnly, Category = "Zeus|Attributes") int32 BaseLevel = 1;
UPROPERTY(BlueprintReadOnly, Category = "Zeus|Attributes") int64 BaseExp = 0; UPROPERTY(BlueprintReadOnly, Category = "Zeus|Attributes") int64 BaseExp = 0;
/** EXP pra subir o proximo base level (server resolve via JobsDatabase).
* 0 = ja' em cap -> barra cheia. % = BaseExp / BaseExpToNext. */
UPROPERTY(BlueprintReadOnly, Category = "Zeus|Attributes") int64 BaseExpToNext = 0;
UPROPERTY(BlueprintReadOnly, Category = "Zeus|Attributes") int32 JobLevel = 1; UPROPERTY(BlueprintReadOnly, Category = "Zeus|Attributes") int32 JobLevel = 1;
UPROPERTY(BlueprintReadOnly, Category = "Zeus|Attributes") int64 JobExp = 0; UPROPERTY(BlueprintReadOnly, Category = "Zeus|Attributes") int64 JobExp = 0;
/** EXP pra subir o proximo job level. 0 = ja' em cap -> barra cheia. */
UPROPERTY(BlueprintReadOnly, Category = "Zeus|Attributes") int64 JobExpToNext = 0;
// === Stats primarios (Camada 1 base; efetivos vao em derivados) === // === Stats primarios (Camada 1 base; efetivos vao em derivados) ===
UPROPERTY(BlueprintReadOnly, Category = "Zeus|Attributes|Stats") int32 Str = 1; UPROPERTY(BlueprintReadOnly, Category = "Zeus|Attributes|Stats") int32 Str = 1;