feat(ui/status): nova janela com sub-widgets StatRow/BonusRow + cleanup

- 3 novas classes C++ (UUIPlayerStatus_Window/StatRow/BonusRow) substituem
  UZMMOStatusWindowWidget (removida); StatRow/BonusRow sao reutilizaveis
  via BindWidgetOptional + delegate OnPlusClicked(StatId).
- Window bindea no UZMMOAttributeComponent (PlayerState) e popula header
  (CharName, Job via UZMMOJobsSubsystem, BaseLevel/JobLevel) + 14 rows
  (6 primarios em StatRow com botao "+", 8 derivados em BonusRow).
- Fix spam-click do "+": bPlusButtonsLocked desabilita botoes ate
  OnStatAllocReply do server ou proximo snapshot.
- WBPs movidos pra /UI/Screens/PlayerStatus/, material pra /UI/Materials/.
- 58 widgets renomeados seguindo convencao
  (Frame_/Layout_/Panel_/Container_/Border_/Text_/Image_/Spacer_/List_).
- DA_InGameScreenSet auto-atualizado pelo move.
- Remove WBP_StatusWindow inline (legado).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-05-26 00:09:28 -03:00
parent 28bfa9a567
commit 9390ed9da9
14 changed files with 558 additions and 275 deletions

View File

