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>
This commit is contained in:
2026-05-23 04:00:32 -03:00
parent 0e96956a17
commit 7d83dd800f
16 changed files with 777 additions and 1 deletions

View File

@@ -0,0 +1,36 @@
#include "ZMMOAttributeComponent.h"
UZMMOAttributeComponent::UZMMOAttributeComponent()
{
PrimaryComponentTick.bCanEverTick = false;
}
void UZMMOAttributeComponent::ApplySnapshot(const FZMMOAttributesSnapshot& InSnapshot)
{
const int32 OldHp = Current.Hp;
const int32 OldSp = Current.Sp;
Current = InSnapshot;
OnAttributesChanged.Broadcast(Current);
if (OldHp != Current.Hp || OldSp != Current.Sp)
{
OnHpSpChanged.Broadcast(Current.Hp, Current.Sp);
}
}
void UZMMOAttributeComponent::ApplyHpSpUpdate(int32 NewHp, int32 NewSp)
{
if (Current.Hp == NewHp && Current.Sp == NewSp)
{
return;
}
Current.Hp = NewHp;
Current.Sp = NewSp;
OnHpSpChanged.Broadcast(Current.Hp, Current.Sp);
}
void UZMMOAttributeComponent::NotifyLevelUp(int32 NewBaseLevel, int32 StatusPointDelta)
{
// O snapshot subsequente vai trazer todos os outros campos atualizados —
// aqui apenas dispara o delegate para efeitos visuais imediatos.
OnLevelUp.Broadcast(NewBaseLevel, StatusPointDelta);
}

View File

