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 0000000..f16ba4f Binary files /dev/null and b/Content/ZMMO/UI/HUD/WBP_HUD_HpSpBar.uasset differ 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": [