@@ -1,169 +0,0 @@
#include "ZMMOStatusWindowWidget.h"
#include "CommonInputTypeEnum.h"
#include "Components/Button.h"
#include "Components/TextBlock.h"
#include "Engine/World.h"
#include "GameFramework/PlayerController.h"
#include "GameFramework/PlayerState.h"
#include "Internationalization/Text.h"
#include "ZMMO.h"
#include "ZMMOAttributeComponent.h"
#include "UI/InGame/UIInGameFlowSubsystem.h"
#include "UI/InGameTypes.h"
UZMMOStatusWindowWidget::UZMMOStatusWindowWidget(const FObjectInitializer& ObjectInitializer)
: Super(ObjectInitializer)
{
bIsBackHandler = true; // Esc/B fecha
bAutoActivate = true;
}
TOptional<FUIInputConfig> UZMMOStatusWindowWidget::GetDesiredInputConfig() const
{
// Modo Menu: cursor visivel, input flui pra UI (clicks nos botões),
// movimento bloqueado. Padrao pra menus modais.
return FUIInputConfig(ECommonInputMode::Menu, EMouseCaptureMode::NoCapture);
}
void UZMMOStatusWindowWidget::NativeOnActivated()
{
Super::NativeOnActivated();
BindToLocalPlayer();
if (PlusBtn_Str) PlusBtn_Str->OnClicked.AddDynamic(this, &UZMMOStatusWindowWidget::OnPlusStr);
if (PlusBtn_Agi) PlusBtn_Agi->OnClicked.AddDynamic(this, &UZMMOStatusWindowWidget::OnPlusAgi);
if (PlusBtn_Vit) PlusBtn_Vit->OnClicked.AddDynamic(this, &UZMMOStatusWindowWidget::OnPlusVit);
if (PlusBtn_Int) PlusBtn_Int->OnClicked.AddDynamic(this, &UZMMOStatusWindowWidget::OnPlusInt);
if (PlusBtn_Dex) PlusBtn_Dex->OnClicked.AddDynamic(this, &UZMMOStatusWindowWidget::OnPlusDex);
if (PlusBtn_Luk) PlusBtn_Luk->OnClicked.AddDynamic(this, &UZMMOStatusWindowWidget::OnPlusLuk);
if (CloseBtn) CloseBtn->OnClicked.AddDynamic(this, &UZMMOStatusWindowWidget::OnCloseClicked);
UE_LOG(LogZMMO, Log, TEXT("ZMMOStatusWindow activated"));
}
void UZMMOStatusWindowWidget::NativeOnDeactivated()
{
UnbindFromComponent();
if (PlusBtn_Str) PlusBtn_Str->OnClicked.RemoveDynamic(this, &UZMMOStatusWindowWidget::OnPlusStr);
if (PlusBtn_Agi) PlusBtn_Agi->OnClicked.RemoveDynamic(this, &UZMMOStatusWindowWidget::OnPlusAgi);
if (PlusBtn_Vit) PlusBtn_Vit->OnClicked.RemoveDynamic(this, &UZMMOStatusWindowWidget::OnPlusVit);
if (PlusBtn_Int) PlusBtn_Int->OnClicked.RemoveDynamic(this, &UZMMOStatusWindowWidget::OnPlusInt);
if (PlusBtn_Dex) PlusBtn_Dex->OnClicked.RemoveDynamic(this, &UZMMOStatusWindowWidget::OnPlusDex);
if (PlusBtn_Luk) PlusBtn_Luk->OnClicked.RemoveDynamic(this, &UZMMOStatusWindowWidget::OnPlusLuk);
if (CloseBtn) CloseBtn->OnClicked.RemoveDynamic(this, &UZMMOStatusWindowWidget::OnCloseClicked);
Super::NativeOnDeactivated();
}
void UZMMOStatusWindowWidget::BindToLocalPlayer()
{
UWorld* World = GetWorld();
if (!World) { return; }
APlayerController* PC = World->GetFirstPlayerController();
if (!PC || !PC->PlayerState) { return; }
UZMMOAttributeComponent* Comp = PC->PlayerState->FindComponentByClass<UZMMOAttributeComponent>();
if (!Comp || Comp == BoundComponent.Get()) { return; }
UnbindFromComponent();
BoundComponent = Comp;
Comp->OnAttributesChanged.AddDynamic(this, &UZMMOStatusWindowWidget::HandleAttributesChanged);
Comp->OnStatAllocReply.AddDynamic(this, &UZMMOStatusWindowWidget::HandleStatAllocReply);
RefreshFromSnapshot(Comp->GetSnapshot());
}
void UZMMOStatusWindowWidget::UnbindFromComponent()
{
if (UZMMOAttributeComponent* Old = BoundComponent.Get())
{
Old->OnAttributesChanged.RemoveDynamic(this, &UZMMOStatusWindowWidget::HandleAttributesChanged);
Old->OnStatAllocReply.RemoveDynamic(this, &UZMMOStatusWindowWidget::HandleStatAllocReply);
}
BoundComponent.Reset();
}
void UZMMOStatusWindowWidget::HandleAttributesChanged(const FZMMOAttributesSnapshot& Snapshot)
{
RefreshFromSnapshot(Snapshot);
}
void UZMMOStatusWindowWidget::HandleStatAllocReply(bool bAccepted, int32 Reason)
{
UE_LOG(LogZMMO, Log, TEXT("StatusWindow: alloc reply accepted=%d reason=%d"),
bAccepted ? 1 : 0, Reason);
// Snapshot subsequente vai disparar HandleAttributesChanged e atualizar UI.
// Aqui poderia popup de erro visual quando bAccepted=false (TODO polish).
}
void UZMMOStatusWindowWidget::RefreshFromSnapshot(const FZMMOAttributesSnapshot& S)
{
if (StrText) StrText->SetText(FText::FromString(FString::Printf(TEXT("STR %d"), S.Str)));
if (AgiText) AgiText->SetText(FText::FromString(FString::Printf(TEXT("AGI %d"), S.Agi)));
if (VitText) VitText->SetText(FText::FromString(FString::Printf(TEXT("VIT %d"), S.Vit)));
if (IntText) IntText->SetText(FText::FromString(FString::Printf(TEXT("INT %d"), S.Int)));
if (DexText) DexText->SetText(FText::FromString(FString::Printf(TEXT("DEX %d"), S.Dex)));
if (LukText) LukText->SetText(FText::FromString(FString::Printf(TEXT("LUK %d"), S.Luk)));
if (StatusPointText)
{
StatusPointText->SetText(FText::FromString(
FString::Printf(TEXT("Status Points: %d"), S.StatusPoint)));
}
// Derivados — formato RO "STAT base + equip" (ex.: "ATK 51 + 0").
// Servidor manda os dois separados; cliente NUNCA recalcula.
if (AtkText) AtkText->SetText(FText::FromString(FString::Printf(TEXT("ATK %d + %d"), S.AtkBase, S.AtkEquipBonus)));
// MATK pre-renewal: range "min ~ max + equip" (rathena status_base_matk_min/max).
if (MatkText) MatkText->SetText(FText::FromString(FString::Printf(TEXT("MATK %d ~ %d + %d"), S.MatkBaseMin, S.MatkBaseMax, S.MatkEquipBonus)));
if (DefText) DefText->SetText(FText::FromString(FString::Printf(TEXT("DEF %d + %d"), S.DefBase, S.DefEquipBonus)));
if (MdefText) MdefText->SetText(FText::FromString(FString::Printf(TEXT("MDEF %d + %d"), S.MdefBase, S.MdefEquipBonus)));
if (HitText) HitText->SetText(FText::FromString(FString::Printf(TEXT("HIT %d + %d"), S.HitBase, S.HitEquipBonus)));
if (FleeText) FleeText->SetText(FText::FromString(FString::Printf(TEXT("FLEE %d + %d"), S.FleeBase, S.FleeEquipBonus)));
if (CritText)
{
// CRIT vem ×10 em ambas as camadas — soma X10 antes de converter pra
// decimal (10 = 1.0). Display: "CRIT base + equip" com 1 casa cada.
const int32 BaseInt = S.CritBaseX10 / 10;
const int32 BaseFrac = S.CritBaseX10 % 10;
const int32 EquipInt = S.CritEquipBonusX10 / 10;
const int32 EquipFrac = S.CritEquipBonusX10 % 10;
CritText->SetText(FText::FromString(FString::Printf(
TEXT("CRIT %d.%d + %d.%d"), BaseInt, BaseFrac, EquipInt, EquipFrac)));
}
// ASPD nao splita (calculo nao-linear server-side via aspd_base × stats).
if (AspdText) AspdText->SetText(FText::FromString(FString::Printf(TEXT("ASPD %d"), S.Aspd)));
// Habilita botões `+` somente se ha status_point disponível.
const bool bCanAlloc = S.StatusPoint > 0;
if (PlusBtn_Str) PlusBtn_Str->SetIsEnabled(bCanAlloc);
if (PlusBtn_Agi) PlusBtn_Agi->SetIsEnabled(bCanAlloc);
if (PlusBtn_Vit) PlusBtn_Vit->SetIsEnabled(bCanAlloc);
if (PlusBtn_Int) PlusBtn_Int->SetIsEnabled(bCanAlloc);
if (PlusBtn_Dex) PlusBtn_Dex->SetIsEnabled(bCanAlloc);
if (PlusBtn_Luk) PlusBtn_Luk->SetIsEnabled(bCanAlloc);
}
void UZMMOStatusWindowWidget::RequestAlloc(int32 StatId)
{
if (UZMMOAttributeComponent* Comp = BoundComponent.Get())
{
Comp->RequestStatAlloc(StatId, /*Amount*/ 1);
}
}
void UZMMOStatusWindowWidget::OnPlusStr() { RequestAlloc(0); }
void UZMMOStatusWindowWidget::OnPlusAgi() { RequestAlloc(1); }
void UZMMOStatusWindowWidget::OnPlusVit() { RequestAlloc(2); }
void UZMMOStatusWindowWidget::OnPlusInt() { RequestAlloc(3); }
void UZMMOStatusWindowWidget::OnPlusDex() { RequestAlloc(4); }
void UZMMOStatusWindowWidget::OnPlusLuk() { RequestAlloc(5); }
void UZMMOStatusWindowWidget::OnCloseClicked()
{
if (UGameInstance* GI = GetGameInstance())
{
if (UUIInGameFlowSubsystem* Flow = GI->GetSubsystem<UUIInGameFlowSubsystem>())
{
Flow->SetState(EZMMOInGameUIState::Playing);
}
}
}