@@ -0,0 +1,182 @@
#include "ZMMOAttributeNetworkHandler.h"
#include "Engine/GameInstance.h"
#include "Engine/World.h"
#include "EngineUtils.h" // TActorIterator
#include "GameFramework/Actor.h"
#include "ZMMOAttributeComponent.h"
#include "ZMMOAttributeTypes.h"
#include "ZeusNetworkSubsystem.h"
namespace
{
// Helper privado — converte o payload binario do plugin pra USTRUCT
// Blueprint que o componente exibe.
FZMMOAttributesSnapshot ToSnapshot(const FZeusAttributesPayload& P)
{
FZMMOAttributesSnapshot S;
S.EntityId = P.EntityId;
S.ClassId = P.ClassId;
S.BaseLevel = P.BaseLevel;
S.BaseExp = P.BaseExp;
S.JobLevel = P.JobLevel;
S.JobExp = P.JobExp;
S.Str = P.Str;
S.Agi = P.Agi;
S.Vit = P.Vit;
S.Int = P.Int;
S.Dex = P.Dex;
S.Luk = P.Luk;
S.StatusPoint = P.StatusPoint;
S.SkillPoint = P.SkillPoint;
S.Hp = P.Hp;
S.MaxHp = P.MaxHp;
S.Sp = P.Sp;
S.MaxSp = P.MaxSp;
S.Money = P.Money;
S.Atk = P.Atk;
S.Matk = P.Matk;
S.Def = P.Def;
S.Mdef = P.Mdef;
S.Hit = P.Hit;
S.Flee = P.Flee;
S.CritX10 = P.CritX10;
S.Aspd = P.Aspd;
return S;
}
// V1 — resolve por EntityId varrendo atores do mundo procurando um que
// tenha `UZMMOAttributeComponent`. Funciona para PIE com 1-2 players +
// proxies. Otimizacao futura: usar `UZMMOWorldSubsystem::GetActorByEntityId`
// (precisa expor) ou cache local de `EntityId -> UAttributeComponent*`.
//
// Convencao: `IZMMOEntityInterface::GetZMMOEntityId()` retorna o EntityId
// autoritativo do servidor. Nao depender da interface aqui para manter
// ZMMOAttributes sem `#include` do modulo ZMMO core — varremos `Tags`
// alternativamente, mas o caminho mais simples e' a propria component
// keys de `EntityId` no proprio componente (TODO Fase 2: armazenar
// EntityId no componente quando snapshot chega).
UZMMOAttributeComponent* FindComponentForEntity(UWorld* World, int32 EntityId)
{
if (!World || EntityId == 0) { return nullptr; }
for (TActorIterator<AActor> It(World); It; ++It)
{
AActor* Actor = *It;
if (!Actor) { continue; }
UZMMOAttributeComponent* Comp = Actor->FindComponentByClass<UZMMOAttributeComponent>();
if (!Comp) { continue; }
if (Comp->GetSnapshot().EntityId == EntityId)
{
return Comp;
}
}
return nullptr;
}
}
bool UZMMOAttributeNetworkHandler::ShouldCreateSubsystem(UObject* Outer) const
{
// Cria apenas para mundos de gameplay (PIE/Game), nao para editor preview.
UWorld* World = Cast<UWorld>(Outer);
if (!World) { return false; }
return World->WorldType == EWorldType::Game || World->WorldType == EWorldType::PIE;
}
void UZMMOAttributeNetworkHandler::Initialize(FSubsystemCollectionBase& Collection)
{
Super::Initialize(Collection);
if (UZeusNetworkSubsystem* Net = GetZeusNetSubsystem())
{
SnapshotFullHandle = Net->OnAttributeSnapshotFull.AddUObject(
this, &UZMMOAttributeNetworkHandler::HandleAttributeSnapshotFull);
HpSpUpdateHandle = Net->OnHpSpUpdate.AddUObject(
this, &UZMMOAttributeNetworkHandler::HandleHpSpUpdate);
LevelUpHandle = Net->OnLevelUp.AddUObject(
this, &UZMMOAttributeNetworkHandler::HandleLevelUp);
}
}
void UZMMOAttributeNetworkHandler::Deinitialize()
{
if (UZeusNetworkSubsystem* Net = GetZeusNetSubsystem())
{
if (SnapshotFullHandle.IsValid())
{
Net->OnAttributeSnapshotFull.Remove(SnapshotFullHandle);
SnapshotFullHandle.Reset();
}
if (HpSpUpdateHandle.IsValid())
{
Net->OnHpSpUpdate.Remove(HpSpUpdateHandle);
HpSpUpdateHandle.Reset();
}
if (LevelUpHandle.IsValid())
{
Net->OnLevelUp.Remove(LevelUpHandle);
LevelUpHandle.Reset();
}
}
Super::Deinitialize();
}
UZeusNetworkSubsystem* UZMMOAttributeNetworkHandler::GetZeusNetSubsystem() const
{
const UWorld* World = GetWorld();
if (!World) { return nullptr; }
UGameInstance* GI = World->GetGameInstance();
if (!GI) { return nullptr; }
return GI->GetSubsystem<UZeusNetworkSubsystem>();
}
void UZMMOAttributeNetworkHandler::HandleAttributeSnapshotFull(const FZeusAttributesPayload& Payload)
{
UWorld* World = GetWorld();
if (!World) { return; }
UZMMOAttributeComponent* Comp = FindComponentForEntity(World, Payload.EntityId);
if (!Comp)
{
// V1 fallback: aplica ao primeiro UZMMOAttributeComponent encontrado
// (player local). Isso resolve o caso do primeiro snapshot chegar
// antes do `EntityId` estar registrado no componente (que so' acontece
// apos o primeiro snapshot — chicken-and-egg). Apos o primeiro apply,
// `Current.EntityId` ja' bate e o caminho normal funciona.
for (TActorIterator<AActor> It(World); It; ++It)
{
AActor* Actor = *It;
if (!Actor) { continue; }
Comp = Actor->FindComponentByClass<UZMMOAttributeComponent>();
if (Comp && Comp->GetSnapshot().EntityId == 0)
{
break;
}
Comp = nullptr;
}
}
if (Comp)
{
Comp->ApplySnapshot(ToSnapshot(Payload));
}
}
void UZMMOAttributeNetworkHandler::HandleHpSpUpdate(int32 EntityId, int32 Hp, int32 Sp)
{
UWorld* World = GetWorld();
if (!World) { return; }
UZMMOAttributeComponent* Comp = FindComponentForEntity(World, EntityId);
if (Comp)
{
Comp->ApplyHpSpUpdate(Hp, Sp);
}
}
void UZMMOAttributeNetworkHandler::HandleLevelUp(int32 EntityId, int32 NewBaseLevel, int32 StatusPointDelta)
{
UWorld* World = GetWorld();
if (!World) { return; }
UZMMOAttributeComponent* Comp = FindComponentForEntity(World, EntityId);
if (Comp)
{
Comp->NotifyLevelUp(NewBaseLevel, StatusPointDelta);
}
}

View File

