From 7d83dd800f8487ca1cab85b6726b01424f1d03ea Mon Sep 17 00:00:00 2001 From: Mateus Rodrigues Date: Sat, 23 May 2026 04:00:32 -0300 Subject: [PATCH] =?UTF-8?q?feat(attributes):=20cliente=20Fase=201=20?= =?UTF-8?q?=E2=80=94=20sub-modulo=20ZMMOAttributes=20+=20HUD=20HP/SP?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 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 --- Content/ZMMO/UI/HUD/WBP_HUD_HpSpBar.uasset | Bin 0 -> 33509 bytes .../ZMMO/Game/Entity/ZMMOPlayerCharacter.cpp | 73 +++++++ Source/ZMMO/Game/Entity/ZMMOPlayerCharacter.h | 19 ++ Source/ZMMO/ZMMO.Build.cs | 3 +- .../Private/ZMMOAttributeComponent.cpp | 36 ++++ .../Private/ZMMOAttributeNetworkHandler.cpp | 182 ++++++++++++++++++ .../Private/ZMMOAttributesModule.cpp | 13 ++ .../Private/ZMMOHudHpSpWidget.cpp | 101 ++++++++++ .../Public/ZMMOAttributeComponent.h | 86 +++++++++ .../Public/ZMMOAttributeNetworkHandler.h | 45 +++++ .../Public/ZMMOAttributeTypes.h | 63 ++++++ .../Public/ZMMOAttributesModule.h | 19 ++ .../ZMMOAttributes/Public/ZMMOHudHpSpWidget.h | 85 ++++++++ Source/ZMMOAttributes/ZMMOAttributes.Build.cs | 29 +++ Source/ZMMOAttributes/module.json | 19 ++ ZMMO.uproject | 5 + 16 files changed, 777 insertions(+), 1 deletion(-) create mode 100644 Content/ZMMO/UI/HUD/WBP_HUD_HpSpBar.uasset create mode 100644 Source/ZMMOAttributes/Private/ZMMOAttributeComponent.cpp create mode 100644 Source/ZMMOAttributes/Private/ZMMOAttributeNetworkHandler.cpp create mode 100644 Source/ZMMOAttributes/Private/ZMMOAttributesModule.cpp create mode 100644 Source/ZMMOAttributes/Private/ZMMOHudHpSpWidget.cpp create mode 100644 Source/ZMMOAttributes/Public/ZMMOAttributeComponent.h create mode 100644 Source/ZMMOAttributes/Public/ZMMOAttributeNetworkHandler.h create mode 100644 Source/ZMMOAttributes/Public/ZMMOAttributeTypes.h create mode 100644 Source/ZMMOAttributes/Public/ZMMOAttributesModule.h create mode 100644 Source/ZMMOAttributes/Public/ZMMOHudHpSpWidget.h create mode 100644 Source/ZMMOAttributes/ZMMOAttributes.Build.cs create mode 100644 Source/ZMMOAttributes/module.json diff --git a/Content/ZMMO/UI/HUD/WBP_HUD_HpSpBar.uasset b/Content/ZMMO/UI/HUD/WBP_HUD_HpSpBar.uasset new file mode 100644 index 0000000000000000000000000000000000000000..f16ba4f9d38d8b05aff4ffb7d642a59bc2b4d198 GIT binary patch literal 33509 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 literal 0 HcmV?d00001 diff --git a/Source/ZMMO/Game/Entity/ZMMOPlayerCharacter.cpp b/Source/ZMMO/Game/Entity/ZMMOPlayerCharacter.cpp index fcc9ac7..d6cd981 100644 --- a/Source/ZMMO/Game/Entity/ZMMOPlayerCharacter.cpp +++ b/Source/ZMMO/Game/Entity/ZMMOPlayerCharacter.cpp @@ -13,9 +13,12 @@ #include "GameFramework/SpringArmComponent.h" #include "InputAction.h" #include "InputActionValue.h" +#include "Blueprint/UserWidget.h" #include "Subsystems/WorldSubsystem.h" #include "UObject/ConstructorHelpers.h" #include "ZMMO.h" +#include "ZMMOAttributeComponent.h" +#include "ZMMOHudHpSpWidget.h" #include "ZMMOWorldSubsystem.h" #include "ZeusNetworkSubsystem.h" #include "UI/FrontEnd/UIFrontEndFlowSubsystem.h" @@ -52,6 +55,13 @@ AZMMOPlayerCharacter::AZMMOPlayerCharacter() FollowCamera->SetupAttachment(CameraBoom, USpringArmComponent::SocketName); FollowCamera->bUsePawnControlRotation = false; + // AttributeSystem cliente (sub-modulo ZMMOAttributes). Estado authoritative + // vive no servidor (Game/MMO/Modules/AttributeSystem); este componente + // apenas armazena o ultimo snapshot recebido e expoe delegates pro HUD. + // Roteamento server -> componente via UZMMOAttributeNetworkHandler + // (UWorldSubsystem em ZMMOAttributes/Private/). + AttributeComponent = CreateDefaultSubobject(TEXT("AttributeComponent")); + // Defaults visuais (mesh + AnimBP) — alinhados ao ZClientMMO. Permitem ao // motor spawnar AZMMOPlayerCharacter directamente como DefaultPawnClass do // AZMMOGameMode sem exigir um BP filho. Um BP filho continua opcional para @@ -101,6 +111,20 @@ AZMMOPlayerCharacter::AZMMOPlayerCharacter() if (LookActionAsset.Succeeded()) { LookAction = LookActionAsset.Object; } if (MouseLookActionAsset.Succeeded()) { MouseLookAction = MouseLookActionAsset.Object; } if (JumpActionAsset.Succeeded()) { JumpAction = JumpActionAsset.Object; } + + // HUD default — WBP_HUD_HpSpBar herda de UZMMOHudHpSpWidget. BP filho do + // player character pode sobrescrever via EditAnywhere. + static ConstructorHelpers::FClassFinder HudClassFinder( + TEXT("/Game/ZMMO/UI/HUD/WBP_HUD_HpSpBar")); + if (HudClassFinder.Succeeded()) + { + HudHpSpWidgetClass = HudClassFinder.Class; + } + else + { + UE_LOG(LogZMMOPlayer, Warning, + TEXT("WBP_HUD_HpSpBar not found at /Game/ZMMO/UI/HUD/ — HUD desactivado")); + } } void AZMMOPlayerCharacter::BeginPlay() @@ -140,6 +164,11 @@ void AZMMOPlayerCharacter::EndPlay(const EEndPlayReason::Type EndPlayReason) { UnbindZeusSpawnDelegate(); ZeusNetwork = nullptr; + if (HudHpSpWidget) + { + HudHpSpWidget->RemoveFromParent(); + HudHpSpWidget = nullptr; + } Super::EndPlay(EndPlayReason); } @@ -343,6 +372,50 @@ void AZMMOPlayerCharacter::HandleLocalSpawnReady(const int32 InEntityId, const F WorldSubsystem->RegisterLocalEntity(EntityIdAsInt64, this); } } + + // Seed do EntityId no AttributeComponent: o S_ATTRIBUTE_SNAPSHOT_FULL ja + // pode estar em transito (server envia logo apos S_SPAWN_PLAYER) — sem + // este seed o NetworkHandler nao consegue achar o componente certo via + // lookup por EntityId. + if (AttributeComponent) + { + AttributeComponent->SeedEntityId(InEntityId); + } + + // Spawn do HUD de atributos (HP/SP/level). Apenas player local (este + // metodo so' roda quando bIsLocal=true em HandleZeusPlayerSpawned). + UE_LOG(LogZMMOPlayer, Warning, + TEXT("HUD spawn check: HudClass=%s AttrComp=%s HudPrev=%s Controller=%s"), + HudHpSpWidgetClass ? *HudHpSpWidgetClass->GetName() : TEXT("NULL"), + AttributeComponent ? TEXT("OK") : TEXT("NULL"), + HudHpSpWidget ? TEXT("ALREADY_EXISTS") : TEXT("none"), + GetController() ? *GetController()->GetName() : TEXT("NULL")); + + if (HudHpSpWidgetClass && AttributeComponent && !HudHpSpWidget) + { + APlayerController* PC = Cast(GetController()); + if (!PC) + { + UE_LOG(LogZMMOPlayer, Warning, TEXT("HUD spawn: GetController() retornou null/nao-PC. Adiando.")); + } + else + { + HudHpSpWidget = CreateWidget(PC, HudHpSpWidgetClass); + if (!HudHpSpWidget) + { + UE_LOG(LogZMMOPlayer, Error, TEXT("HUD spawn: CreateWidget retornou NULL para classe %s"), + *HudHpSpWidgetClass->GetName()); + } + else + { + HudHpSpWidget->AddToViewport(/*ZOrder=*/10); + HudHpSpWidget->BindToAttributeComponent(AttributeComponent); + UE_LOG(LogZMMOPlayer, Warning, + TEXT("HUD spawn: widget criado e adicionado ao viewport (%s)"), + *HudHpSpWidget->GetName()); + } + } + } } void AZMMOPlayerCharacter::FlushInputAxisToServer(const float DeltaSeconds) diff --git a/Source/ZMMO/Game/Entity/ZMMOPlayerCharacter.h b/Source/ZMMO/Game/Entity/ZMMOPlayerCharacter.h index 4523c42..35c28ca 100644 --- a/Source/ZMMO/Game/Entity/ZMMOPlayerCharacter.h +++ b/Source/ZMMO/Game/Entity/ZMMOPlayerCharacter.h @@ -11,6 +11,8 @@ class USpringArmComponent; class UCameraComponent; class UInputAction; class UZeusNetworkSubsystem; +class UZMMOAttributeComponent; +class UZMMOHudHpSpWidget; struct FInputActionValue; DECLARE_LOG_CATEGORY_EXTERN(LogZMMOPlayer, Log, All); @@ -47,6 +49,11 @@ class ZMMO_API AZMMOPlayerCharacter : public ACharacter, public IZMMOEntityInter UPROPERTY(VisibleAnywhere, BlueprintReadOnly, Category = "Components", meta = (AllowPrivateAccess = "true")) UCameraComponent* FollowCamera; + /** Atributos do MMO (HP/SP/STR/etc). Estado authoritative no server; + * cliente apenas exibe via UI bindando em `OnAttributesChanged`. */ + UPROPERTY(VisibleAnywhere, BlueprintReadOnly, Category = "Components", meta = (AllowPrivateAccess = "true")) + UZMMOAttributeComponent* AttributeComponent; + protected: UPROPERTY(EditAnywhere, Category = "Input") UInputAction* JumpAction; @@ -73,6 +80,12 @@ protected: UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "ZMMO|Networking", meta = (ClampMin = "0.05", ClampMax = "5.0")) float HeartbeatIntervalSec = 0.2f; + /** Classe do HUD de HP/SP. Spawnada quando o pawn local recebe S_SPAWN_PLAYER. + * Default = WBP_HUD_HpSpBar que herda de UZMMOHudHpSpWidget (criado em Fase 1 + * via MCP em /Game/ZMMO/UI/HUD/). Deixar null desativa o HUD. */ + UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "ZMMO|UI") + TSubclassOf HudHpSpWidgetClass; + public: AZMMOPlayerCharacter(); @@ -89,6 +102,7 @@ public: FORCEINLINE USpringArmComponent* GetCameraBoom() const { return CameraBoom; } FORCEINLINE UCameraComponent* GetFollowCamera() const { return FollowCamera; } + FORCEINLINE UZMMOAttributeComponent* GetAttributeComponent() const { return AttributeComponent; } protected: virtual void BeginPlay() override; @@ -159,4 +173,9 @@ private: float SendAccumulatorSec = 0.0f; float TimeSinceLastSendSec = 0.0f; int32 InputSequence = 0; + + /** Instancia do HUD spawnada no spawn local. nullptr ate o spawn ou se + * HudHpSpWidgetClass nao foi setado. */ + UPROPERTY(Transient) + TObjectPtr HudHpSpWidget; }; diff --git a/Source/ZMMO/ZMMO.Build.cs b/Source/ZMMO/ZMMO.Build.cs index 1550ba0..80ae3e4 100644 --- a/Source/ZMMO/ZMMO.Build.cs +++ b/Source/ZMMO/ZMMO.Build.cs @@ -18,7 +18,8 @@ public class ZMMO : ModuleRules "CommonUI", "CommonInput", "GameplayTags", - "ZeusNetwork" + "ZeusNetwork", + "ZMMOAttributes" }); PrivateDependencyModuleNames.AddRange(new string[] { diff --git a/Source/ZMMOAttributes/Private/ZMMOAttributeComponent.cpp b/Source/ZMMOAttributes/Private/ZMMOAttributeComponent.cpp new file mode 100644 index 0000000..fc7d371 --- /dev/null +++ b/Source/ZMMOAttributes/Private/ZMMOAttributeComponent.cpp @@ -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); +} diff --git a/Source/ZMMOAttributes/Private/ZMMOAttributeNetworkHandler.cpp b/Source/ZMMOAttributes/Private/ZMMOAttributeNetworkHandler.cpp new file mode 100644 index 0000000..6b0543b --- /dev/null +++ b/Source/ZMMOAttributes/Private/ZMMOAttributeNetworkHandler.cpp @@ -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 It(World); It; ++It) + { + AActor* Actor = *It; + if (!Actor) { continue; } + UZMMOAttributeComponent* Comp = Actor->FindComponentByClass(); + 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(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(); +} + +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 It(World); It; ++It) + { + AActor* Actor = *It; + if (!Actor) { continue; } + Comp = Actor->FindComponentByClass(); + 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); + } +} diff --git a/Source/ZMMOAttributes/Private/ZMMOAttributesModule.cpp b/Source/ZMMOAttributes/Private/ZMMOAttributesModule.cpp new file mode 100644 index 0000000..28f71f5 --- /dev/null +++ b/Source/ZMMOAttributes/Private/ZMMOAttributesModule.cpp @@ -0,0 +1,13 @@ +#include "ZMMOAttributesModule.h" + +#include "Modules/ModuleManager.h" + +void FZMMOAttributesModule::StartupModule() +{ +} + +void FZMMOAttributesModule::ShutdownModule() +{ +} + +IMPLEMENT_MODULE(FZMMOAttributesModule, ZMMOAttributes) diff --git a/Source/ZMMOAttributes/Private/ZMMOHudHpSpWidget.cpp b/Source/ZMMOAttributes/Private/ZMMOHudHpSpWidget.cpp new file mode 100644 index 0000000..2a67494 --- /dev/null +++ b/Source/ZMMOAttributes/Private/ZMMOHudHpSpWidget.cpp @@ -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(Snapshot.Hp) / static_cast(Snapshot.MaxHp) : 0.f); + } + if (SpBar) + { + SpBar->SetPercent(Snapshot.MaxSp > 0 + ? static_cast(Snapshot.Sp) / static_cast(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(Hp) / static_cast(Snap.MaxHp) : 0.f); + } + if (SpBar) + { + SpBar->SetPercent(Snap.MaxSp > 0 + ? static_cast(Sp) / static_cast(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))); + } + } +} diff --git a/Source/ZMMOAttributes/Public/ZMMOAttributeComponent.h b/Source/ZMMOAttributes/Public/ZMMOAttributeComponent.h new file mode 100644 index 0000000..efc5fb5 --- /dev/null +++ b/Source/ZMMOAttributes/Public/ZMMOAttributeComponent.h @@ -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(Current.Hp) / static_cast(Current.MaxHp) : 0.f; + } + + UFUNCTION(BlueprintPure, Category = "Zeus|Attributes") + float GetSpRatio() const + { + return Current.MaxSp > 0 ? static_cast(Current.Sp) / static_cast(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; +}; diff --git a/Source/ZMMOAttributes/Public/ZMMOAttributeNetworkHandler.h b/Source/ZMMOAttributes/Public/ZMMOAttributeNetworkHandler.h new file mode 100644 index 0000000..45ba179 --- /dev/null +++ b/Source/ZMMOAttributes/Public/ZMMOAttributeNetworkHandler.h @@ -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; +}; diff --git a/Source/ZMMOAttributes/Public/ZMMOAttributeTypes.h b/Source/ZMMOAttributes/Public/ZMMOAttributeTypes.h new file mode 100644 index 0000000..339525c --- /dev/null +++ b/Source/ZMMOAttributes/Public/ZMMOAttributeTypes.h @@ -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; +}; diff --git a/Source/ZMMOAttributes/Public/ZMMOAttributesModule.h b/Source/ZMMOAttributes/Public/ZMMOAttributesModule.h new file mode 100644 index 0000000..cdc8874 --- /dev/null +++ b/Source/ZMMOAttributes/Public/ZMMOAttributesModule.h @@ -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; +}; diff --git a/Source/ZMMOAttributes/Public/ZMMOHudHpSpWidget.h b/Source/ZMMOAttributes/Public/ZMMOHudHpSpWidget.h new file mode 100644 index 0000000..735b186 --- /dev/null +++ b/Source/ZMMOAttributes/Public/ZMMOHudHpSpWidget.h @@ -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 BoundComponent; +}; diff --git a/Source/ZMMOAttributes/ZMMOAttributes.Build.cs b/Source/ZMMOAttributes/ZMMOAttributes.Build.cs new file mode 100644 index 0000000..d42700a --- /dev/null +++ b/Source/ZMMOAttributes/ZMMOAttributes.Build.cs @@ -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 + } +} diff --git a/Source/ZMMOAttributes/module.json b/Source/ZMMOAttributes/module.json new file mode 100644 index 0000000..ad8b1a9 --- /dev/null +++ b/Source/ZMMOAttributes/module.json @@ -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." +} diff --git a/ZMMO.uproject b/ZMMO.uproject index b4a7881..7d4ee6e 100644 --- a/ZMMO.uproject +++ b/ZMMO.uproject @@ -8,6 +8,11 @@ "Name": "ZMMO", "Type": "Runtime", "LoadingPhase": "Default" + }, + { + "Name": "ZMMOAttributes", + "Type": "Runtime", + "LoadingPhase": "PreDefault" } ], "Plugins": [