View File

@@ -1,106 +0,0 @@
#pragma once
#include "CommonActivatableWidget.h"
#include "CoreMinimal.h"
#include "ZMMOAttributeTypes.h"
#include "ZMMOStatusWindowWidget.generated.h"
class UButton;
class UTextBlock;
class UCommonButtonBase;
class UZMMOAttributeComponent;
/**
* Tela "Status" — atributos do jogador + alocacao via botões `+`.
*
* Modo Menu: cursor visível, movimento bloqueado, Esc fecha. Push em
* UI.Layer.GameMenu pelo `UUIInGameFlowSubsystem::ToggleScreen(StatusWindow)`.
* HUD continua visível em UI.Layer.Game por baixo.
*
* Composicao do WBP_StatusWindow (BindWidgetOptional pra permitir variantes):
* - Labels de stats primarios: StrText / AgiText / VitText / IntText / DexText / LukText
* - Botões +: PlusBtn_Str / PlusBtn_Agi / PlusBtn_Vit / PlusBtn_Int / PlusBtn_Dex / PlusBtn_Luk
* - Derivados (server-authoritative): AtkText / MatkText / DefText / MdefText /
* HitText / FleeText / CritText / AspdText
* - StatusPointText (pontos disponíveis pra alocar)
* - CloseBtn (fecha — fallback do Esc/back handler)
*
* Workflow:
* 1. NativeOnActivated: busca AttributeComponent (PlayerState do player local)
* 2. Subscreve OnAttributesChanged + OnStatAllocReply
* 3. Refresh inicial via snapshot atual
* 4. Clique no `+X` chama `Comp->RequestStatAlloc(X, 1)` → server valida
* 5. Server responde com S_ATTRIBUTE_STAT_ALLOC_OK + (se ok) snapshot novo
* 6. UI atualiza via delegate
*/
UCLASS(Abstract, Blueprintable, BlueprintType)
class ZMMO_API UZMMOStatusWindowWidget : public UCommonActivatableWidget
{
GENERATED_BODY()
public:
UZMMOStatusWindowWidget(const FObjectInitializer& ObjectInitializer);
protected:
virtual void NativeOnActivated() override;
virtual void NativeOnDeactivated() override;
/// Modo Menu: cursor visível + bloqueia input de gameplay (movimento).
virtual TOptional<FUIInputConfig> GetDesiredInputConfig() const override;
/// Texto dos stats (label + valor). BindWidgetOptional pra permitir
/// WBPs custom (ex.: sem display de SP em variantes).
UPROPERTY(BlueprintReadOnly, meta = (BindWidgetOptional), Category = "Zeus|Status") UTextBlock* StrText = nullptr;
UPROPERTY(BlueprintReadOnly, meta = (BindWidgetOptional), Category = "Zeus|Status") UTextBlock* AgiText = nullptr;
UPROPERTY(BlueprintReadOnly, meta = (BindWidgetOptional), Category = "Zeus|Status") UTextBlock* VitText = nullptr;
UPROPERTY(BlueprintReadOnly, meta = (BindWidgetOptional), Category = "Zeus|Status") UTextBlock* IntText = nullptr;
UPROPERTY(BlueprintReadOnly, meta = (BindWidgetOptional), Category = "Zeus|Status") UTextBlock* DexText = nullptr;
UPROPERTY(BlueprintReadOnly, meta = (BindWidgetOptional), Category = "Zeus|Status") UTextBlock* LukText = nullptr;
/// Pontos restantes pra alocar + label "Status Points: N".
UPROPERTY(BlueprintReadOnly, meta = (BindWidgetOptional), Category = "Zeus|Status") UTextBlock* StatusPointText = nullptr;
/// Derivados (sempre calculados pelo servidor — cliente NUNCA recalcula).
/// Mostra `base + bonus` quando aplicavel (camadas equip/buff vem do server
/// pre-somadas no snapshot). BindWidgetOptional pra suportar HUDs minimalistas.
UPROPERTY(BlueprintReadOnly, meta = (BindWidgetOptional), Category = "Zeus|Status|Derived") UTextBlock* AtkText = nullptr;
UPROPERTY(BlueprintReadOnly, meta = (BindWidgetOptional), Category = "Zeus|Status|Derived") UTextBlock* MatkText = nullptr;
UPROPERTY(BlueprintReadOnly, meta = (BindWidgetOptional), Category = "Zeus|Status|Derived") UTextBlock* DefText = nullptr;
UPROPERTY(BlueprintReadOnly, meta = (BindWidgetOptional), Category = "Zeus|Status|Derived") UTextBlock* MdefText = nullptr;
UPROPERTY(BlueprintReadOnly, meta = (BindWidgetOptional), Category = "Zeus|Status|Derived") UTextBlock* HitText = nullptr;
UPROPERTY(BlueprintReadOnly, meta = (BindWidgetOptional), Category = "Zeus|Status|Derived") UTextBlock* FleeText = nullptr;
UPROPERTY(BlueprintReadOnly, meta = (BindWidgetOptional), Category = "Zeus|Status|Derived") UTextBlock* CritText = nullptr;
UPROPERTY(BlueprintReadOnly, meta = (BindWidgetOptional), Category = "Zeus|Status|Derived") UTextBlock* AspdText = nullptr;
/// Botoes + por stat.
UPROPERTY(BlueprintReadOnly, meta = (BindWidgetOptional), Category = "Zeus|Status") UButton* PlusBtn_Str = nullptr;
UPROPERTY(BlueprintReadOnly, meta = (BindWidgetOptional), Category = "Zeus|Status") UButton* PlusBtn_Agi = nullptr;
UPROPERTY(BlueprintReadOnly, meta = (BindWidgetOptional), Category = "Zeus|Status") UButton* PlusBtn_Vit = nullptr;
UPROPERTY(BlueprintReadOnly, meta = (BindWidgetOptional), Category = "Zeus|Status") UButton* PlusBtn_Int = nullptr;
UPROPERTY(BlueprintReadOnly, meta = (BindWidgetOptional), Category = "Zeus|Status") UButton* PlusBtn_Dex = nullptr;
UPROPERTY(BlueprintReadOnly, meta = (BindWidgetOptional), Category = "Zeus|Status") UButton* PlusBtn_Luk = nullptr;
/// Botao Close (fallback do Esc/back).
UPROPERTY(BlueprintReadOnly, meta = (BindWidgetOptional), Category = "Zeus|Status") UButton* CloseBtn = nullptr;
private:
void BindToLocalPlayer();
void UnbindFromComponent();
void RefreshFromSnapshot(const FZMMOAttributesSnapshot& Snap);
UFUNCTION() void HandleAttributesChanged(const FZMMOAttributesSnapshot& Snapshot);
UFUNCTION() void HandleStatAllocReply(bool bAccepted, int32 Reason);
UFUNCTION() void OnPlusStr();
UFUNCTION() void OnPlusAgi();
UFUNCTION() void OnPlusVit();
UFUNCTION() void OnPlusInt();
UFUNCTION() void OnPlusDex();
UFUNCTION() void OnPlusLuk();
UFUNCTION() void OnCloseClicked();
void RequestAlloc(int32 StatId);
UPROPERTY(Transient)
TWeakObjectPtr<UZMMOAttributeComponent> BoundComponent;
};