@@ -0,0 +1,13 @@
#include "ZMMOAttributesModule.h"
#include "Modules/ModuleManager.h"
void FZMMOAttributesModule::StartupModule()
{
}
void FZMMOAttributesModule::ShutdownModule()
{
}
IMPLEMENT_MODULE(FZMMOAttributesModule, ZMMOAttributes)

View File

@@ -0,0 +1,101 @@
#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)));
}
}
}

View File

@@ -0,0 +1,86 @@
#pragma once
#include "CoreMinimal.h"
#include "Components/ActorComponent.h"
#include "ZMMOAttributeTypes.h"
#include "ZMMOAttributeComponent.generated.h"
DECLARE_DYNAMIC_MULTICAST_DELEGATE_OneParam(FZMMOOnAttributesChanged, const FZMMOAttributesSnapshot&, Snapshot);
DECLARE_DYNAMIC_MULTICAST_DELEGATE_TwoParams(FZMMOOnHpSpChanged, int32, Hp, int32, Sp);
DECLARE_DYNAMIC_MULTICAST_DELEGATE_TwoParams(FZMMOOnLevelUp, int32, NewBaseLevel, int32, StatusPointDelta);
/**
* Componente de atributos do MMO ligado ao ator que o representa em-jogo
* (player local, proxies remotos, NPCs futuros).
*
* Estado authoritative vive no servidor (CharRuntimeStatus em
* Game/MMO/Modules/AttributeSystem). Cliente apenas armazena o ultimo
* snapshot recebido e expoe delegates para UI/HUD reagir.
*
* Roteamento server → componente: `UZMMOAttributeNetworkHandler`
* (UWorldSubsystem) liga em `UZeusNetworkSubsystem::OnAttributeSnapshotFull`,
* resolve `EntityId -> AActor*` via `UZMMOWorldSubsystem` e chama
* `ApplySnapshot` no componente do ator.
*/
UCLASS(ClassGroup=(ZMMO), meta=(BlueprintSpawnableComponent),
hidecategories=(Activation, Collision, Cooking, Object, Replication))
class ZMMOATTRIBUTES_API UZMMOAttributeComponent : public UActorComponent
{
GENERATED_BODY()
public:
UZMMOAttributeComponent();
/// Aplica snapshot full recebido do servidor. Substitui `Current` e
/// dispara `OnAttributesChanged` + `OnHpSpChanged` (se hp/sp mudaram).
UFUNCTION(BlueprintCallable, Category = "Zeus|Attributes")
void ApplySnapshot(const FZMMOAttributesSnapshot& InSnapshot);
/// Aplica apenas delta de HP/SP (sem mexer em outros campos).
UFUNCTION(BlueprintCallable, Category = "Zeus|Attributes")
void ApplyHpSpUpdate(int32 NewHp, int32 NewSp);
/// Notifica level up — o snapshot subsequente atualiza demais campos.
UFUNCTION(BlueprintCallable, Category = "Zeus|Attributes")
void NotifyLevelUp(int32 NewBaseLevel, int32 StatusPointDelta);
/// 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
/// por EntityId desde o inicio.
UFUNCTION(BlueprintCallable, Category = "Zeus|Attributes")
void SeedEntityId(int32 InEntityId) { Current.EntityId = InEntityId; }
UFUNCTION(BlueprintPure, Category = "Zeus|Attributes")
const FZMMOAttributesSnapshot& GetSnapshot() const { return Current; }
UFUNCTION(BlueprintPure, Category = "Zeus|Attributes")
float GetHpRatio() const
{
return Current.MaxHp > 0 ? static_cast<float>(Current.Hp) / static_cast<float>(Current.MaxHp) : 0.f;
}
UFUNCTION(BlueprintPure, Category = "Zeus|Attributes")
float GetSpRatio() const
{
return Current.MaxSp > 0 ? static_cast<float>(Current.Sp) / static_cast<float>(Current.MaxSp) : 0.f;
}
// === Delegates Blueprint ===
/** Snapshot novo aplicado. Use isto na UI para refresh geral. */
UPROPERTY(BlueprintAssignable, Category = "Zeus|Attributes")
FZMMOOnAttributesChanged OnAttributesChanged;
/** HP ou SP mudou (efêmero ou via snapshot). Para barras animadas. */
UPROPERTY(BlueprintAssignable, Category = "Zeus|Attributes")
FZMMOOnHpSpChanged OnHpSpChanged;
/** Subiu de nível. Para efeitos visuais ("LEVEL UP!" toast). */
UPROPERTY(BlueprintAssignable, Category = "Zeus|Attributes")
FZMMOOnLevelUp OnLevelUp;
protected:
UPROPERTY(BlueprintReadOnly, Category = "Zeus|Attributes")
FZMMOAttributesSnapshot Current;
};

