feat(attributes/ui): Status Window com alocação STR/AGI/VIT/INT/DEX/LUK + derivados

- ZMMOStatusWindowWidget (UCommonActivatableWidget, modo Menu): grid 2 colunas
  estilo RO. Esquerda: stats primários com botões + por stat (RequestStatAlloc).
  Direita: derivados ATK/MATK/DEF/MDEF/HIT/FLEE/CRIT/ASPD vindos do snapshot
  do server — cliente NUNCA recalcula (anti-cheat foundation).
- Botão + envia C_STAT_ALLOC → server valida cost RO (floor((stat-1)/10)+2),
  aplica + recalcula derivados + SaveCharFull async, manda
  S_ATTRIBUTE_SNAPSHOT_FULL + reply. UI atualiza em tempo real via
  OnAttributesChanged.
- ZMMOAttributeComponent ganha RequestStatAlloc + NotifyStatAllocReply +
  delegate OnStatAllocReply para feedback de rejeição.
- ZMMOAttributeNetworkHandler: HandleStatAllocReply roteia pro componente do
  player local (reply não traz EntityId — sempre quem fez o request).
- PlayerController: hotkey Alt+A (FInputChord legacy + BindKey, sem precisar
  de IA asset). ToggleStatusWindow via UUIInGameFlowSubsystem.
- WBP_StatusWindow: layout 2 colunas via MCP set_widget_tree (43 widgets) +
  BgBorder escuro semi-transparente + centralizado no overlay.
- DA_InGameScreenSet: StatusWindow → WBP_StatusWindow.
This commit is contained in:
2026-05-23 11:22:58 -03:00
parent 396223e2a8
commit f585b0d2b9
10 changed files with 372 additions and 0 deletions

View File

@@ -1,12 +1,17 @@
#include "ZMMOPlayerController.h"
#include "Blueprint/UserWidget.h"
#include "Components/InputComponent.h"
#include "EnhancedInputSubsystems.h"
#include "Engine/GameInstance.h"
#include "Engine/LocalPlayer.h"
#include "Framework/Commands/InputChord.h"
#include "InputMappingContext.h"
#include "UObject/ConstructorHelpers.h"
#include "Widgets/Input/SVirtualJoystick.h"
#include "ZMMO.h"
#include "UI/InGame/UIInGameFlowSubsystem.h"
#include "UI/InGameTypes.h"
AZMMOPlayerController::AZMMOPlayerController()
{
@@ -86,6 +91,31 @@ void AZMMOPlayerController::SetupInputComponent()
}
}
}
// Hotkey global Alt+A -> toggle StatusWindow. Uso InputComponent legacy
// (BindKey com FInputChord) pra nao depender de assets InputAction.
// Migrar pra Enhanced Input (IA_OpenStatus, IA_OpenInventory, etc.) quando
// vier outro menu (Inventory I, SkillTree K).
if (InputComponent)
{
FInputKeyBinding& Binding = InputComponent->BindKey(
FInputChord(EKeys::A, /*bShift*/ false, /*bCtrl*/ false, /*bAlt*/ true, /*bCmd*/ false),
IE_Pressed,
this, &AZMMOPlayerController::ToggleStatusWindow);
Binding.bConsumeInput = true;
Binding.bExecuteWhenPaused = true; // dispara mesmo com StatusWindow aberto (CommonUI menu mode)
}
}
void AZMMOPlayerController::ToggleStatusWindow()
{
if (UGameInstance* GI = GetGameInstance())
{
if (UUIInGameFlowSubsystem* Flow = GI->GetSubsystem<UUIInGameFlowSubsystem>())
{
Flow->ToggleScreen(EZMMOInGameUIState::StatusWindow);
}
}
}
bool AZMMOPlayerController::ShouldUseTouchControls() const

View File

@@ -47,4 +47,7 @@ protected:
virtual void SetupInputComponent() override;
bool ShouldUseTouchControls() const;
/** Hotkey Alt+A → toggle StatusWindow via UUIInGameFlowSubsystem. */
void ToggleStatusWindow();
};

View File