View File

@@ -0,0 +1,69 @@
#include "UIPlayerStatus_BonusRow.h"
#include "Components/TextBlock.h"
#include "Internationalization/Text.h"
void UUIPlayerStatus_BonusRow::SetStatName(FText DisplayName)
{
if (Text_StatName) Text_StatName->SetText(DisplayName);
}
void UUIPlayerStatus_BonusRow::SetValues(int32 Base, int32 Bonus)
{
if (Text_BasePart) Text_BasePart->SetText(FText::AsNumber(Base));
const bool bShowBonus = (Bonus != 0);
SetBonusVisibility(bShowBonus);
if (bShowBonus)
{
if (Text_SplitOp) Text_SplitOp->SetText(FText::FromString(Bonus > 0 ? TEXT("+") : TEXT("-")));
if (Text_BonusPart) Text_BonusPart->SetText(FText::AsNumber(FMath::Abs(Bonus)));
}
}
void UUIPlayerStatus_BonusRow::SetRangeValues(int32 MinBase, int32 MaxBase, int32 Bonus)
{
if (Text_BasePart)
{
Text_BasePart->SetText(FText::FromString(FString::Printf(TEXT("%d~%d"), MinBase, MaxBase)));
}
const bool bShowBonus = (Bonus != 0);
SetBonusVisibility(bShowBonus);
if (bShowBonus)
{
if (Text_SplitOp) Text_SplitOp->SetText(FText::FromString(Bonus > 0 ? TEXT("+") : TEXT("-")));
if (Text_BonusPart) Text_BonusPart->SetText(FText::AsNumber(FMath::Abs(Bonus)));
}
}
void UUIPlayerStatus_BonusRow::SetValuesX10(int32 BaseX10, int32 BonusX10)
{
auto FormatX10 = [](int32 ValX10) -> FString
{
const int32 Sign = ValX10 < 0 ? -1 : 1;
const int32 Abs = FMath::Abs(ValX10);
return FString::Printf(TEXT("%d.%d"), Sign * (Abs / 10), Abs % 10);
};
if (Text_BasePart) Text_BasePart->SetText(FText::FromString(FormatX10(BaseX10)));
const bool bShowBonus = (BonusX10 != 0);
SetBonusVisibility(bShowBonus);
if (bShowBonus)
{
if (Text_SplitOp) Text_SplitOp->SetText(FText::FromString(BonusX10 > 0 ? TEXT("+") : TEXT("-")));
if (Text_BonusPart)
{
const int32 Abs = FMath::Abs(BonusX10);
Text_BonusPart->SetText(FText::FromString(FString::Printf(TEXT("%d.%d"), Abs / 10, Abs % 10)));
}
}
}
void UUIPlayerStatus_BonusRow::SetBonusVisibility(bool bShow)
{
const ESlateVisibility Vis = bShow ? ESlateVisibility::Visible : ESlateVisibility::Collapsed;
if (Text_SplitOp) Text_SplitOp->SetVisibility(Vis);
if (Text_BonusPart) Text_BonusPart->SetVisibility(Vis);
}

View File

@@ -0,0 +1,46 @@
#pragma once
#include "CoreMinimal.h"
#include "CommonUserWidget.h"
#include "UIPlayerStatus_BonusRow.generated.h"
class UTextBlock;
/**
* Linha de um stat derivado (ATK/DEF/HIT/etc.) na janela de Status.
* Layout: nome + valor base + (separador + bônus). Sem botão.
*
* Três modos de valor:
* - SetValues(base, bonus) "10 + 2"
* - SetRangeValues(min, max, bonus) "10~15 + 2" (MATK pre-renewal)
* - SetValuesX10(baseX10, bonusX10) "1.5 + 0.0" (CRIT armazenado ×10)
*
* Pareada com o WBP UI_PlayerStatus_BonusRow.
*/
UCLASS(Abstract, Blueprintable)
class ZMMO_API UUIPlayerStatus_BonusRow : public UCommonUserWidget
{
GENERATED_BODY()
public:
UFUNCTION(BlueprintCallable, Category = "Zeus|Status")
void SetStatName(FText DisplayName);
UFUNCTION(BlueprintCallable, Category = "Zeus|Status")
void SetValues(int32 Base, int32 Bonus);
UFUNCTION(BlueprintCallable, Category = "Zeus|Status")
void SetRangeValues(int32 MinBase, int32 MaxBase, int32 Bonus);
UFUNCTION(BlueprintCallable, Category = "Zeus|Status")
void SetValuesX10(int32 BaseX10, int32 BonusX10);
protected:
UPROPERTY(meta = (BindWidgetOptional)) TObjectPtr<UTextBlock> Text_StatName;
UPROPERTY(meta = (BindWidgetOptional)) TObjectPtr<UTextBlock> Text_BasePart;
UPROPERTY(meta = (BindWidgetOptional)) TObjectPtr<UTextBlock> Text_SplitOp;
UPROPERTY(meta = (BindWidgetOptional)) TObjectPtr<UTextBlock> Text_BonusPart;
private:
void SetBonusVisibility(bool bShow);
};