View File

@@ -0,0 +1,45 @@
#pragma once
#include "CoreMinimal.h"
#include "Subsystems/WorldSubsystem.h"
#include "ZeusAttributesPayload.h"
#include "ZMMOAttributeNetworkHandler.generated.h"
class UZeusNetworkSubsystem;
/**
* Ponte entre o `UZeusNetworkSubsystem` (GameInstanceSubsystem do plugin
* ZeusNetwork) e os `UZMMOAttributeComponent` ligados aos atores do mundo.
*
* Por que UWorldSubsystem (e nao GameInstanceSubsystem)?
* - O registry de atores (`UZMMOWorldSubsystem::GetActorByEntityId`) e'
* World-scoped, reset a cada OpenLevel.
* - World subsystem so existe enquanto ha mundo carregado, o que e' exato
* quando precisamos rotear snapshots (no front-end nao ha pawn).
*
* Lifecycle: `Initialize` binda nos 3 delegates do ZeusNetworkSubsystem;
* `Deinitialize` unbinda. Conversao `FZeusAttributesPayload ->
* FZMMOAttributesSnapshot` (USTRUCT Blueprint) acontece aqui.
*/
UCLASS()
class ZMMOATTRIBUTES_API UZMMOAttributeNetworkHandler : public UWorldSubsystem
{
GENERATED_BODY()
public:
virtual void Initialize(FSubsystemCollectionBase& Collection) override;
virtual void Deinitialize() override;
virtual bool ShouldCreateSubsystem(UObject* Outer) const override;
private:
void HandleAttributeSnapshotFull(const FZeusAttributesPayload& Payload);
void HandleHpSpUpdate(int32 EntityId, int32 Hp, int32 Sp);
void HandleLevelUp(int32 EntityId, int32 NewBaseLevel, int32 StatusPointDelta);
UZeusNetworkSubsystem* GetZeusNetSubsystem() const;
FDelegateHandle SnapshotFullHandle;
FDelegateHandle HpSpUpdateHandle;
FDelegateHandle LevelUpHandle;
};

View File