@@ -0,0 +1,162 @@
#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 — sempre vem prontos do servidor (camadas equip/buff ja somadas).
// CritX10 vem internamente x10 (1.0 = 10); divide pra exibicao com 1 casa.
if (AtkText) AtkText->SetText(FText::FromString(FString::Printf(TEXT("ATK %d"), S.Atk)));
if (MatkText) MatkText->SetText(FText::FromString(FString::Printf(TEXT("MATK %d"), S.Matk)));
if (DefText) DefText->SetText(FText::FromString(FString::Printf(TEXT("DEF %d"), S.Def)));
if (MdefText) MdefText->SetText(FText::FromString(FString::Printf(TEXT("MDEF %d"), S.Mdef)));
if (HitText) HitText->SetText(FText::FromString(FString::Printf(TEXT("HIT %d"), S.Hit)));
if (FleeText) FleeText->SetText(FText::FromString(FString::Printf(TEXT("FLEE %d"), S.Flee)));
if (CritText)
{
const int32 CritInt = S.CritX10 / 10;
const int32 CritFrac = S.CritX10 % 10;
CritText->SetText(FText::FromString(FString::Printf(TEXT("CRIT %d.%d"), CritInt, CritFrac)));
}
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

@@ -0,0 +1,106 @@
#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

@@ -1,5 +1,8 @@
#include "ZMMOAttributeComponent.h"
#include "Engine/GameInstance.h"
#include "ZeusNetworkSubsystem.h"
UZMMOAttributeComponent::UZMMOAttributeComponent()
{
PrimaryComponentTick.bCanEverTick = false;
@@ -34,3 +37,19 @@ void UZMMOAttributeComponent::NotifyLevelUp(int32 NewBaseLevel, int32 StatusPoin
// aqui apenas dispara o delegate para efeitos visuais imediatos.
OnLevelUp.Broadcast(NewBaseLevel, StatusPointDelta);
}
void UZMMOAttributeComponent::NotifyStatAllocReply(bool bAccepted, int32 Reason)
{
OnStatAllocReply.Broadcast(bAccepted, Reason);
}
void UZMMOAttributeComponent::RequestStatAlloc(int32 StatId, int32 Amount)
{
UWorld* World = GetWorld();
if (!World) { return; }
UGameInstance* GI = World->GetGameInstance();
if (!GI) { return; }
UZeusNetworkSubsystem* Net = GI->GetSubsystem<UZeusNetworkSubsystem>();
if (!Net) { return; }
Net->SendStatAlloc(StatId, Amount);
}

View File

@@ -3,6 +3,7 @@
#include "Engine/GameInstance.h"
#include "Engine/World.h"
#include "GameFramework/GameStateBase.h"
#include "GameFramework/PlayerController.h"
#include "GameFramework/PlayerState.h"
#include "ZMMOAttributeComponent.h"
#include "ZMMOAttributeTypes.h"
@@ -91,6 +92,8 @@ void UZMMOAttributeNetworkHandler::Initialize(FSubsystemCollectionBase& Collecti
this, &UZMMOAttributeNetworkHandler::HandleHpSpUpdate);
LevelUpHandle = Net->OnLevelUp.AddUObject(
this, &UZMMOAttributeNetworkHandler::HandleLevelUp);
StatAllocReplyHandle = Net->OnStatAllocReply.AddUObject(
this, &UZMMOAttributeNetworkHandler::HandleStatAllocReply);
}
}
@@ -113,6 +116,11 @@ void UZMMOAttributeNetworkHandler::Deinitialize()
Net->OnLevelUp.Remove(LevelUpHandle);
LevelUpHandle.Reset();
}
if (StatAllocReplyHandle.IsValid())
{
Net->OnStatAllocReply.Remove(StatAllocReplyHandle);
StatAllocReplyHandle.Reset();
}
}
Super::Deinitialize();
}
@@ -179,3 +187,22 @@ void UZMMOAttributeNetworkHandler::HandleLevelUp(int32 EntityId, int32 NewBaseLe
Comp->NotifyLevelUp(NewBaseLevel, StatusPointDelta);
}
}
void UZMMOAttributeNetworkHandler::HandleStatAllocReply(bool bAccepted, int32 Reason)
{
// S_ATTRIBUTE_STAT_ALLOC_OK nao traz EntityId no payload — sempre do
// player local que fez o request. Busca o AttributeComponent via PC.
if (UZMMOAttributeComponent* Comp = FindLocalPlayerAttributeComponent())
{
Comp->NotifyStatAllocReply(bAccepted, Reason);
}
}
UZMMOAttributeComponent* UZMMOAttributeNetworkHandler::FindLocalPlayerAttributeComponent() const
{
UWorld* World = GetWorld();
if (!World) { return nullptr; }
APlayerController* PC = World->GetFirstPlayerController();
if (!PC || !PC->PlayerState) { return nullptr; }
return PC->PlayerState->FindComponentByClass<UZMMOAttributeComponent>();
}