View File

@@ -0,0 +1,73 @@
#include "UIPlayerStatus_StatRow.h"
#include "Components/Button.h"
#include "Components/HorizontalBox.h"
#include "Components/TextBlock.h"
#include "Internationalization/Text.h"
void UUIPlayerStatus_StatRow::SetupStat(int32 InStatId, FText IconLetter, FText DisplayName)
{
StatId = InStatId;
if (Border_StatIconWrap_Label) Border_StatIconWrap_Label->SetText(IconLetter);
if (Text_StatName) Text_StatName->SetText(DisplayName);
}
void UUIPlayerStatus_StatRow::SetValues(int32 Base, int32 Bonus)
{
if (Text_StatBase) Text_StatBase->SetText(FText::AsNumber(Base));
const bool bShowBonus = (Bonus != 0);
const ESlateVisibility BonusVis = bShowBonus ? ESlateVisibility::Visible : ESlateVisibility::Collapsed;
if (Layout_BonusGroup)
{
Layout_BonusGroup->SetVisibility(BonusVis);
}
else
{
if (Text_Sign) Text_Sign->SetVisibility(BonusVis);
if (Text_StatBonus) Text_StatBonus->SetVisibility(BonusVis);
}
if (bShowBonus)
{
if (Text_Sign) Text_Sign->SetText(FText::FromString(Bonus > 0 ? TEXT("+") : TEXT("-")));
if (Text_StatBonus) Text_StatBonus->SetText(FText::AsNumber(FMath::Abs(Bonus)));
}
}
void UUIPlayerStatus_StatRow::SetPlusVisible(bool bVisible)
{
if (Button_PlusBtn)
{
Button_PlusBtn->SetVisibility(bVisible ? ESlateVisibility::Visible : ESlateVisibility::Collapsed);
}
}
void UUIPlayerStatus_StatRow::SetPlusEnabled(bool bEnabled)
{
if (Button_PlusBtn) Button_PlusBtn->SetIsEnabled(bEnabled);
}
void UUIPlayerStatus_StatRow::NativeOnInitialized()
{
Super::NativeOnInitialized();
if (Button_PlusBtn)
{
Button_PlusBtn->OnClicked.AddDynamic(this, &UUIPlayerStatus_StatRow::HandlePlusClicked);
}
}
void UUIPlayerStatus_StatRow::NativeDestruct()
{
if (Button_PlusBtn)
{
Button_PlusBtn->OnClicked.RemoveDynamic(this, &UUIPlayerStatus_StatRow::HandlePlusClicked);
}
Super::NativeDestruct();
}
void UUIPlayerStatus_StatRow::HandlePlusClicked()
{
OnPlusClicked.Broadcast(StatId);
}

View File

@@ -0,0 +1,68 @@
#pragma once
#include "CoreMinimal.h"
#include "CommonUserWidget.h"
#include "UIPlayerStatus_StatRow.generated.h"
class UButton;
class UHorizontalBox;
class UTextBlock;
DECLARE_DYNAMIC_MULTICAST_DELEGATE_OneParam(FZMMOOnStatRowPlusClicked, int32, StatId);
/**
* Linha de um stat primário (STR/AGI/VIT/INT/DEX/LUK) na janela de Status.
* Layout: ícone + nome + valor base + (sinal + bônus) + botão "+".
*
* Sem lógica de domínio — a janela (UUIPlayerStatus_Window) injeta o
* StatId e os valores via Setup/SetValues; o clique no "+" é re-emitido
* como OnPlusClicked(StatId).
*
* Pareada com o WBP UI_PlayerStatus_StatRow.
*/
UCLASS(Abstract, Blueprintable)
class ZMMO_API UUIPlayerStatus_StatRow : public UCommonUserWidget
{
GENERATED_BODY()
public:
/** Setup único chamado pela window quando inicializa a linha. */
UFUNCTION(BlueprintCallable, Category = "Zeus|Status")
void SetupStat(int32 InStatId, FText IconLetter, FText DisplayName);
/** Atualiza valores. Bonus=0 esconde a parte "+N". */
UFUNCTION(BlueprintCallable, Category = "Zeus|Status")
void SetValues(int32 Base, int32 Bonus);
/** Mostra/esconde o botão "+". */
UFUNCTION(BlueprintCallable, Category = "Zeus|Status")
void SetPlusVisible(bool bVisible);
/** Habilita/desabilita o botão "+" (independente da visibilidade). */
UFUNCTION(BlueprintCallable, Category = "Zeus|Status")
void SetPlusEnabled(bool bEnabled);
/** Disparado quando o usuário clica em "+". */
UPROPERTY(BlueprintAssignable, Category = "Zeus|Status")
FZMMOOnStatRowPlusClicked OnPlusClicked;
protected:
virtual void NativeOnInitialized() override;
virtual void NativeDestruct() override;
UPROPERTY(meta = (BindWidgetOptional)) TObjectPtr<UTextBlock> Border_StatIconWrap_Label;
UPROPERTY(meta = (BindWidgetOptional)) TObjectPtr<UTextBlock> Text_StatName;
UPROPERTY(meta = (BindWidgetOptional)) TObjectPtr<UTextBlock> Text_StatBase;
UPROPERTY(meta = (BindWidgetOptional)) TObjectPtr<UTextBlock> Text_Sign;
UPROPERTY(meta = (BindWidgetOptional)) TObjectPtr<UTextBlock> Text_StatBonus;
UPROPERTY(meta = (BindWidgetOptional)) TObjectPtr<UButton> Button_PlusBtn;
/** HBox que envolve sinal+bonus; ocultado quando bonus = 0. Bind opcional —
* se o WBP não expuser, cai pra ocultar Text_Sign/Text_StatBonus individualmente. */
UPROPERTY(meta = (BindWidgetOptional)) TObjectPtr<UHorizontalBox> Layout_BonusGroup;
private:
UFUNCTION() void HandlePlusClicked();
int32 StatId = 0;
};