@@ -0,0 +1,63 @@
#pragma once
#include "CoreMinimal.h"
#include "ZMMOAttributeTypes.generated.h"
/**
* Snapshot completo dos atributos do char vindo do server via
* S_ATTRIBUTE_SNAPSHOT_FULL (opcode 1200).
*
* Cliente NUNCA recalcula derivados — apenas exibe o que recebe. Os campos
* HP/MaxHp/SP/MaxSp ja sao os "efetivos" (base + equip + buff somados no
* servidor); o cliente nao ve as 3 camadas separadas.
*
* Espelha `FZeusAttributesPayload` do plugin ZeusNetwork + a serializacao
* em `Server/.../AttributeService.cpp::WriteSnapshotPayload`.
*/
USTRUCT(BlueprintType)
struct ZMMOATTRIBUTES_API FZMMOAttributesSnapshot
{
GENERATED_BODY()
// === Identidade + classe ===
UPROPERTY(BlueprintReadOnly, Category = "Zeus|Attributes") int32 EntityId = 0;
UPROPERTY(BlueprintReadOnly, Category = "Zeus|Attributes") int32 ClassId = 0;
// === Progressao ===
UPROPERTY(BlueprintReadOnly, Category = "Zeus|Attributes") int32 BaseLevel = 1;
UPROPERTY(BlueprintReadOnly, Category = "Zeus|Attributes") int64 BaseExp = 0;
UPROPERTY(BlueprintReadOnly, Category = "Zeus|Attributes") int32 JobLevel = 1;
UPROPERTY(BlueprintReadOnly, Category = "Zeus|Attributes") int64 JobExp = 0;
// === 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 Agi = 1;
UPROPERTY(BlueprintReadOnly, Category = "Zeus|Attributes|Stats") int32 Vit = 1;
UPROPERTY(BlueprintReadOnly, Category = "Zeus|Attributes|Stats") int32 Int = 1;
UPROPERTY(BlueprintReadOnly, Category = "Zeus|Attributes|Stats") int32 Dex = 1;
UPROPERTY(BlueprintReadOnly, Category = "Zeus|Attributes|Stats") int32 Luk = 1;
// === Pontos nao gastos ===
UPROPERTY(BlueprintReadOnly, Category = "Zeus|Attributes") int32 StatusPoint = 0;
UPROPERTY(BlueprintReadOnly, Category = "Zeus|Attributes") int32 SkillPoint = 0;
// === Pool efetivo (camadas 1+2+3 somadas no servidor) ===
UPROPERTY(BlueprintReadOnly, Category = "Zeus|Attributes") int32 Hp = 0;
UPROPERTY(BlueprintReadOnly, Category = "Zeus|Attributes") int32 MaxHp = 0;
UPROPERTY(BlueprintReadOnly, Category = "Zeus|Attributes") int32 Sp = 0;
UPROPERTY(BlueprintReadOnly, Category = "Zeus|Attributes") int32 MaxSp = 0;
// === Moeda ===
UPROPERTY(BlueprintReadOnly, Category = "Zeus|Attributes") int64 Money = 0;
// === Derivados (efetivos; servidor sempre recalcula) ===
UPROPERTY(BlueprintReadOnly, Category = "Zeus|Attributes|Derived") int32 Atk = 0;
UPROPERTY(BlueprintReadOnly, Category = "Zeus|Attributes|Derived") int32 Matk = 0;
UPROPERTY(BlueprintReadOnly, Category = "Zeus|Attributes|Derived") int32 Def = 0;
UPROPERTY(BlueprintReadOnly, Category = "Zeus|Attributes|Derived") int32 Mdef = 0;
UPROPERTY(BlueprintReadOnly, Category = "Zeus|Attributes|Derived") int32 Hit = 0;
UPROPERTY(BlueprintReadOnly, Category = "Zeus|Attributes|Derived") int32 Flee = 0;
/** Critico × 10 internamente (10 = 1.0). Divida por 10 para exibir. */
UPROPERTY(BlueprintReadOnly, Category = "Zeus|Attributes|Derived") int32 CritX10 = 0;
UPROPERTY(BlueprintReadOnly, Category = "Zeus|Attributes|Derived") int32 Aspd = 0;
};

View File

@@ -0,0 +1,19 @@
#pragma once
#include "CoreMinimal.h"
#include "Modules/ModuleInterface.h"
/**
* Sub-modulo cliente do AttributeSystem.
*
* Espelho de `Server/ZeusServerEngine/Game/MMO/Modules/AttributeSystem/`.
* Roda como modulo Runtime carregado em `PreDefault` para que o
* `UAttributeComponent` esteja registrado antes do `ZMMO` instanciar o
* player character.
*/
class FZMMOAttributesModule : public IModuleInterface
{
public:
virtual void StartupModule() override;
virtual void ShutdownModule() override;
};

View File