View File

@@ -9,6 +9,10 @@ DECLARE_DYNAMIC_MULTICAST_DELEGATE_OneParam(FZMMOOnAttributesChanged, const FZMM
DECLARE_DYNAMIC_MULTICAST_DELEGATE_TwoParams(FZMMOOnHpSpChanged, int32, Hp, int32, Sp);
DECLARE_DYNAMIC_MULTICAST_DELEGATE_TwoParams(FZMMOOnLevelUp, int32, NewBaseLevel, int32, StatusPointDelta);
/// Resposta server ao C_STAT_ALLOC. Reason e' EAllocRejectReason (None=0,
/// InvalidStat=1, InvalidAmount=2, NotEnoughPoints=3, StatCapped=4, NoSession=5).
DECLARE_DYNAMIC_MULTICAST_DELEGATE_TwoParams(FZMMOOnStatAllocReply, bool, bAccepted, int32, Reason);
/**
* Componente de atributos do MMO ligado ao ator que o representa em-jogo
* (player local, proxies remotos, NPCs futuros).
@@ -44,6 +48,17 @@ public:
UFUNCTION(BlueprintCallable, Category = "Zeus|Attributes")
void NotifyLevelUp(int32 NewBaseLevel, int32 StatusPointDelta);
/// Notifica resposta server ao C_STAT_ALLOC. UI pode reagir (toast/SFX
/// quando aceito, erro vermelho quando rejeitado).
UFUNCTION(BlueprintCallable, Category = "Zeus|Attributes")
void NotifyStatAllocReply(bool bAccepted, int32 Reason);
/// Envia C_STAT_ALLOC pro server. Wrapper conveniente — busca o
/// UZeusNetworkSubsystem via GameInstance + chama SendStatAlloc.
/// `StatId`: 0=STR, 1=AGI, 2=VIT, 3=INT, 4=DEX, 5=LUK. `Amount`: 1..10.
UFUNCTION(BlueprintCallable, Category = "Zeus|Attributes")
void RequestStatAlloc(int32 StatId, int32 Amount = 1);
/// Seed do EntityId vindo de S_SPAWN_PLAYER local. Chamado pelo
/// `AZMMOPlayerCharacter` antes do primeiro S_ATTRIBUTE_SNAPSHOT_FULL
/// chegar, garantindo que o NetworkHandler consiga rotear via lookup
@@ -80,6 +95,10 @@ public:
UPROPERTY(BlueprintAssignable, Category = "Zeus|Attributes")
FZMMOOnLevelUp OnLevelUp;
/** Resposta ao C_STAT_ALLOC (aceito ou rejeitado). UI reage. */
UPROPERTY(BlueprintAssignable, Category = "Zeus|Attributes")
FZMMOOnStatAllocReply OnStatAllocReply;
protected:
UPROPERTY(BlueprintReadOnly, Category = "Zeus|Attributes")
FZMMOAttributesSnapshot Current;

View File

@@ -36,10 +36,16 @@ private:
void HandleAttributeSnapshotFull(const FZeusAttributesPayload& Payload);
void HandleHpSpUpdate(int32 EntityId, int32 Hp, int32 Sp);
void HandleLevelUp(int32 EntityId, int32 NewBaseLevel, int32 StatusPointDelta);
void HandleStatAllocReply(bool bAccepted, int32 Reason);
UZeusNetworkSubsystem* GetZeusNetSubsystem() const;
/// Acha o AttributeComponent do player local (Pawn->PlayerState).
/// Usado pro StatAllocReply (sem EntityId no payload).
class UZMMOAttributeComponent* FindLocalPlayerAttributeComponent() const;
FDelegateHandle SnapshotFullHandle;
FDelegateHandle HpSpUpdateHandle;
FDelegateHandle LevelUpHandle;
FDelegateHandle StatAllocReplyHandle;
};