View File

@@ -0,0 +1,220 @@
#include "UIPlayerStatus_Window.h"
#include "CommonInputTypeEnum.h"
#include "Components/Button.h"
#include "Components/TextBlock.h"
#include "Engine/World.h"
#include "GameFramework/PlayerController.h"
#include "GameFramework/PlayerState.h"
#include "Internationalization/Text.h"
#include "ZMMO.h"
#include "ZMMOAttributeComponent.h"
#include "ZMMOJobDataAsset.h"
#include "ZMMOJobsSubsystem.h"
#include "UI/InGame/UIInGameFlowSubsystem.h"
#include "UI/InGameTypes.h"
#include "UI/Widgets/UIPlayerStatus_BonusRow.h"
#include "UI/Widgets/UIPlayerStatus_StatRow.h"
UUIPlayerStatus_Window::UUIPlayerStatus_Window(const FObjectInitializer& ObjectInitializer)
: Super(ObjectInitializer)
{
bIsBackHandler = true;
bAutoActivate = true;
}
TOptional<FUIInputConfig> UUIPlayerStatus_Window::GetDesiredInputConfig() const
{
return FUIInputConfig(ECommonInputMode::Menu, EMouseCaptureMode::NoCapture);
}
void UUIPlayerStatus_Window::NativeOnActivated()
{
Super::NativeOnActivated();
BindRow(Row_Str, 0, TEXT("S"), TEXT("STR"));
BindRow(Row_Agi, 1, TEXT("A"), TEXT("AGI"));
BindRow(Row_Vit, 2, TEXT("V"), TEXT("VIT"));
BindRow(Row_Int, 3, TEXT("I"), TEXT("INT"));
BindRow(Row_Dex, 4, TEXT("D"), TEXT("DEX"));
BindRow(Row_Luk, 5, TEXT("L"), TEXT("LUK"));
if (Row_Atk) Row_Atk->SetStatName(FText::FromString(TEXT("ATK")));
if (Row_Matk) Row_Matk->SetStatName(FText::FromString(TEXT("MATK")));
if (Row_Def) Row_Def->SetStatName(FText::FromString(TEXT("DEF")));
if (Row_Mdef) Row_Mdef->SetStatName(FText::FromString(TEXT("MDEF")));
if (Row_Hit) Row_Hit->SetStatName(FText::FromString(TEXT("HIT")));
if (Row_Flee) Row_Flee->SetStatName(FText::FromString(TEXT("FLEE")));
if (Row_Crit) Row_Crit->SetStatName(FText::FromString(TEXT("CRIT")));
if (Row_Aspd) Row_Aspd->SetStatName(FText::FromString(TEXT("ASPD")));
if (TitleText) TitleText->SetText(FText::FromString(TEXT("Status")));
if (CloseBtn) CloseBtn->OnClicked.AddDynamic(this, &UUIPlayerStatus_Window::OnCloseClicked);
BindToLocalPlayer();
UE_LOG(LogZMMO, Log, TEXT("UIPlayerStatus_Window activated"));
}
void UUIPlayerStatus_Window::NativeOnDeactivated()
{
UnbindFromComponent();
auto UnbindRow = [this](UUIPlayerStatus_StatRow* Row)
{
if (Row) Row->OnPlusClicked.RemoveDynamic(this, &UUIPlayerStatus_Window::HandleRowPlusClicked);
};
UnbindRow(Row_Str); UnbindRow(Row_Agi); UnbindRow(Row_Vit);
UnbindRow(Row_Int); UnbindRow(Row_Dex); UnbindRow(Row_Luk);
if (CloseBtn) CloseBtn->OnClicked.RemoveDynamic(this, &UUIPlayerStatus_Window::OnCloseClicked);
Super::NativeOnDeactivated();
}
void UUIPlayerStatus_Window::BindRow(UUIPlayerStatus_StatRow* Row, int32 StatId, const TCHAR* IconLetter, const TCHAR* DisplayName)
{
if (!Row) return;
Row->SetupStat(StatId, FText::FromString(IconLetter), FText::FromString(DisplayName));
Row->OnPlusClicked.AddDynamic(this, &UUIPlayerStatus_Window::HandleRowPlusClicked);
}
void UUIPlayerStatus_Window::BindToLocalPlayer()
{
UWorld* World = GetWorld();
if (!World) return;
APlayerController* PC = World->GetFirstPlayerController();
if (!PC || !PC->PlayerState) return;
// CharName vem do PlayerState (estável por sessão) — popula uma vez.
if (Text_CharName)
{
Text_CharName->SetText(FText::FromString(PC->PlayerState->GetPlayerName()));
}
UZMMOAttributeComponent* Comp = PC->PlayerState->FindComponentByClass<UZMMOAttributeComponent>();
if (!Comp || Comp == BoundComponent.Get()) return;
UnbindFromComponent();
BoundComponent = Comp;
Comp->OnAttributesChanged.AddDynamic(this, &UUIPlayerStatus_Window::HandleAttributesChanged);
Comp->OnStatAllocReply.AddDynamic(this, &UUIPlayerStatus_Window::HandleStatAllocReply);
RefreshFromSnapshot(Comp->GetSnapshot());
}
void UUIPlayerStatus_Window::UnbindFromComponent()
{
if (UZMMOAttributeComponent* Old = BoundComponent.Get())
{
Old->OnAttributesChanged.RemoveDynamic(this, &UUIPlayerStatus_Window::HandleAttributesChanged);
Old->OnStatAllocReply.RemoveDynamic(this, &UUIPlayerStatus_Window::HandleStatAllocReply);
}
BoundComponent.Reset();
}
void UUIPlayerStatus_Window::HandleAttributesChanged(const FZMMOAttributesSnapshot& Snapshot)
{
bPlusButtonsLocked = false;
RefreshFromSnapshot(Snapshot);
}
void UUIPlayerStatus_Window::HandleStatAllocReply(bool bAccepted, int32 Reason)
{
UE_LOG(LogZMMO, Log, TEXT("UIPlayerStatus_Window: alloc reply accepted=%d reason=%d"),
bAccepted ? 1 : 0, Reason);
bPlusButtonsLocked = false;
if (UZMMOAttributeComponent* Comp = BoundComponent.Get())
{
const FZMMOAttributesSnapshot& S = Comp->GetSnapshot();
SetAllPrimaryRowsPlusEnabled(S.StatusPoint > 0);
}
}
void UUIPlayerStatus_Window::HandleRowPlusClicked(int32 StatId)
{
if (bPlusButtonsLocked) return;
UZMMOAttributeComponent* Comp = BoundComponent.Get();
if (!Comp) return;
bPlusButtonsLocked = true;
SetAllPrimaryRowsPlusEnabled(false);
Comp->RequestStatAlloc(StatId, /*Amount*/ 1);
}
void UUIPlayerStatus_Window::SetAllPrimaryRowsPlusEnabled(bool bEnabled)
{
auto Set = [bEnabled](UUIPlayerStatus_StatRow* Row)
{
if (Row) Row->SetPlusEnabled(bEnabled);
};
Set(Row_Str); Set(Row_Agi); Set(Row_Vit);
Set(Row_Int); Set(Row_Dex); Set(Row_Luk);
}
void UUIPlayerStatus_Window::RefreshFromSnapshot(const FZMMOAttributesSnapshot& S)
{
// Primários: snapshot atual não splita base/bonus de primários (só derivados).
if (Row_Str) Row_Str->SetValues(S.Str, 0);
if (Row_Agi) Row_Agi->SetValues(S.Agi, 0);
if (Row_Vit) Row_Vit->SetValues(S.Vit, 0);
if (Row_Int) Row_Int->SetValues(S.Int, 0);
if (Row_Dex) Row_Dex->SetValues(S.Dex, 0);
if (Row_Luk) Row_Luk->SetValues(S.Luk, 0);
// Header (level/job).
if (Text_BaseLevel_Value) Text_BaseLevel_Value->SetText(FText::AsNumber(S.BaseLevel));
if (Text_JobLevel_Value) Text_JobLevel_Value->SetText(FText::AsNumber(S.JobLevel));
if (Text_Job)
{
FText JobName;
if (UGameInstance* GI = GetGameInstance())
{
if (const UZMMOJobsSubsystem* Jobs = GI->GetSubsystem<UZMMOJobsSubsystem>())
{
if (const UZMMOJobDataAsset* Data = Jobs->GetJobData(S.ClassId))
{
JobName = Data->DisplayName;
}
}
}
if (JobName.IsEmpty())
{
JobName = FText::FromString(FString::Printf(TEXT("Job %d"), S.ClassId));
}
Text_Job->SetText(JobName);
}
if (StatusPointText)
{
StatusPointText->SetText(FText::FromString(
FString::Printf(TEXT("Status Points: %d"), S.StatusPoint)));
}
// Derivados (server-authoritative: base + equip).
if (Row_Atk) Row_Atk->SetValues(S.AtkBase, S.AtkEquipBonus);
if (Row_Matk) Row_Matk->SetRangeValues(S.MatkBaseMin, S.MatkBaseMax, S.MatkEquipBonus);
if (Row_Def) Row_Def->SetValues(S.DefBase, S.DefEquipBonus);
if (Row_Mdef) Row_Mdef->SetValues(S.MdefBase, S.MdefEquipBonus);
if (Row_Hit) Row_Hit->SetValues(S.HitBase, S.HitEquipBonus);
if (Row_Flee) Row_Flee->SetValues(S.FleeBase, S.FleeEquipBonus);
if (Row_Crit) Row_Crit->SetValuesX10(S.CritBaseX10, S.CritEquipBonusX10);
// ASPD não splita.
if (Row_Aspd) Row_Aspd->SetValues(S.Aspd, 0);
const bool bCanAlloc = !bPlusButtonsLocked && S.StatusPoint > 0;
SetAllPrimaryRowsPlusEnabled(bCanAlloc);
}
void UUIPlayerStatus_Window::OnCloseClicked()
{
if (UGameInstance* GI = GetGameInstance())
{
if (UUIInGameFlowSubsystem* Flow = GI->GetSubsystem<UUIInGameFlowSubsystem>())
{
Flow->SetState(EZMMOInGameUIState::Playing);
}
}
}