@@ -0,0 +1,85 @@
#pragma once
#include "Blueprint/UserWidget.h"
#include "CoreMinimal.h"
#include "ZMMOAttributeTypes.h"
#include "ZMMOHudHpSpWidget.generated.h"
class UProgressBar;
class UTextBlock;
class UZMMOAttributeComponent;
/**
* Widget base do HUD de atributos (HP/SP/level). Spawnada por
* `AZMMOPlayerCharacter::BeginPlay` quando o pawn local virou autoritativo.
*
* Layout (cor, textos, ancoras) vem do WBP filho via UMG.
* Comportamento (binding com AttributeComponent + refresh on delegate)
* vive aqui em C++ — permite que outros modulos consumam esta API
* sem precisar abrir o WBP. Sub-classe via Blueprint apenas para skin
* visual e disposicao dos sub-widgets.
*
* Convencao `meta=(BindWidget)`: o WBP filho DEVE ter widgets com os
* mesmos nomes (HpBar, SpBar). `BindWidgetOptional` permite ao WBP
* omitir o widget (ex.: HUD compacto sem texto numerico).
*/
UCLASS(Abstract, Blueprintable, BlueprintType)
class ZMMOATTRIBUTES_API UZMMOHudHpSpWidget : public UUserWidget
{
GENERATED_BODY()
public:
/**
* Liga o widget a um `UZMMOAttributeComponent`. Subscreve aos delegates
* e faz um refresh imediato com o snapshot corrente. Idempotente —
* chamar com outro componente desliga do anterior.
* Se `InComponent` for nullptr, desliga sem religar.
*/
UFUNCTION(BlueprintCallable, Category = "Zeus|HUD")
void BindToAttributeComponent(UZMMOAttributeComponent* InComponent);
UFUNCTION(BlueprintPure, Category = "Zeus|HUD")
UZMMOAttributeComponent* GetBoundComponent() const { return BoundComponent.Get(); }
protected:
virtual void NativeDestruct() override;
/**
* Hook BlueprintNativeEvent para skin visual reagir a um snapshot novo
* (cores, animacao de level up, etc.). Implementacao default em C++
* apenas atualiza progress bars + textos via `BindWidget`/`BindWidgetOptional`.
*/
UFUNCTION(BlueprintNativeEvent, Category = "Zeus|HUD")
void OnSnapshotApplied(const FZMMOAttributesSnapshot& Snapshot);
virtual void OnSnapshotApplied_Implementation(const FZMMOAttributesSnapshot& Snapshot);
UFUNCTION(BlueprintNativeEvent, Category = "Zeus|HUD")
void OnHpSpDelta(int32 Hp, int32 Sp);
virtual void OnHpSpDelta_Implementation(int32 Hp, int32 Sp);
// === BindWidget — devem existir no WBP com EXATAMENTE estes nomes ===
UPROPERTY(BlueprintReadOnly, meta = (BindWidget), Category = "Zeus|HUD")
UProgressBar* HpBar = nullptr;
UPROPERTY(BlueprintReadOnly, meta = (BindWidget), Category = "Zeus|HUD")
UProgressBar* SpBar = nullptr;
UPROPERTY(BlueprintReadOnly, meta = (BindWidgetOptional), Category = "Zeus|HUD")
UTextBlock* HpText = nullptr;
UPROPERTY(BlueprintReadOnly, meta = (BindWidgetOptional), Category = "Zeus|HUD")
UTextBlock* SpText = nullptr;
UPROPERTY(BlueprintReadOnly, meta = (BindWidgetOptional), Category = "Zeus|HUD")
UTextBlock* LevelText = nullptr;
private:
UFUNCTION()
void HandleAttributesChanged(const FZMMOAttributesSnapshot& Snapshot);
UFUNCTION()
void HandleHpSpChanged(int32 Hp, int32 Sp);
UPROPERTY()
TWeakObjectPtr<UZMMOAttributeComponent> BoundComponent;
};

View File

@@ -0,0 +1,29 @@
using UnrealBuildTool;
public class ZMMOAttributes : ModuleRules
{
public ZMMOAttributes(ReadOnlyTargetRules Target) : base(Target)
{
PCHUsage = PCHUsageMode.UseExplicitOrSharedPCHs;
PublicDependencyModuleNames.AddRange(new string[] {
"Core",
"CoreUObject",
"Engine",
"UMG",
"Slate",
"ZeusNetwork"
});
PrivateDependencyModuleNames.AddRange(new string[] {
"SlateCore"
});
// Dependencias entre modulos (vide
// Server/ZeusServerEngine/Game/MMO/Modules/AttributeSystem/module.json
// — espelho do manifest no cliente):
// - engine: Core, CoreUObject, Engine
// - modules: [] (este e' modulo de base do MMO)
// - plugin ZeusNetwork: para opcodes + delegates
}
}

View File

@@ -0,0 +1,19 @@
{
"name": "ZMMOAttributes",
"gameType": "MMO",
"version": "0.1.0",
"side": "client",
"dependencies": {
"engine": ["Core", "CoreUObject", "Engine", "UMG", "Slate"],
"plugins": ["ZeusNetwork"],
"modules": []
},
"publicHeaders": [
"ZMMOAttributesModule.h",
"ZMMOAttributeTypes.h",
"ZMMOAttributeComponent.h",
"ZMMOAttributeNetworkHandler.h"
],
"loadOrder": 100,
"description": "Cliente do AttributeSystem do MMO. UActorComponent + UWorldSubsystem que recebe S_ATTRIBUTE_SNAPSHOT_FULL/HP_SP_UPDATE/LEVEL_UP do ZeusNetworkSubsystem e expõe pra UI/HUD."
}