View File

@@ -0,0 +1,82 @@
#pragma once
#include "CoreMinimal.h"
#include "CommonActivatableWidget.h"
#include "ZMMOAttributeTypes.h"
#include "UIPlayerStatus_Window.generated.h"
class UButton;
class UTextBlock;
class UUIPlayerStatus_BonusRow;
class UUIPlayerStatus_StatRow;
class UZMMOAttributeComponent;
/**
* Janela de Status do player — composta por instâncias dos sub-widgets
* UUIPlayerStatus_StatRow (6 primários) e UUIPlayerStatus_BonusRow (8 derivados).
*
* Consome o UZMMOAttributeComponent do PlayerState e a pipeline de
* C_STAT_ALLOC (RequestStatAlloc → server valida → OnStatAllocReply).
*
* Pareada com o WBP UI_PlayerStatus_Window. Os 14 rows são BindWidgetOptional
* pra suportar variantes (ex.: HUD compacto sem CRIT/ASPD).
*/
UCLASS(Abstract, Blueprintable, BlueprintType)
class ZMMO_API UUIPlayerStatus_Window : public UCommonActivatableWidget
{
GENERATED_BODY()
public:
UUIPlayerStatus_Window(const FObjectInitializer& ObjectInitializer);
protected:
virtual void NativeOnActivated() override;
virtual void NativeOnDeactivated() override;
virtual TOptional<FUIInputConfig> GetDesiredInputConfig() const override;
UPROPERTY(BlueprintReadOnly, meta = (BindWidgetOptional), Category = "Zeus|Status") TObjectPtr<UTextBlock> TitleText;
UPROPERTY(BlueprintReadOnly, meta = (BindWidgetOptional), Category = "Zeus|Status") TObjectPtr<UTextBlock> StatusPointText;
// Header: identidade do char.
UPROPERTY(BlueprintReadOnly, meta = (BindWidgetOptional), Category = "Zeus|Status|Header") TObjectPtr<UTextBlock> Text_CharName;
UPROPERTY(BlueprintReadOnly, meta = (BindWidgetOptional), Category = "Zeus|Status|Header") TObjectPtr<UTextBlock> Text_Job;
UPROPERTY(BlueprintReadOnly, meta = (BindWidgetOptional), Category = "Zeus|Status|Header") TObjectPtr<UTextBlock> Text_BaseLevel_Value;
UPROPERTY(BlueprintReadOnly, meta = (BindWidgetOptional), Category = "Zeus|Status|Header") TObjectPtr<UTextBlock> Text_JobLevel_Value;
UPROPERTY(BlueprintReadOnly, meta = (BindWidgetOptional), Category = "Zeus|Status") TObjectPtr<UUIPlayerStatus_StatRow> Row_Str;
UPROPERTY(BlueprintReadOnly, meta = (BindWidgetOptional), Category = "Zeus|Status") TObjectPtr<UUIPlayerStatus_StatRow> Row_Agi;
UPROPERTY(BlueprintReadOnly, meta = (BindWidgetOptional), Category = "Zeus|Status") TObjectPtr<UUIPlayerStatus_StatRow> Row_Vit;
UPROPERTY(BlueprintReadOnly, meta = (BindWidgetOptional), Category = "Zeus|Status") TObjectPtr<UUIPlayerStatus_StatRow> Row_Int;
UPROPERTY(BlueprintReadOnly, meta = (BindWidgetOptional), Category = "Zeus|Status") TObjectPtr<UUIPlayerStatus_StatRow> Row_Dex;
UPROPERTY(BlueprintReadOnly, meta = (BindWidgetOptional), Category = "Zeus|Status") TObjectPtr<UUIPlayerStatus_StatRow> Row_Luk;
UPROPERTY(BlueprintReadOnly, meta = (BindWidgetOptional), Category = "Zeus|Status|Derived") TObjectPtr<UUIPlayerStatus_BonusRow> Row_Atk;
UPROPERTY(BlueprintReadOnly, meta = (BindWidgetOptional), Category = "Zeus|Status|Derived") TObjectPtr<UUIPlayerStatus_BonusRow> Row_Matk;
UPROPERTY(BlueprintReadOnly, meta = (BindWidgetOptional), Category = "Zeus|Status|Derived") TObjectPtr<UUIPlayerStatus_BonusRow> Row_Def;
UPROPERTY(BlueprintReadOnly, meta = (BindWidgetOptional), Category = "Zeus|Status|Derived") TObjectPtr<UUIPlayerStatus_BonusRow> Row_Mdef;
UPROPERTY(BlueprintReadOnly, meta = (BindWidgetOptional), Category = "Zeus|Status|Derived") TObjectPtr<UUIPlayerStatus_BonusRow> Row_Hit;
UPROPERTY(BlueprintReadOnly, meta = (BindWidgetOptional), Category = "Zeus|Status|Derived") TObjectPtr<UUIPlayerStatus_BonusRow> Row_Flee;
UPROPERTY(BlueprintReadOnly, meta = (BindWidgetOptional), Category = "Zeus|Status|Derived") TObjectPtr<UUIPlayerStatus_BonusRow> Row_Crit;
UPROPERTY(BlueprintReadOnly, meta = (BindWidgetOptional), Category = "Zeus|Status|Derived") TObjectPtr<UUIPlayerStatus_BonusRow> Row_Aspd;
UPROPERTY(BlueprintReadOnly, meta = (BindWidgetOptional), Category = "Zeus|Status") TObjectPtr<UButton> CloseBtn;
private:
void BindToLocalPlayer();
void UnbindFromComponent();
void RefreshFromSnapshot(const FZMMOAttributesSnapshot& Snap);
void SetAllPrimaryRowsPlusEnabled(bool bEnabled);
void BindRow(UUIPlayerStatus_StatRow* Row, int32 StatId, const TCHAR* IconLetter, const TCHAR* DisplayName);
UFUNCTION() void HandleAttributesChanged(const FZMMOAttributesSnapshot& Snapshot);
UFUNCTION() void HandleStatAllocReply(bool bAccepted, int32 Reason);
UFUNCTION() void HandleRowPlusClicked(int32 StatId);
UFUNCTION() void OnCloseClicked();
UPROPERTY(Transient)
TWeakObjectPtr<UZMMOAttributeComponent> BoundComponent;
/** Trava local pra evitar spam-click: vira true ao clicar, false ao
* receber StatAllocReply (ou snapshot novo, o que vier primeiro). */
bool bPlusButtonsLocked = false;
};