From 7d83dd800f8487ca1cab85b6726b01424f1d03ea Mon Sep 17 00:00:00 2001 From: Mateus Rodrigues Date: Sat, 23 May 2026 04:00:32 -0300 Subject: [PATCH 1/6] =?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": [ -- 2.54.0.windows.1 From 396223e2a859af0bdeb6d34ff0482403cf3ea22f Mon Sep 17 00:00:00 2001 From: Mateus Rodrigues Date: Sat, 23 May 2026 05:27:41 -0300 Subject: [PATCH 2/6] feat(ui-ingame): UI in-game system + PlayerState + Component Registry + HUD composite MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Espelho simétrico do front-end pattern (UUIFrontEndFlowSubsystem + DA_FrontEndScreenSet + UUIPrimaryGameLayout_Base) para a UI in-game. Pavimenta caminho pra Inventory, Skills, StatusWindow, EscapeMenu, etc. ## UI in-game system (Source/ZMMO/Game/UI/InGame/) - EZMMOInGameUIState enum (None, Playing, StatusWindow, Inventory, SkillTree, EscapeMenu, Loading) em Data/UI/InGameTypes.h - UUIInGameScreenSet DataAsset: TMap> - UUIInGameFlowSubsystem (GameInstanceSubsystem) com SetState/ToggleScreen - DA_InGameScreenSet em Content/ZMMO/UI/InGame/ mapeia Playing->WBP_HUD - Layer routing: Playing -> UI.Layer.Game | menus -> GameMenu | loading -> Modal ## AHUD pattern (idiomatic UE5) - AZMMOHUD : AHUD em Source/ZMMO/Game/Modes/ - AZMMOGameMode::HUDClass = AZMMOHUD::StaticClass() - AZMMOHUD::BeginPlay chama UIInGameFlowSubsystem::StartInGame - Pawn não cria mais HUD (separação de responsabilidades) ## PlayerState + Component Registry (Open-Closed) - AZMMOPlayerState : APlayerState em Source/ZMMO/Game/Modes/ - AZMMOGameMode::PlayerStateClass = AZMMOPlayerState::StaticClass() - UPROPERTY Config TArray> ComponentClasses - DefaultGame.ini: +ComponentClasses=/Script/ZMMOAttributes.ZMMOAttributeComponent - Ctor lê via GConfig (mais robusto que UPROPERTY Config + parsing) e instancia cada componente como CreateDefaultSubobject Por que componentes no PlayerState e não no Pawn: - Sobrevive ao despawn (morte/respawn) - Spectator-friendly - 1:1 com player (Pawn pode trocar: vehicle, mount, possession) - Padrão MMO clássico (rathena, TrinityCore) Adicionar novo módulo (Inventory, Skills, Guild) = 1 linha no DefaultGame.ini, sem editar AZMMOPlayerState.cpp. ## HUD composite - UZMMOHudWidget : UCommonActivatableWidget em ZMMO core (UI/InGame/) - WBP_HUD em Content/ZMMO/UI/HUD/ herda UZMMOHudWidget - HpSpBar (UZMMOHudHpSpWidget — sub-modulo ZMMOAttributes) embedded via BindWidgetOptional. Sub-widgets futuros: PlayerInfo, Minimap, QuickBar, Buffs, Chat, TargetInfo - UZMMOHudHpSpWidget volta a ser UUserWidget puro (sub-widget, não root) - UZMMOHudWidget::NativeOnActivated: - SetVisibility(HitTestInvisible) — mouse passa direto pro Pawn - BindToLocalPlayer: busca AttributeComponent no PC->PlayerState - Subscreve OnAttributesChanged/OnHpSpChanged/OnLevelUp - Propaga snapshots pra sub-widgets via HpSpBar->ApplySnapshot - GetDesiredInputConfig override: ECommonInputMode::Game (sem cursor, movimento livre). Menus override pra GameAndMenu/Menu. ## NetworkHandler refatorado (AttributeComponent agora no PlayerState) - ZMMOAttributeNetworkHandler::FindComponentForEntity: busca via GameState->PlayerArray (O(N_players)), não TActorIterator (O(N_actors)) - ZMMOAttributes.Build.cs += CommonUI, CommonInput ## EnsureRootLayout (fix crítico) - OpenLevel invalida widgets no viewport ("InvalidateAllWidgets") - RootLayout (criado pelo FrontEndFlow no boot) sobrevive como UObject (LocalPlayerSubsystem) mas fica órfão fora do viewport - Push do WBP_HUD no Stack_Game funcionava mas widget pai (RootLayout) estava fora do viewport → invisível - UUIFrontEndFlowSubsystem::EnsureRootLayout (público) recria via UIManagerSubsystem (idempotente) - UUIInGameFlowSubsystem::StartInGame chama EnsureRootLayout antes do push ## PlayerCharacter simplificado - Removido AttributeComponent (vai pro PlayerState via Registry) - Removido HudHpSpWidgetClass + spawn manual de HUD - HandleLocalSpawnReady só faz seed do EntityId via PlayerState - UI lifecycle agora é responsabilidade do AZMMOHUD::BeginPlay ## Verificação (smoke test) PIE: Login -> CharSelect -> Lobby -> EnterWorld ↓ LogZMMO: AZMMOPlayerState ctor: 1 componente(s) no registry LogZMMO: ZMMOPlayerState: componente registrado [0] ZMMOAttributeComponent LogZMMO: FrontEndFlow::EnsureRootLayout: RootLayout recriado LogZMMO: InGameFlow: state None -> Playing LogZMMO: InGameFlow: tela Playing pushed em UI.Layer.Game (widget=WBP_HUD_C_0) LogZMMO: ZMMOHudWidget activated (HpSpBar=WBP_HUD_HpSpBar_C_0) ↓ HUD aparece com Lv 5 / HP 10/200 / SP 5/20 (valores do DB) Regen funciona (HP/SP enchem a cada 6s/8s) Movimento normal (input flui pro Pawn) Co-Authored-By: Claude Opus 4.7 --- Config/DefaultGame.ini | 23 ++ Content/ZMMO/UI/HUD/WBP_HUD.uasset | Bin 0 -> 28757 bytes Content/ZMMO/UI/HUD/WBP_HUD_HpSpBar.uasset | Bin 33509 -> 38740 bytes .../ZMMO/UI/InGame/DA_InGameScreenSet.uasset | Bin 0 -> 1560 bytes Source/ZMMO/Data/UI/InGameTypes.h | 34 +++ .../ZMMO/Game/Entity/ZMMOPlayerCharacter.cpp | 82 ++----- Source/ZMMO/Game/Entity/ZMMOPlayerCharacter.h | 20 +- Source/ZMMO/Game/Modes/ZMMOGameMode.cpp | 4 + Source/ZMMO/Game/Modes/ZMMOHUD.cpp | 40 ++++ Source/ZMMO/Game/Modes/ZMMOHUD.h | 36 +++ Source/ZMMO/Game/Modes/ZMMOPlayerState.cpp | 82 +++++++ Source/ZMMO/Game/Modes/ZMMOPlayerState.h | 104 +++++++++ .../UI/FrontEnd/UIFrontEndFlowSubsystem.cpp | 33 ++- .../UI/FrontEnd/UIFrontEndFlowSubsystem.h | 12 + .../Game/UI/InGame/UIInGameFlowSubsystem.cpp | 210 ++++++++++++++++++ .../Game/UI/InGame/UIInGameFlowSubsystem.h | 97 ++++++++ .../ZMMO/Game/UI/InGame/UIInGameScreenSet.cpp | 7 + .../ZMMO/Game/UI/InGame/UIInGameScreenSet.h | 35 +++ Source/ZMMO/Game/UI/InGame/ZMMOHudWidget.cpp | 122 ++++++++++ Source/ZMMO/Game/UI/InGame/ZMMOHudWidget.h | 102 +++++++++ .../Private/ZMMOAttributeNetworkHandler.cpp | 55 +++-- .../Private/ZMMOHudHpSpWidget.cpp | 106 +++------ .../ZMMOAttributes/Public/ZMMOHudHpSpWidget.h | 63 +++--- Source/ZMMOAttributes/ZMMOAttributes.Build.cs | 2 + Source/ZMMOAttributes/module.json | 2 +- 25 files changed, 1029 insertions(+), 242 deletions(-) create mode 100644 Content/ZMMO/UI/HUD/WBP_HUD.uasset create mode 100644 Content/ZMMO/UI/InGame/DA_InGameScreenSet.uasset create mode 100644 Source/ZMMO/Data/UI/InGameTypes.h create mode 100644 Source/ZMMO/Game/Modes/ZMMOHUD.cpp create mode 100644 Source/ZMMO/Game/Modes/ZMMOHUD.h create mode 100644 Source/ZMMO/Game/Modes/ZMMOPlayerState.cpp create mode 100644 Source/ZMMO/Game/Modes/ZMMOPlayerState.h create mode 100644 Source/ZMMO/Game/UI/InGame/UIInGameFlowSubsystem.cpp create mode 100644 Source/ZMMO/Game/UI/InGame/UIInGameFlowSubsystem.h create mode 100644 Source/ZMMO/Game/UI/InGame/UIInGameScreenSet.cpp create mode 100644 Source/ZMMO/Game/UI/InGame/UIInGameScreenSet.h create mode 100644 Source/ZMMO/Game/UI/InGame/ZMMOHudWidget.cpp create mode 100644 Source/ZMMO/Game/UI/InGame/ZMMOHudWidget.h diff --git a/Config/DefaultGame.ini b/Config/DefaultGame.ini index a93670a..11749f3 100644 --- a/Config/DefaultGame.ini +++ b/Config/DefaultGame.ini @@ -34,3 +34,26 @@ ScreenSetAsset=/Game/ZMMO/UI/FrontEnd/DA_FrontEndScreenSet.DA_FrontEndScreenSet ; server (Server/ZeusServerEngine/Config/DataTables/maps_config.json) — o ; cliente resolve `mapId -> ClientLevel/spawns` via FZMMOMapDef rows. MapsTableAsset=/Game/ZMMO/Data/World/DT_Maps.DT_Maps + +; ----------------------------------------------------------------------------- +; UI in-game (PR 19+). Espelho do front-end pattern: +; - ScreenSetAsset mapeia EZMMOInGameUIState -> WBP (Playing -> WBP_HUD, +; StatusWindow -> WBP_StatusWindow, etc.) +; - Subsystem orquestra: AZMMOHUD::BeginPlay chama StartInGame que vai pra +; Playing. +; Asset criado via MCP em /Game/ZMMO/UI/InGame/DA_InGameScreenSet. +; ----------------------------------------------------------------------------- +[/Script/ZMMO.UIInGameFlowSubsystem] +ScreenSetAsset=/Game/ZMMO/UI/InGame/DA_InGameScreenSet.DA_InGameScreenSet + +; ----------------------------------------------------------------------------- +; PlayerState Component Registry (PR 19+). Cada módulo MMO registra seus +; UActorComponent aqui. O AZMMOPlayerState lê esta lista no construtor e +; instancia cada classe como subobject — pattern Open-Closed: adicionar +; Inventory/Skills/Guild = mais uma linha aqui, sem editar AZMMOPlayerState. +; +; Convenção: usa o path /Script/. (sem o "U" do prefixo +; C++; UClass resolve pelo nome curto). +; ----------------------------------------------------------------------------- +[/Script/ZMMO.ZMMOPlayerState] ++ComponentClasses=/Script/ZMMOAttributes.ZMMOAttributeComponent diff --git a/Content/ZMMO/UI/HUD/WBP_HUD.uasset b/Content/ZMMO/UI/HUD/WBP_HUD.uasset new file mode 100644 index 0000000000000000000000000000000000000000..b3bc8f539d6b63578573d5df1e471d1a7cacd0c5 GIT binary patch literal 28757 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 CSy(6l literal 0 HcmV?d00001 diff --git a/Content/ZMMO/UI/HUD/WBP_HUD_HpSpBar.uasset b/Content/ZMMO/UI/HUD/WBP_HUD_HpSpBar.uasset index f16ba4f9d38d8b05aff4ffb7d642a59bc2b4d198..eb05158c0d4bd41f9ae6627efc1c79b417f59173 100644 GIT binary patch literal 38740 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 b`+X-c@YRDuno@uB=vM0Rt`?3Ai1_~p<{Zmz delta 8547 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 UI.Layer.Game (HUD principal) + * - StatusWindow / Inventory / + * SkillTree / EscapeMenu -> UI.Layer.GameMenu (sobreposicoes) + * - Loading -> UI.Layer.Modal (overlay critico) + * + * `Playing` e' o estado base — quando in-game, sempre ha um HUD no + * Layer.Game. Abrir Status/Inventory NAO substitui o HUD; empilha em + * Layer.GameMenu por cima dele. + */ +UENUM(BlueprintType) +enum class EZMMOInGameUIState : uint8 +{ + None, ///< Antes do player local spawnar; UI in-game inativa. + Playing, ///< HUD principal visivel (HP/SP/level/etc). + StatusWindow, ///< Janela de atributos + botões de alocacao (Fase 4). + Inventory, ///< (futuro) Bag/equipamentos. + SkillTree, ///< (futuro) Arvore de skills. + EscapeMenu, ///< (futuro) Pausa, settings, logout. + Loading ///< Overlay de loading generico (transitorio). +}; diff --git a/Source/ZMMO/Game/Entity/ZMMOPlayerCharacter.cpp b/Source/ZMMO/Game/Entity/ZMMOPlayerCharacter.cpp index d6cd981..a4a2832 100644 --- a/Source/ZMMO/Game/Entity/ZMMOPlayerCharacter.cpp +++ b/Source/ZMMO/Game/Entity/ZMMOPlayerCharacter.cpp @@ -13,12 +13,11 @@ #include "GameFramework/SpringArmComponent.h" #include "InputAction.h" #include "InputActionValue.h" -#include "Blueprint/UserWidget.h" +#include "GameFramework/PlayerState.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" @@ -55,12 +54,8 @@ 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")); + // AttributeComponent migrou pro PlayerState (AZMMOPlayerState + Component + // Registry config-driven). Pawn fica leve — so' movement/input/camera. // Defaults visuais (mesh + AnimBP) — alinhados ao ZClientMMO. Permitem ao // motor spawnar AZMMOPlayerCharacter directamente como DefaultPawnClass do @@ -111,20 +106,6 @@ 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() @@ -164,11 +145,6 @@ void AZMMOPlayerCharacter::EndPlay(const EEndPlayReason::Type EndPlayReason) { UnbindZeusSpawnDelegate(); ZeusNetwork = nullptr; - if (HudHpSpWidget) - { - HudHpSpWidget->RemoveFromParent(); - HudHpSpWidget = nullptr; - } Super::EndPlay(EndPlayReason); } @@ -373,49 +349,23 @@ void AZMMOPlayerCharacter::HandleLocalSpawnReady(const int32 InEntityId, const F } } - // 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) + // Seed do EntityId no AttributeComponent do PlayerState. O componente + // vive no AZMMOPlayerState via Component Registry — busca via PlayerState + // (sobrevive ao despawn do Pawn). S_ATTRIBUTE_SNAPSHOT_FULL chega via + // UDP e e' rouado pelo ZMMOAttributeNetworkHandler via lookup por + // EntityId no PlayerArray do GameState. + if (APlayerState* PS = GetPlayerState()) { - 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) + if (UZMMOAttributeComponent* AttrComp = PS->FindComponentByClass()) { - 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()); - } + AttrComp->SeedEntityId(InEntityId); } } + + // UI in-game e' responsabilidade do AZMMOHUD (GameMode.HUDClass), nao do + // Pawn. AHUD::BeginPlay chama UUIInGameFlowSubsystem::StartInGame, que + // pushea WBP_HUD em UI.Layer.Game. UZMMOHudWidget::NativeOnActivated auto-binda + // no UZMMOAttributeComponent via PlayerState. } void AZMMOPlayerCharacter::FlushInputAxisToServer(const float DeltaSeconds) diff --git a/Source/ZMMO/Game/Entity/ZMMOPlayerCharacter.h b/Source/ZMMO/Game/Entity/ZMMOPlayerCharacter.h index 35c28ca..dc6d166 100644 --- a/Source/ZMMO/Game/Entity/ZMMOPlayerCharacter.h +++ b/Source/ZMMO/Game/Entity/ZMMOPlayerCharacter.h @@ -11,8 +11,6 @@ class USpringArmComponent; class UCameraComponent; class UInputAction; class UZeusNetworkSubsystem; -class UZMMOAttributeComponent; -class UZMMOHudHpSpWidget; struct FInputActionValue; DECLARE_LOG_CATEGORY_EXTERN(LogZMMOPlayer, Log, All); @@ -49,10 +47,8 @@ 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; + // AttributeComponent vive no PlayerState (AZMMOPlayerState + Component Registry). + // Acesso: GetPlayerState()->GetZMMOComponent(). protected: UPROPERTY(EditAnywhere, Category = "Input") @@ -80,12 +76,6 @@ 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(); @@ -102,7 +92,6 @@ public: FORCEINLINE USpringArmComponent* GetCameraBoom() const { return CameraBoom; } FORCEINLINE UCameraComponent* GetFollowCamera() const { return FollowCamera; } - FORCEINLINE UZMMOAttributeComponent* GetAttributeComponent() const { return AttributeComponent; } protected: virtual void BeginPlay() override; @@ -173,9 +162,4 @@ 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/Game/Modes/ZMMOGameMode.cpp b/Source/ZMMO/Game/Modes/ZMMOGameMode.cpp index 48a5144..75821f3 100644 --- a/Source/ZMMO/Game/Modes/ZMMOGameMode.cpp +++ b/Source/ZMMO/Game/Modes/ZMMOGameMode.cpp @@ -1,10 +1,14 @@ #include "ZMMOGameMode.h" +#include "ZMMOHUD.h" #include "ZMMOPlayerCharacter.h" #include "ZMMOPlayerController.h" +#include "ZMMOPlayerState.h" AZMMOGameMode::AZMMOGameMode() { DefaultPawnClass = AZMMOPlayerCharacter::StaticClass(); PlayerControllerClass = AZMMOPlayerController::StaticClass(); + PlayerStateClass = AZMMOPlayerState::StaticClass(); + HUDClass = AZMMOHUD::StaticClass(); } diff --git a/Source/ZMMO/Game/Modes/ZMMOHUD.cpp b/Source/ZMMO/Game/Modes/ZMMOHUD.cpp new file mode 100644 index 0000000..c7c6087 --- /dev/null +++ b/Source/ZMMO/Game/Modes/ZMMOHUD.cpp @@ -0,0 +1,40 @@ +#include "ZMMOHUD.h" + +#include "Engine/GameInstance.h" +#include "ZMMO.h" +#include "UI/InGame/UIInGameFlowSubsystem.h" + +AZMMOHUD::AZMMOHUD() +{ + // AHUD nao precisa tick — toda atualizacao vem via UMG (delegates + + // CommonUI). PrimaryActorTick default e' off; mantemos. +} + +void AZMMOHUD::BeginPlay() +{ + Super::BeginPlay(); + + // UI in-game assume responsabilidade aqui. Subsystem orquestra: + // resolve DA_InGameScreenSet[Playing] -> WBP_HUD, pushea em UI.Layer.Game. + if (const UGameInstance* GI = GetGameInstance()) + { + if (UUIInGameFlowSubsystem* Flow = GI->GetSubsystem()) + { + Flow->StartInGame(); + } + else + { + UE_LOG(LogZMMO, Warning, + TEXT("AZMMOHUD: UUIInGameFlowSubsystem nao encontrado — HUD nao sera spawnado")); + } + } +} + +void AZMMOHUD::EndPlay(const EEndPlayReason::Type EndPlayReason) +{ + // UUIInGameFlowSubsystem persiste no GameInstance entre OpenLevel — nao + // limpamos UI in-game aqui (o front-end flow vai tomar conta na proxima + // transicao de estado). Cleanup do widget atual acontece no + // NativeOnDeactivated quando o stack for esvaziado pelo flow. + Super::EndPlay(EndPlayReason); +} diff --git a/Source/ZMMO/Game/Modes/ZMMOHUD.h b/Source/ZMMO/Game/Modes/ZMMOHUD.h new file mode 100644 index 0000000..44c95f7 --- /dev/null +++ b/Source/ZMMO/Game/Modes/ZMMOHUD.h @@ -0,0 +1,36 @@ +#pragma once + +#include "CoreMinimal.h" +#include "GameFramework/HUD.h" +#include "ZMMOHUD.generated.h" + +/** + * HUD do MMO. Pattern UE5 idiomatico: o GameMode atribui `HUDClass`, o + * engine cria 1 instancia por PlayerController automaticamente. + * + * Responsabilidade unica: orquestrar o lifecycle da UI in-game. Em + * BeginPlay, pede ao `UUIInGameFlowSubsystem` pra entrar em `Playing` + * (que pushea WBP_HUD em UI.Layer.Game). Em EndPlay/destrucao, o flow + * subsystem cuida do cleanup. + * + * Separacao de responsabilidades: + * - AZMMOPlayerCharacter (Pawn) — movimento, input, AttributeComponent + * - AZMMOPlayerController — input mapping, mouse + * - AZMMOHUD — UI lifecycle + * - AZMMOGameMode — wires tudo via class slots + * + * NAO desenha nada via canvas (AHUD::DrawHUD). UI inteira vive em UMG + + * CommonUI (UI.Layer.Game / GameMenu / Modal). + */ +UCLASS() +class ZMMO_API AZMMOHUD : public AHUD +{ + GENERATED_BODY() + +public: + AZMMOHUD(); + +protected: + virtual void BeginPlay() override; + virtual void EndPlay(const EEndPlayReason::Type EndPlayReason) override; +}; diff --git a/Source/ZMMO/Game/Modes/ZMMOPlayerState.cpp b/Source/ZMMO/Game/Modes/ZMMOPlayerState.cpp new file mode 100644 index 0000000..e7cfda9 --- /dev/null +++ b/Source/ZMMO/Game/Modes/ZMMOPlayerState.cpp @@ -0,0 +1,82 @@ +#include "ZMMOPlayerState.h" + +#include "Components/ActorComponent.h" +#include "Misc/ConfigCacheIni.h" +#include "ZMMO.h" + +AZMMOPlayerState::AZMMOPlayerState() +{ + // Component Registry: instancia cada classe declarada em + // DefaultGame.ini::[/Script/ZMMO.ZMMOPlayerState]+ComponentClasses=... + // + // Le manualmente via GConfig para evitar timing do UPROPERTY(Config), + // que requer LoadConfig() ja ter rodado no CDO antes da instance ser + // construida. Pra propriedades em arrays config com `+` syntax, lendo + // direto e mais previsivel. + if (ComponentClasses.Num() == 0 && GConfig) + { + TArray ClassPaths; + GConfig->GetArray(TEXT("/Script/ZMMO.ZMMOPlayerState"), + TEXT("ComponentClasses"), + ClassPaths, + GGameIni); + for (const FString& Path : ClassPaths) + { + if (UClass* C = LoadClass(nullptr, *Path)) + { + ComponentClasses.Add(C); + } + else + { + UE_LOG(LogZMMO, Warning, + TEXT("ZMMOPlayerState: ComponentClasses path nao resolveu: %s"), *Path); + } + } + } + + UE_LOG(LogZMMO, Log, + TEXT("AZMMOPlayerState ctor: %d componente(s) no registry (instance=%s)"), + ComponentClasses.Num(), *GetName()); + + int32 SubobjectIndex = 0; + for (const TSubclassOf& CompClassRef : ComponentClasses) + { + UClass* CompClass = CompClassRef.Get(); + if (!CompClass) + { + UE_LOG(LogZMMO, Warning, + TEXT("ZMMOPlayerState: ComponentClass null em ComponentClasses[%d]"), + SubobjectIndex); + ++SubobjectIndex; + continue; + } + const FName CompName = MakeUniqueObjectName(this, CompClass, + FName(*FString::Printf(TEXT("ZMMOComp_%d_%s"), SubobjectIndex, *CompClass->GetName()))); + // Overload runtime de CreateDefaultSubobject retorna UObject* (sem template) — + // precisa cast para UActorComponent. + UActorComponent* Comp = Cast(CreateDefaultSubobject(CompName, CompClass, CompClass, + /*bIsRequired*/ false, /*bIsTransient*/ false)); + if (Comp) + { + UE_LOG(LogZMMO, Log, + TEXT("ZMMOPlayerState: componente registrado [%d] %s"), + SubobjectIndex, *CompClass->GetName()); + } + else + { + UE_LOG(LogZMMO, Warning, + TEXT("ZMMOPlayerState: CreateDefaultSubobject falhou para %s"), + *CompClass->GetName()); + } + ++SubobjectIndex; + } +} + +void AZMMOPlayerState::SetPublicIdentity(int64 InEntityId, const FString& InCharId, + int32 InBaseLevel, int32 InClassId) +{ + ZMMOEntityId = InEntityId; + CharId = InCharId; + BaseLevel = InBaseLevel; + ClassId = InClassId; +} diff --git a/Source/ZMMO/Game/Modes/ZMMOPlayerState.h b/Source/ZMMO/Game/Modes/ZMMOPlayerState.h new file mode 100644 index 0000000..d7be27f --- /dev/null +++ b/Source/ZMMO/Game/Modes/ZMMOPlayerState.h @@ -0,0 +1,104 @@ +#pragma once + +#include "CoreMinimal.h" +#include "GameFramework/PlayerState.h" +#include "Templates/SubclassOf.h" +#include "ZMMOPlayerState.generated.h" + +class UActorComponent; + +/** + * PlayerState do MMO. Pattern UE5 idiomatico: o GameMode atribui + * `PlayerStateClass`, engine cria 1 instancia por PlayerController. + * + * Responsabilidade — estado **publico** do jogador (visivel pra outros): + * - Identidade: charId, entityId, name, guild + * - Progressao publica: baseLevel, classId + * - Estado: alive/dead, in-combat (futuro) + * + * Dados PRIVADOS (HP/SP atuais, stats brutos, inventory, hotbar) vivem + * em componentes anexados via Component Registry (vide abaixo). + * + * Por que componentes no PlayerState e nao no Pawn: + * - **Sobrevive ao despawn** (morte/respawn — Pawn destrui, PlayerState + * mantem). Padrao MMO classico (rathena, TrinityCore). + * - **Spectator-friendly**: dados acessiveis sem ter pawn. + * - **1:1 com player** (Pawn pode trocar — vehicle, mount, possession). + * + * ## Component Registry (Open-Closed) + * + * Cada modulo (ZMMOAttributes, ZMMOInventory, ZMMOSkills futuros) declara + * sua component class em DefaultGame.ini: + * + * [/Script/ZMMO.ZMMOPlayerState] + * +ComponentClasses=/Script/ZMMOAttributes.ZMMOAttributeComponent + * +ComponentClasses=/Script/ZMMOInventory.ZMMOInventoryComponent + * +ComponentClasses=/Script/ZMMOSkills.ZMMOSkillComponent + * + * O construtor de AZMMOPlayerState le essa lista (Config) e instancia + * cada classe como subobject. PlayerState fica fechado pra modificacao, + * modulos abertos pra extensao — adicionar Inventory NAO requer + * recompilar/editar AZMMOPlayerState. + * + * Acesso: `PlayerState->FindComponentByClass()` + * ou o helper `GetZMMOComponent()`. + */ +UCLASS(Config = Game, BlueprintType) +class ZMMO_API AZMMOPlayerState : public APlayerState +{ + GENERATED_BODY() + +public: + AZMMOPlayerState(); + + // === Identidade publica === + + /** EntityId autoritativo do server. 0 ate o spawn local confirmar. */ + UPROPERTY(VisibleInstanceOnly, BlueprintReadOnly, Category = "ZMMO|Identity") + int64 ZMMOEntityId = 0; + + /** CharId (BIGINT no DB; FString pra preservar 64 bits em Blueprint). */ + UPROPERTY(VisibleInstanceOnly, BlueprintReadOnly, Category = "ZMMO|Identity") + FString CharId; + + // === Progressao publica === + + UPROPERTY(VisibleInstanceOnly, BlueprintReadOnly, Category = "ZMMO|Progress") + int32 BaseLevel = 1; + + UPROPERTY(VisibleInstanceOnly, BlueprintReadOnly, Category = "ZMMO|Progress") + int32 ClassId = 0; + + // === Guild (futuro) === + + UPROPERTY(VisibleInstanceOnly, BlueprintReadOnly, Category = "ZMMO|Guild") + FString GuildName; + + // === Helpers === + + /// Atualizado quando S_ATTRIBUTE_SNAPSHOT_FULL chega — UZMMOHudWidget chama + /// isso pra refletir os dados publicos do snapshot no PlayerState. + UFUNCTION(BlueprintCallable, Category = "ZMMO|Identity") + void SetPublicIdentity(int64 InEntityId, const FString& InCharId, + int32 InBaseLevel, int32 InClassId); + + /// Template helper: PS->GetZMMOComponent() + template + T* GetZMMOComponent() const + { + return Cast(FindComponentByClass(T::StaticClass())); + } + +protected: + /** + * Component Registry — config-driven. Modulos registram suas classes + * via DefaultGame.ini com prefix `+`. Construtor instancia cada uma + * como CreateDefaultSubobject. + * + * TSubclassOf (nao TSoftClassPtr): config UE resolve classes nativas + * diretamente. Modulos C++ precisam estar carregados antes do CDO ser + * instanciado — garantido por LoadingPhase=PreDefault no .uproject. + */ + UPROPERTY(Config, EditDefaultsOnly, Category = "ZMMO|Components") + TArray> ComponentClasses; +}; diff --git a/Source/ZMMO/Game/UI/FrontEnd/UIFrontEndFlowSubsystem.cpp b/Source/ZMMO/Game/UI/FrontEnd/UIFrontEndFlowSubsystem.cpp index cdfd506..cd2393e 100644 --- a/Source/ZMMO/Game/UI/FrontEnd/UIFrontEndFlowSubsystem.cpp +++ b/Source/ZMMO/Game/UI/FrontEnd/UIFrontEndFlowSubsystem.cpp @@ -41,18 +41,7 @@ void UUIFrontEndFlowSubsystem::Deinitialize() void UUIFrontEndFlowSubsystem::StartFrontEnd() { - UUIFrontEndScreenSet* SS = GetScreenSet(); - - if (UUIManagerSubsystem* Mgr = GetUIManager()) - { - TSubclassOf LayoutClass; - if (SS && !SS->RootLayoutClass.IsNull()) - { - LayoutClass = SS->RootLayoutClass.LoadSynchronous(); - } - Mgr->CreateAndAddRootLayout(LayoutClass); - } - + EnsureRootLayout(); BindNetwork(); SetState(EZMMOFrontEndState::Boot); @@ -69,6 +58,26 @@ void UUIFrontEndFlowSubsystem::StartFrontEnd() } } +void UUIFrontEndFlowSubsystem::EnsureRootLayout() +{ + UUIManagerSubsystem* Mgr = GetUIManager(); + if (!Mgr) { return; } + if (Mgr->IsRootLayoutReady()) + { + return; // ja' no viewport + } + UUIFrontEndScreenSet* SS = GetScreenSet(); + if (!SS || SS->RootLayoutClass.IsNull()) + { + UE_LOG(LogZMMO, Warning, + TEXT("FrontEndFlow::EnsureRootLayout: RootLayoutClass ausente em ScreenSet")); + return; + } + TSubclassOf LayoutClass = SS->RootLayoutClass.LoadSynchronous(); + Mgr->CreateAndAddRootLayout(LayoutClass); + UE_LOG(LogZMMO, Log, TEXT("FrontEndFlow::EnsureRootLayout: RootLayout recriado")); +} + void UUIFrontEndFlowSubsystem::SetState(EZMMOFrontEndState NewState) { if (NewState == CurrentState) diff --git a/Source/ZMMO/Game/UI/FrontEnd/UIFrontEndFlowSubsystem.h b/Source/ZMMO/Game/UI/FrontEnd/UIFrontEndFlowSubsystem.h index 1f3b156..579939b 100644 --- a/Source/ZMMO/Game/UI/FrontEnd/UIFrontEndFlowSubsystem.h +++ b/Source/ZMMO/Game/UI/FrontEnd/UIFrontEndFlowSubsystem.h @@ -46,6 +46,18 @@ public: UFUNCTION(BlueprintCallable, Category = "FrontEnd") void StartFrontEnd(); + /** + * Garante que o root layout (UUIPrimaryGameLayout_Base) esta no viewport. + * Chamado pelo UIInGameFlowSubsystem::StartInGame apos OpenLevel — o + * `OpenLevel` invalida widgets no viewport ("InvalidateAllWidgets") e o + * RootLayout fica orfao. Idempotente: CreateAndAddRootLayout do + * UIManagerSubsystem checa `IsInViewport` antes de recriar. + * + * C++ puro (sem UFUNCTION) — Live Coding nao registra UFUNCTIONs novas; + * consumidores externos chamam direto via C++ (UIInGameFlowSubsystem). + */ + void EnsureRootLayout(); + UFUNCTION(BlueprintCallable, Category = "FrontEnd") void SetState(EZMMOFrontEndState NewState); diff --git a/Source/ZMMO/Game/UI/InGame/UIInGameFlowSubsystem.cpp b/Source/ZMMO/Game/UI/InGame/UIInGameFlowSubsystem.cpp new file mode 100644 index 0000000..cb16f33 --- /dev/null +++ b/Source/ZMMO/Game/UI/InGame/UIInGameFlowSubsystem.cpp @@ -0,0 +1,210 @@ +#include "UIInGameFlowSubsystem.h" + +#include "CommonActivatableWidget.h" // Necessario pra TSoftClassPtr::Get() +#include "Engine/AssetManager.h" +#include "Engine/GameInstance.h" +#include "Engine/StreamableManager.h" +#include "GameFramework/PlayerController.h" +#include "GameplayTagContainer.h" +#include "ZMMO.h" +#include "Data/UI/UILayerTags.h" +#include "FrontEnd/UIFrontEndFlowSubsystem.h" +#include "FrontEnd/UIManagerSubsystem.h" +#include "InGame/UIInGameScreenSet.h" + +void UUIInGameFlowSubsystem::Initialize(FSubsystemCollectionBase& Collection) +{ + Super::Initialize(Collection); + UE_LOG(LogZMMO, Log, TEXT("UIInGameFlowSubsystem initialized")); +} + +void UUIInGameFlowSubsystem::Deinitialize() +{ + ScreenSet = nullptr; + CurrentState = EZMMOInGameUIState::None; + Super::Deinitialize(); +} + +void UUIInGameFlowSubsystem::StartInGame() +{ + if (CurrentState != EZMMOInGameUIState::None) + { + // Idempotente: chamadas extras (ex.: respawn) sao no-op se ja in-world. + UE_LOG(LogZMMO, Verbose, + TEXT("InGameFlow: StartInGame ignorado (ja em estado %s)"), + *UEnum::GetValueAsString(CurrentState)); + return; + } + + // CRITICAL: OpenLevel invalida widgets no viewport ("InvalidateAllWidgets"), + // orfanando o RootLayout (UUIPrimaryGameLayout_Base) que foi criado pelo + // FrontEndFlow no boot. Sem RootLayout no viewport, PushScreenToLayer + // silenciosamente adiciona widgets ao stack do RootLayout — mas o stack + // nao e' visivel porque o parent (RootLayout) nao esta no viewport. + // + // Re-cria via EnsureRootLayout (idempotente) ANTES de qualquer push. + if (UGameInstance* GI = GetGameInstance()) + { + if (UUIFrontEndFlowSubsystem* FE = GI->GetSubsystem()) + { + FE->EnsureRootLayout(); + } + } + + // Limpa qualquer residuo do front-end (Menu/Modal podem ter "Loading" ou + // dialogs orfaos do handoff). Layer.Game vai receber o HUD novo. + if (UUIManagerSubsystem* Mgr = GetUIManager()) + { + Mgr->ClearLayer(ZMMOUITags::UI_Layer_Menu.GetTag()); + Mgr->ClearLayer(ZMMOUITags::UI_Layer_Modal.GetTag()); + Mgr->ClearLayer(ZMMOUITags::UI_Layer_GameMenu.GetTag()); + } + + SetState(EZMMOInGameUIState::Playing); +} + +void UUIInGameFlowSubsystem::SetState(EZMMOInGameUIState NewState) +{ + if (NewState == CurrentState) + { + return; + } + + const EZMMOInGameUIState OldState = CurrentState; + CurrentState = NewState; + + UE_LOG(LogZMMO, Log, TEXT("InGameFlow: state %s -> %s"), + *UEnum::GetValueAsString(OldState), + *UEnum::GetValueAsString(NewState)); + + // Decisao de stack: ao voltar pra `Playing`, fechamos quaisquer overlays + // (Status/Inventory/Menu) limpando o Layer.GameMenu — HUD em Layer.Game + // permanece. Para qualquer outro estado, mantemos HUD e empilhamos no + // layer correspondente. + if (UUIManagerSubsystem* Mgr = GetUIManager()) + { + const bool bGoingToPlaying = (NewState == EZMMOInGameUIState::Playing); + if (bGoingToPlaying) + { + Mgr->ClearLayer(ZMMOUITags::UI_Layer_GameMenu.GetTag()); + } + } + + ResolveAndPushScreen(NewState); + OnStateChanged.Broadcast(NewState); +} + +void UUIInGameFlowSubsystem::ToggleScreen(EZMMOInGameUIState Screen) +{ + if (Screen == EZMMOInGameUIState::Playing || Screen == EZMMOInGameUIState::None) + { + // Toggle para Playing nao faz sentido (estado base). Use SetState direto. + return; + } + if (CurrentState == Screen) + { + SetState(EZMMOInGameUIState::Playing); + } + else + { + SetState(Screen); + } +} + +UUIInGameScreenSet* UUIInGameFlowSubsystem::GetScreenSet() +{ + if (ScreenSet) { return ScreenSet; } + if (ScreenSetAsset.IsNull()) + { + UE_LOG(LogZMMO, Warning, + TEXT("InGameFlow: ScreenSetAsset nao configurado em DefaultGame.ini " + "[/Script/ZMMO.UIInGameFlowSubsystem] ScreenSetAsset=...")); + return nullptr; + } + // Sync load — DA e' pequeno (so' soft refs); PIE/dev OK. + ScreenSet = ScreenSetAsset.LoadSynchronous(); + return ScreenSet; +} + +UUIManagerSubsystem* UUIInGameFlowSubsystem::GetUIManager() const +{ + if (const UGameInstance* GI = GetGameInstance()) + { + if (ULocalPlayer* LP = GI->GetFirstGamePlayer()) + { + return LP->GetSubsystem(); + } + } + return nullptr; +} + +FGameplayTag UUIInGameFlowSubsystem::LayerForState(EZMMOInGameUIState State) const +{ + switch (State) + { + case EZMMOInGameUIState::Playing: + return ZMMOUITags::UI_Layer_Game.GetTag(); + case EZMMOInGameUIState::Loading: + return ZMMOUITags::UI_Layer_Modal.GetTag(); + case EZMMOInGameUIState::StatusWindow: + case EZMMOInGameUIState::Inventory: + case EZMMOInGameUIState::SkillTree: + case EZMMOInGameUIState::EscapeMenu: + default: + return ZMMOUITags::UI_Layer_GameMenu.GetTag(); + } +} + +void UUIInGameFlowSubsystem::ResolveAndPushScreen(EZMMOInGameUIState State) +{ + if (State == EZMMOInGameUIState::None) { return; } + + UUIInGameScreenSet* SS = GetScreenSet(); + if (!SS) + { + UE_LOG(LogZMMO, Warning, + TEXT("InGameFlow: ScreenSet ausente; tela de %s ignorada"), + *UEnum::GetValueAsString(State)); + return; + } + + const TSoftClassPtr Soft = SS->GetScreenForState(State); + if (Soft.IsNull()) + { + UE_LOG(LogZMMO, Warning, + TEXT("InGameFlow: Screen for state %s not configured em DA_InGameScreenSet (verificar StateScreens)"), + *UEnum::GetValueAsString(State)); + return; + } + + UUIManagerSubsystem* Mgr = GetUIManager(); + if (!Mgr) + { + UE_LOG(LogZMMO, Warning, TEXT("InGameFlow: UIManagerSubsystem ausente")); + return; + } + + const FGameplayTag Layer = LayerForState(State); + const FSoftObjectPath Path = Soft.ToSoftObjectPath(); + + UAssetManager::GetStreamableManager().RequestAsyncLoad( + Path, + FStreamableDelegate::CreateWeakLambda(this, [this, Soft, Layer, State]() + { + UUIManagerSubsystem* M = GetUIManager(); + if (!M) { return; } + UClass* Cls = Soft.Get(); + if (!Cls) + { + UE_LOG(LogZMMO, Warning, + TEXT("InGameFlow: async load falhou pra %s"), + *UEnum::GetValueAsString(State)); + return; + } + UCommonActivatableWidget* W = M->PushScreenToLayer(Layer, Cls); + UE_LOG(LogZMMO, Log, + TEXT("InGameFlow: tela %s pushed em %s (widget=%s)"), + *UEnum::GetValueAsString(State), *Layer.ToString(), + W ? *W->GetName() : TEXT("null")); + })); +} diff --git a/Source/ZMMO/Game/UI/InGame/UIInGameFlowSubsystem.h b/Source/ZMMO/Game/UI/InGame/UIInGameFlowSubsystem.h new file mode 100644 index 0000000..5037bde --- /dev/null +++ b/Source/ZMMO/Game/UI/InGame/UIInGameFlowSubsystem.h @@ -0,0 +1,97 @@ +#pragma once + +#include "CoreMinimal.h" +#include "Subsystems/GameInstanceSubsystem.h" +#include "UI/InGameTypes.h" +#include "UIInGameFlowSubsystem.generated.h" + +class UUIInGameScreenSet; +class UUIManagerSubsystem; +class UCommonActivatableWidget; +struct FGameplayTag; + +DECLARE_DYNAMIC_MULTICAST_DELEGATE_OneParam(FOnZMMOInGameStateChanged, EZMMOInGameUIState, NewState); + +/** + * Orquestrador da UI in-game. Espelho simetrico do UUIFrontEndFlowSubsystem + * (Game/UI/FrontEnd/UIFrontEndFlowSubsystem.h) — o front-end gerencia + * Boot/Login/ServerSelect/Lobby; este subsystem gerencia + * HUD/StatusWindow/Inventory/SkillTree/EscapeMenu in-world. + * + * UGameInstanceSubsystem pra persistir entre OpenLevel (futuro: trocar + * de mapa in-game preserva estado da UI). Inicializa em modo `None`; + * caller (`AZMMOPlayerCharacter::HandleLocalSpawnReady`) chama + * `StartInGame()` ao confirmar spawn local, que vai pra `Playing`. + * + * Convencao de layer (resolvida internamente): + * - Playing -> UI.Layer.Game + * - StatusWindow/Inventory/ + * SkillTree/EscapeMenu -> UI.Layer.GameMenu + * - Loading -> UI.Layer.Modal + * + * Adicionar nova tela in-game = 3 passos: + * 1. Adicionar entry em `EZMMOInGameUIState` + * 2. Adicionar entry no DA_InGameScreenSet (estado -> WBP) + * 3. (se for em layer custom) adicionar caso em `LayerForState` + * Subsystem nao precisa de mudancas. + */ +UCLASS(Config = Game) +class ZMMO_API UUIInGameFlowSubsystem : public UGameInstanceSubsystem +{ + GENERATED_BODY() + +public: + virtual void Initialize(FSubsystemCollectionBase& Collection) override; + virtual void Deinitialize() override; + + /** + * Ativa a UI in-game. Chamado pelo `AZMMOPlayerCharacter::HandleLocalSpawnReady` + * quando o servidor confirma o spawn local. Garante root layout, limpa + * layers do front-end (Menu/Modal residuais), e vai pra `Playing` (HUD). + */ + UFUNCTION(BlueprintCallable, Category = "InGame|UI") + void StartInGame(); + + /** Define o estado top-level. Resolve a tela do DA + push no layer correto. */ + UFUNCTION(BlueprintCallable, Category = "InGame|UI") + void SetState(EZMMOInGameUIState NewState); + + UFUNCTION(BlueprintPure, Category = "InGame|UI") + EZMMOInGameUIState GetCurrentState() const { return CurrentState; } + + /** + * Atalho UX: alterna entre `Playing` e a tela passada. Util pra teclas + * de toggle (Alt+A abre/fecha StatusWindow, I abre/fecha Inventory). + * Se ja' estiver em `Screen`, volta pra Playing; senao vai pra `Screen`. + */ + UFUNCTION(BlueprintCallable, Category = "InGame|UI") + void ToggleScreen(EZMMOInGameUIState Screen); + + /** Disparado a cada SetState bem-sucedido. */ + UPROPERTY(BlueprintAssignable, Category = "InGame|UI") + FOnZMMOInGameStateChanged OnStateChanged; + +protected: + /** DA com o mapa estado->tela. Config em DefaultGame.ini: + * [/Script/ZMMO.UIInGameFlowSubsystem] + * ScreenSetAsset=/Game/ZMMO/UI/InGame/DA_InGameScreenSet.DA_InGameScreenSet + */ + UPROPERTY(Config, EditDefaultsOnly, Category = "InGame|UI") + TSoftObjectPtr ScreenSetAsset; + +private: + UUIInGameScreenSet* GetScreenSet(); + UUIManagerSubsystem* GetUIManager() const; + + /** Retorna a layer tag onde a tela do estado deve ir. */ + FGameplayTag LayerForState(EZMMOInGameUIState State) const; + + /** Resolve a soft class + push no layer apropriado (async load). */ + void ResolveAndPushScreen(EZMMOInGameUIState State); + + UPROPERTY(Transient) + EZMMOInGameUIState CurrentState = EZMMOInGameUIState::None; + + UPROPERTY(Transient) + TObjectPtr ScreenSet; +}; diff --git a/Source/ZMMO/Game/UI/InGame/UIInGameScreenSet.cpp b/Source/ZMMO/Game/UI/InGame/UIInGameScreenSet.cpp new file mode 100644 index 0000000..9e14b95 --- /dev/null +++ b/Source/ZMMO/Game/UI/InGame/UIInGameScreenSet.cpp @@ -0,0 +1,7 @@ +#include "UIInGameScreenSet.h" + +TSoftClassPtr UUIInGameScreenSet::GetScreenForState(EZMMOInGameUIState State) const +{ + const TSoftClassPtr* Found = StateScreens.Find(State); + return Found ? *Found : TSoftClassPtr(); +} diff --git a/Source/ZMMO/Game/UI/InGame/UIInGameScreenSet.h b/Source/ZMMO/Game/UI/InGame/UIInGameScreenSet.h new file mode 100644 index 0000000..2a0bd06 --- /dev/null +++ b/Source/ZMMO/Game/UI/InGame/UIInGameScreenSet.h @@ -0,0 +1,35 @@ +#pragma once + +#include "CoreMinimal.h" +#include "Engine/DataAsset.h" +#include "Templates/SubclassOf.h" +#include "UI/InGameTypes.h" +#include "UIInGameScreenSet.generated.h" + +class UCommonActivatableWidget; + +/** + * Mapa data-driven estado -> tela in-game. Espelho de UUIFrontEndScreenSet + * (Game/UI/FrontEnd/UIFrontEndScreenSet.h) — mesma estrategia: as classes + * concretas das telas (WBP) vivem no DA, nao em codigo. Subsystem orquestra. + * + * Asset concreto: DA_InGameScreenSet em Content/ZMMO/UI/InGame/ + * (criado via MCP no editor). + * + * Adicionar nova tela = adicionar entry em EZMMOInGameUIState + entry no DA + * + WBP herdando UCommonActivatableWidget. Subsystem nao muda. + */ +UCLASS(BlueprintType) +class ZMMO_API UUIInGameScreenSet : public UDataAsset +{ + GENERATED_BODY() + +public: + /** Tela por estado in-game. */ + UPROPERTY(EditDefaultsOnly, BlueprintReadOnly, Category = "InGame") + TMap> StateScreens; + + /** Soft class da tela do estado (vazio se nao configurado). */ + UFUNCTION(BlueprintCallable, Category = "InGame") + TSoftClassPtr GetScreenForState(EZMMOInGameUIState State) const; +}; diff --git a/Source/ZMMO/Game/UI/InGame/ZMMOHudWidget.cpp b/Source/ZMMO/Game/UI/InGame/ZMMOHudWidget.cpp new file mode 100644 index 0000000..378ca3b --- /dev/null +++ b/Source/ZMMO/Game/UI/InGame/ZMMOHudWidget.cpp @@ -0,0 +1,122 @@ +#include "ZMMOHudWidget.h" + +#include "CommonInputTypeEnum.h" // ECommonInputMode +#include "Engine/World.h" +#include "GameFramework/PlayerController.h" +#include "GameFramework/PlayerState.h" +#include "ZMMO.h" +#include "ZMMOAttributeComponent.h" +#include "ZMMOHudHpSpWidget.h" + +UZMMOHudWidget::UZMMOHudWidget(const FObjectInitializer& ObjectInitializer) + : Super(ObjectInitializer) +{ + // HUD nao bloqueia "back" (Esc / B no controller). Menus reais (StatusWindow, + // EscapeMenu) sim — eles setam bIsBackHandler=true. + bIsBackHandler = false; + // HUD nao reativa stack ao virar topo (e' sempre fundo). Garante que o + // UCommonActivatableWidgetStack do Layer.Game nao force activate/deactivate. + bAutoActivate = true; +} + +TOptional UZMMOHudWidget::GetDesiredInputConfig() const +{ + // Modo Game: input flui pro PlayerController/Pawn, mouse capturado (3a pessoa). + // Sem isso, CommonUI assume Menu config (mostra cursor + bloqueia movimento). + return FUIInputConfig(ECommonInputMode::Game, EMouseCaptureMode::CapturePermanently); +} + +void UZMMOHudWidget::NativeOnActivated() +{ + Super::NativeOnActivated(); + // HUD nao intercepta cliques — mouse passa direto pro Pawn (camera, etc.). + // Sub-widgets que precisam de click (futuro: hotkeys) podem override. + SetVisibility(ESlateVisibility::HitTestInvisible); + BindToLocalPlayer(); + UE_LOG(LogZMMO, Log, TEXT("ZMMOHudWidget activated (HpSpBar=%s)"), + HpSpBar ? *HpSpBar->GetName() : TEXT("none")); +} + +void UZMMOHudWidget::NativeOnDeactivated() +{ + UnbindFromComponent(); + Super::NativeOnDeactivated(); +} + +void UZMMOHudWidget::NativeDestruct() +{ + UnbindFromComponent(); + Super::NativeDestruct(); +} + +void UZMMOHudWidget::BindToLocalPlayer() +{ + UWorld* World = GetWorld(); + if (!World) { return; } + + APlayerController* PC = World->GetFirstPlayerController(); + if (!PC) { return; } + APlayerState* PS = PC->PlayerState; + if (!PS) + { + // PlayerState pode nao existir ainda — race com GameMode setup. + // AZMMOPlayerCharacter::HandleLocalSpawnReady chama BindToAttributeComponent + // manualmente como fallback (seed do EntityId). + UE_LOG(LogZMMO, Log, TEXT("ZMMOHudWidget: PlayerState ausente no activate (race fix via HandleLocalSpawnReady esperado)")); + return; + } + UZMMOAttributeComponent* Comp = PS->FindComponentByClass(); + if (!Comp) { return; } + if (Comp == BoundComponent.Get()) { return; } // idempotente + + UnbindFromComponent(); + BoundComponent = Comp; + + Comp->OnAttributesChanged.AddDynamic(this, &UZMMOHudWidget::HandleAttributesChanged); + Comp->OnHpSpChanged.AddDynamic(this, &UZMMOHudWidget::HandleHpSpChanged); + Comp->OnLevelUp.AddDynamic(this, &UZMMOHudWidget::HandleLevelUp); + + // Refresh imediato — caso o snapshot ja' tenha chegado. + HandleAttributesChanged(Comp->GetSnapshot()); +} + +void UZMMOHudWidget::UnbindFromComponent() +{ + if (UZMMOAttributeComponent* Old = BoundComponent.Get()) + { + Old->OnAttributesChanged.RemoveDynamic(this, &UZMMOHudWidget::HandleAttributesChanged); + Old->OnHpSpChanged.RemoveDynamic(this, &UZMMOHudWidget::HandleHpSpChanged); + Old->OnLevelUp.RemoveDynamic(this, &UZMMOHudWidget::HandleLevelUp); + } + BoundComponent.Reset(); +} + +void UZMMOHudWidget::HandleAttributesChanged(const FZMMOAttributesSnapshot& Snapshot) +{ + // Propaga snapshot aos sub-widgets que ja existem. Em V1 so' HpSpBar. + if (HpSpBar) + { + HpSpBar->ApplySnapshot(Snapshot); + } + // Futuros: PlayerInfo->ApplySnapshot, Buffs->RefreshIcons, etc. +} + +void UZMMOHudWidget::HandleHpSpChanged(int32 Hp, int32 Sp) +{ + if (UZMMOAttributeComponent* Comp = BoundComponent.Get()) + { + const FZMMOAttributesSnapshot& Snap = Comp->GetSnapshot(); + if (HpSpBar) + { + HpSpBar->ApplyHpSp(Hp, Snap.MaxHp, Sp, Snap.MaxSp); + } + } +} + +void UZMMOHudWidget::HandleLevelUp(int32 NewBaseLevel, int32 StatusPointDelta) +{ + // Snapshot subsequente do AttributeSystem vai disparar HandleAttributesChanged + // e atualizar HpSpBar->LevelText. Aqui e' o ponto pra efeitos visuais + // instantaneos (toast "LEVEL UP", confetti, sound). V1 noop — UI pode + // hook via Blueprint depois. +} diff --git a/Source/ZMMO/Game/UI/InGame/ZMMOHudWidget.h b/Source/ZMMO/Game/UI/InGame/ZMMOHudWidget.h new file mode 100644 index 0000000..28d6de6 --- /dev/null +++ b/Source/ZMMO/Game/UI/InGame/ZMMOHudWidget.h @@ -0,0 +1,102 @@ +#pragma once + +#include "CommonActivatableWidget.h" +#include "CoreMinimal.h" +#include "ZMMOAttributeTypes.h" +#include "ZMMOHudWidget.generated.h" + +class UZMMOAttributeComponent; +class UZMMOHudHpSpWidget; + +/** + * Root da HUD de gameplay. UCommonActivatableWidget pushed pelo + * UUIInGameFlowSubsystem em UI.Layer.Game quando o estado vira `Playing`. + * + * **Vive em ZMMO core** (UI/InGame/) porque o HUD root e' generico pra + * qualquer GameType — sub-widgets vem dos modulos especializados + * (HpSpBar do sub-modulo ZMMOAttributes, Minimap de ZMMOMap futuro, + * Chat de ZMMOChat futuro, etc.). + * + * Padrao **composite**: o WBP_HUD (filho) declara as PECAS visuais como + * sub-widgets BindWidgetOptional, e este C++ orquestra: + * 1. Em NativeOnActivated: auto-binda no UZMMOAttributeComponent do + * player local (vive no PlayerState via Component Registry) + * 2. Subscreve aos delegates do componente (OnAttributesChanged, + * OnHpSpChanged, OnLevelUp) + * 3. Propaga os deltas pros sub-widgets via metodos publicos + * (HpSpBar->ApplySnapshot, HpSpBar->ApplyHpSp, etc.) + * + * Composicao prevista (sub-widgets — todos `BindWidgetOptional` pra + * permitir GameTypes sem modulos opcionais — ex.: Survival sem stats RO): + * - HpSpBar (UZMMOHudHpSpWidget — sub-modulo ZMMOAttributes) — impl + * - PlayerInfo (futuro — nome/job/level/exp bar) + * - Minimap (futuro) + * - QuickBar (futuro — hotkeys de skills) + * - Buffs (futuro — icones SC ativos) + * - Chat (futuro) + * - TargetInfo (futuro — HP/SP/nome do mob/player targetado) + * + * Adicionar novo sub-widget = adicionar campo BindWidgetOptional aqui + + * setup no NativeOnActivated/handlers + adicionar no WBP_HUD. + * + * Pra mudar o LAYOUT visual ou skin do HUD: edita o WBP_HUD (filho) + * no Designer — sem tocar este C++. + */ +UCLASS(Abstract, Blueprintable, BlueprintType) +class ZMMO_API UZMMOHudWidget : public UCommonActivatableWidget +{ + GENERATED_BODY() + +public: + UZMMOHudWidget(const FObjectInitializer& ObjectInitializer); + +protected: + virtual void NativeOnActivated() override; + virtual void NativeOnDeactivated() override; + virtual void NativeDestruct() override; + + /** + * HUD passa input direto pro Pawn — sem capturar mouse/teclado. Override + * retorna `TOptional(unset)`. Sem isso, UCommonActivatableWidget + * default usa Menu config (`SetInputMode_GameAndUI` + mouse cursor), o que + * desativa input de gameplay. + * + * Menus (StatusWindow, Inventory, etc.) override e' diferente — eles + * retornam GameAndMenu/Menu pra mostrar cursor + bloquear movement. + */ + virtual TOptional GetDesiredInputConfig() const override; + + /// Sub-widget HP/SP/level (sub-modulo ZMMOAttributes). BindWidgetOptional + /// — WBP_HUD pode omitir se o GameType nao usa Attributes (Survival, etc.). + UPROPERTY(BlueprintReadOnly, meta = (BindWidgetOptional), Category = "Zeus|HUD") + UZMMOHudHpSpWidget* HpSpBar = nullptr; + + // === Futuros sub-widgets === + // UPROPERTY(BlueprintReadOnly, meta = (BindWidgetOptional)) UZMMOHudPlayerInfoWidget* PlayerInfo = nullptr; + // UPROPERTY(BlueprintReadOnly, meta = (BindWidgetOptional)) UZMMOHudMinimapWidget* Minimap = nullptr; + // UPROPERTY(BlueprintReadOnly, meta = (BindWidgetOptional)) UZMMOHudQuickBarWidget* QuickBar = nullptr; + // UPROPERTY(BlueprintReadOnly, meta = (BindWidgetOptional)) UZMMOHudBuffsWidget* Buffs = nullptr; + // UPROPERTY(BlueprintReadOnly, meta = (BindWidgetOptional)) UZMMOHudChatWidget* Chat = nullptr; + // UPROPERTY(BlueprintReadOnly, meta = (BindWidgetOptional)) UZMMOHudTargetInfoWidget* TargetInfo = nullptr; + + UFUNCTION(BlueprintPure, Category = "Zeus|HUD") + UZMMOAttributeComponent* GetBoundComponent() const { return BoundComponent.Get(); } + +private: + /// Tenta achar o UZMMOAttributeComponent do player local + subscrever + /// nos delegates. Idempotente. + void BindToLocalPlayer(); + void UnbindFromComponent(); + + UFUNCTION() + void HandleAttributesChanged(const FZMMOAttributesSnapshot& Snapshot); + + UFUNCTION() + void HandleHpSpChanged(int32 Hp, int32 Sp); + + UFUNCTION() + void HandleLevelUp(int32 NewBaseLevel, int32 StatusPointDelta); + + UPROPERTY(Transient) + TWeakObjectPtr BoundComponent; +}; diff --git a/Source/ZMMOAttributes/Private/ZMMOAttributeNetworkHandler.cpp b/Source/ZMMOAttributes/Private/ZMMOAttributeNetworkHandler.cpp index 6b0543b..da21f17 100644 --- a/Source/ZMMOAttributes/Private/ZMMOAttributeNetworkHandler.cpp +++ b/Source/ZMMOAttributes/Private/ZMMOAttributeNetworkHandler.cpp @@ -2,8 +2,8 @@ #include "Engine/GameInstance.h" #include "Engine/World.h" -#include "EngineUtils.h" // TActorIterator -#include "GameFramework/Actor.h" +#include "GameFramework/GameStateBase.h" +#include "GameFramework/PlayerState.h" #include "ZMMOAttributeComponent.h" #include "ZMMOAttributeTypes.h" #include "ZeusNetworkSubsystem.h" @@ -45,25 +45,22 @@ namespace 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*`. + // Resolve `EntityId -> UZMMOAttributeComponent` via PlayerArray do GameState. + // AttributeComponent agora vive no PlayerState (vide AZMMOPlayerState + + // Component Registry em DefaultGame.ini). Itera apenas players (1 por + // conexao), nao 1000+ atores — O(N_players). // - // 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). + // Cada UZMMOAttributeComponent carrega seu proprio EntityId (seed em + // AZMMOPlayerCharacter::HandleLocalSpawnReady ou no primeiro snapshot). UZMMOAttributeComponent* FindComponentForEntity(UWorld* World, int32 EntityId) { if (!World || EntityId == 0) { return nullptr; } - for (TActorIterator It(World); It; ++It) + const AGameStateBase* GS = World->GetGameState(); + if (!GS) { return nullptr; } + for (APlayerState* PS : GS->PlayerArray) { - AActor* Actor = *It; - if (!Actor) { continue; } - UZMMOAttributeComponent* Comp = Actor->FindComponentByClass(); + if (!PS) { continue; } + UZMMOAttributeComponent* Comp = PS->FindComponentByClass(); if (!Comp) { continue; } if (Comp->GetSnapshot().EntityId == EntityId) { @@ -136,21 +133,23 @@ void UZMMOAttributeNetworkHandler::HandleAttributeSnapshotFull(const FZeusAttrib 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) + // Fallback chicken-and-egg: primeiro snapshot chega antes do EntityId + // estar seeded no componente. Aplica ao primeiro componente do + // PlayerArray que ainda tem EntityId=0. Apos o primeiro apply, o + // caminho normal por lookup funciona. + const AGameStateBase* GS = World->GetGameState(); + if (GS) { - AActor* Actor = *It; - if (!Actor) { continue; } - Comp = Actor->FindComponentByClass(); - if (Comp && Comp->GetSnapshot().EntityId == 0) + for (APlayerState* PS : GS->PlayerArray) { - break; + if (!PS) { continue; } + UZMMOAttributeComponent* Candidate = PS->FindComponentByClass(); + if (Candidate && Candidate->GetSnapshot().EntityId == 0) + { + Comp = Candidate; + break; + } } - Comp = nullptr; } } if (Comp) diff --git a/Source/ZMMOAttributes/Private/ZMMOHudHpSpWidget.cpp b/Source/ZMMOAttributes/Private/ZMMOHudHpSpWidget.cpp index 2a67494..543ea80 100644 --- a/Source/ZMMOAttributes/Private/ZMMOHudHpSpWidget.cpp +++ b/Source/ZMMOAttributes/Private/ZMMOHudHpSpWidget.cpp @@ -3,99 +3,43 @@ #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) +void UZMMOHudHpSpWidget::ApplySnapshot(const FZMMOAttributesSnapshot& Snapshot) { + LastSnapshot = Snapshot; OnSnapshotApplied(Snapshot); } -void UZMMOHudHpSpWidget::HandleHpSpChanged(int32 Hp, int32 Sp) +void UZMMOHudHpSpWidget::ApplyHpSp(int32 Hp, int32 MaxHp, int32 Sp, int32 MaxSp) { - OnHpSpDelta(Hp, Sp); + LastSnapshot.Hp = Hp; + LastSnapshot.MaxHp = MaxHp; + LastSnapshot.Sp = Sp; + LastSnapshot.MaxSp = MaxSp; + + if (HpBar) + { + HpBar->SetPercent(MaxHp > 0 ? static_cast(Hp) / static_cast(MaxHp) : 0.f); + } + if (SpBar) + { + SpBar->SetPercent(MaxSp > 0 ? static_cast(Sp) / static_cast(MaxSp) : 0.f); + } + if (HpText) + { + HpText->SetText(FText::FromString(FString::Printf(TEXT("%d / %d"), Hp, MaxHp))); + } + if (SpText) + { + SpText->SetText(FText::FromString(FString::Printf(TEXT("%d / %d"), Sp, MaxSp))); + } } 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))); - } + ApplyHpSp(Snapshot.Hp, Snapshot.MaxHp, 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/ZMMOHudHpSpWidget.h b/Source/ZMMOAttributes/Public/ZMMOHudHpSpWidget.h index 735b186..2713625 100644 --- a/Source/ZMMOAttributes/Public/ZMMOHudHpSpWidget.h +++ b/Source/ZMMOAttributes/Public/ZMMOHudHpSpWidget.h @@ -7,21 +7,27 @@ 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. + * Sub-widget do HUD: barras de HP/SP + label de level. Sem auto-bind no + * AttributeComponent — o parent (`UZMMOHudWidget`) propaga snapshots via + * `ApplySnapshot` / `ApplyHpSpDelta` quando o player local recebe updates. * - * 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. + * Composicao do HUD (ver `UZMMOHudWidget`): + * WBP_HUD (UZMMOHudWidget root) + * |-- HpSpBar (UZMMOHudHpSpWidget — este) + * |-- PlayerInfo (futuro — nome/job/level) + * |-- Minimap (futuro) + * |-- QuickBar (futuro — hotkeys) + * |-- Buffs (futuro — icones SC) + * |-- Chat (futuro) + * |-- TargetInfo (futuro — mob/player targetado) * - * 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). + * Cada sub-widget tem responsabilidade unica (SRP). O parent (UZMMOHudWidget) + * faz binding com AttributeComponent e propaga deltas. + * + * Convencao `meta=(BindWidget)`: o WBP filho deve ter widgets com nomes + * exatos (HpBar, SpBar). `BindWidgetOptional` permite omitir. */ UCLASS(Abstract, Blueprintable, BlueprintType) class ZMMOATTRIBUTES_API UZMMOHudHpSpWidget : public UUserWidget @@ -29,34 +35,24 @@ 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. - */ + /** Atualiza todos os campos a partir do snapshot. Chamado pelo UZMMOHudWidget. */ UFUNCTION(BlueprintCallable, Category = "Zeus|HUD") - void BindToAttributeComponent(UZMMOAttributeComponent* InComponent); + void ApplySnapshot(const FZMMOAttributesSnapshot& Snapshot); - UFUNCTION(BlueprintPure, Category = "Zeus|HUD") - UZMMOAttributeComponent* GetBoundComponent() const { return BoundComponent.Get(); } + /** Atualiza apenas HP/SP (sem mexer no level/text). Otimizacao do tick. */ + UFUNCTION(BlueprintCallable, Category = "Zeus|HUD") + void ApplyHpSp(int32 Hp, int32 MaxHp, int32 Sp, int32 MaxSp); 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`. + * (cores, animacao de level up, etc.). Impl 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; @@ -74,12 +70,7 @@ protected: UTextBlock* LevelText = nullptr; private: - UFUNCTION() - void HandleAttributesChanged(const FZMMOAttributesSnapshot& Snapshot); - - UFUNCTION() - void HandleHpSpChanged(int32 Hp, int32 Sp); - - UPROPERTY() - TWeakObjectPtr BoundComponent; + /// Ultimo snapshot recebido (cache pra ApplyHpSp manter MaxHp/MaxSp). */ + UPROPERTY(Transient) + FZMMOAttributesSnapshot LastSnapshot; }; diff --git a/Source/ZMMOAttributes/ZMMOAttributes.Build.cs b/Source/ZMMOAttributes/ZMMOAttributes.Build.cs index d42700a..85eba18 100644 --- a/Source/ZMMOAttributes/ZMMOAttributes.Build.cs +++ b/Source/ZMMOAttributes/ZMMOAttributes.Build.cs @@ -12,6 +12,8 @@ public class ZMMOAttributes : ModuleRules "Engine", "UMG", "Slate", + "CommonUI", + "CommonInput", "ZeusNetwork" }); diff --git a/Source/ZMMOAttributes/module.json b/Source/ZMMOAttributes/module.json index ad8b1a9..73f678f 100644 --- a/Source/ZMMOAttributes/module.json +++ b/Source/ZMMOAttributes/module.json @@ -4,7 +4,7 @@ "version": "0.1.0", "side": "client", "dependencies": { - "engine": ["Core", "CoreUObject", "Engine", "UMG", "Slate"], + "engine": ["Core", "CoreUObject", "Engine", "UMG", "Slate", "CommonUI", "CommonInput"], "plugins": ["ZeusNetwork"], "modules": [] }, -- 2.54.0.windows.1 From f585b0d2b92f61ff7bfd5dad42b693d2d5cbc1c9 Mon Sep 17 00:00:00 2001 From: Mateus Rodrigues Date: Sat, 23 May 2026 11:22:58 -0300 Subject: [PATCH 3/6] =?UTF-8?q?feat(attributes/ui):=20Status=20Window=20co?= =?UTF-8?q?m=20aloca=C3=A7=C3=A3o=20STR/AGI/VIT/INT/DEX/LUK=20+=20derivado?= =?UTF-8?q?s?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - ZMMOStatusWindowWidget (UCommonActivatableWidget, modo Menu): grid 2 colunas estilo RO. Esquerda: stats primários com botões + por stat (RequestStatAlloc). Direita: derivados ATK/MATK/DEF/MDEF/HIT/FLEE/CRIT/ASPD vindos do snapshot do server — cliente NUNCA recalcula (anti-cheat foundation). - Botão + envia C_STAT_ALLOC → server valida cost RO (floor((stat-1)/10)+2), aplica + recalcula derivados + SaveCharFull async, manda S_ATTRIBUTE_SNAPSHOT_FULL + reply. UI atualiza em tempo real via OnAttributesChanged. - ZMMOAttributeComponent ganha RequestStatAlloc + NotifyStatAllocReply + delegate OnStatAllocReply para feedback de rejeição. - ZMMOAttributeNetworkHandler: HandleStatAllocReply roteia pro componente do player local (reply não traz EntityId — sempre quem fez o request). - PlayerController: hotkey Alt+A (FInputChord legacy + BindKey, sem precisar de IA asset). ToggleStatusWindow via UUIInGameFlowSubsystem. - WBP_StatusWindow: layout 2 colunas via MCP set_widget_tree (43 widgets) + BgBorder escuro semi-transparente + centralizado no overlay. - DA_InGameScreenSet: StatusWindow → WBP_StatusWindow. --- Content/ZMMO/UI/HUD/WBP_StatusWindow.uasset | Bin 0 -> 81121 bytes .../ZMMO/UI/InGame/DA_InGameScreenSet.uasset | Bin 1560 -> 1711 bytes .../Game/Controller/ZMMOPlayerController.cpp | 30 ++++ .../Game/Controller/ZMMOPlayerController.h | 3 + .../Game/UI/InGame/ZMMOStatusWindowWidget.cpp | 162 ++++++++++++++++++ .../Game/UI/InGame/ZMMOStatusWindowWidget.h | 106 ++++++++++++ .../Private/ZMMOAttributeComponent.cpp | 19 ++ .../Private/ZMMOAttributeNetworkHandler.cpp | 27 +++ .../Public/ZMMOAttributeComponent.h | 19 ++ .../Public/ZMMOAttributeNetworkHandler.h | 6 + 10 files changed, 372 insertions(+) create mode 100644 Content/ZMMO/UI/HUD/WBP_StatusWindow.uasset create mode 100644 Source/ZMMO/Game/UI/InGame/ZMMOStatusWindowWidget.cpp create mode 100644 Source/ZMMO/Game/UI/InGame/ZMMOStatusWindowWidget.h diff --git a/Content/ZMMO/UI/HUD/WBP_StatusWindow.uasset b/Content/ZMMO/UI/HUD/WBP_StatusWindow.uasset new file mode 100644 index 0000000000000000000000000000000000000000..0311ba8817b3c5c56606314a33cf96ec6648513e GIT binary patch literal 81121 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 literal 0 HcmV?d00001 diff --git a/Content/ZMMO/UI/InGame/DA_InGameScreenSet.uasset b/Content/ZMMO/UI/InGame/DA_InGameScreenSet.uasset index 16bcec6d6234aa58d070e8a418c1a58b4bfb3f56..1c5275ba6b7451420d8ba149224afe15024b7433 100644 GIT binary patch delta 611 zcmbQivz~W?glO~WJ0?>DkM+bC?Nq)X{UpFbN~GlHMBR8^Q3eKvg^UagOhEej#F;u) z|Ct#WfP97)7Koq;kOqM-NHjlhU$%Q1-yqR34+rE_4iBU6X8FwiV?o$<~LY8$z9C&uVZ{>oG>$_sZ3 z#D^d*-{eAOVKy$X%*0vh^#V{ef>8P&(A6SPHYbz@Q80BNYheHs8X$nI4&-ZyD1;P( za=3wX6O;`K6)2co#UePlfth8pD~k$a-Q++PVK6U*MFdPwWYJ~i0jhpF`7q1;dXRm< zh=2edD9r(-L6XoI14k3g4p7v>912njHvq^6ISK|8U<{CAs(bW#W$~7eA!s3KKf+dMBR8kAqED9g^UagK*W&2Jeik8 z-44iNs9}Z(F|sf)Fah}+fqW1s1oB}tNDYV&(pn0nYk|0zW#WG&Mxn_{jKNBWfcz({ z5cM$fKM>>r#dv@MFIXo}XVjVej$5tx~z>r4@6D&W1a5>G7M%Q0~b_38WkdVFlC$1}K_QCQUCw| diff --git a/Source/ZMMO/Game/Controller/ZMMOPlayerController.cpp b/Source/ZMMO/Game/Controller/ZMMOPlayerController.cpp index b6fb1cb..801f186 100644 --- a/Source/ZMMO/Game/Controller/ZMMOPlayerController.cpp +++ b/Source/ZMMO/Game/Controller/ZMMOPlayerController.cpp @@ -1,12 +1,17 @@ #include "ZMMOPlayerController.h" #include "Blueprint/UserWidget.h" +#include "Components/InputComponent.h" #include "EnhancedInputSubsystems.h" +#include "Engine/GameInstance.h" #include "Engine/LocalPlayer.h" +#include "Framework/Commands/InputChord.h" #include "InputMappingContext.h" #include "UObject/ConstructorHelpers.h" #include "Widgets/Input/SVirtualJoystick.h" #include "ZMMO.h" +#include "UI/InGame/UIInGameFlowSubsystem.h" +#include "UI/InGameTypes.h" AZMMOPlayerController::AZMMOPlayerController() { @@ -86,6 +91,31 @@ void AZMMOPlayerController::SetupInputComponent() } } } + + // Hotkey global Alt+A -> toggle StatusWindow. Uso InputComponent legacy + // (BindKey com FInputChord) pra nao depender de assets InputAction. + // Migrar pra Enhanced Input (IA_OpenStatus, IA_OpenInventory, etc.) quando + // vier outro menu (Inventory I, SkillTree K). + if (InputComponent) + { + FInputKeyBinding& Binding = InputComponent->BindKey( + FInputChord(EKeys::A, /*bShift*/ false, /*bCtrl*/ false, /*bAlt*/ true, /*bCmd*/ false), + IE_Pressed, + this, &AZMMOPlayerController::ToggleStatusWindow); + Binding.bConsumeInput = true; + Binding.bExecuteWhenPaused = true; // dispara mesmo com StatusWindow aberto (CommonUI menu mode) + } +} + +void AZMMOPlayerController::ToggleStatusWindow() +{ + if (UGameInstance* GI = GetGameInstance()) + { + if (UUIInGameFlowSubsystem* Flow = GI->GetSubsystem()) + { + Flow->ToggleScreen(EZMMOInGameUIState::StatusWindow); + } + } } bool AZMMOPlayerController::ShouldUseTouchControls() const diff --git a/Source/ZMMO/Game/Controller/ZMMOPlayerController.h b/Source/ZMMO/Game/Controller/ZMMOPlayerController.h index 83a8d76..1f2c057 100644 --- a/Source/ZMMO/Game/Controller/ZMMOPlayerController.h +++ b/Source/ZMMO/Game/Controller/ZMMOPlayerController.h @@ -47,4 +47,7 @@ protected: virtual void SetupInputComponent() override; bool ShouldUseTouchControls() const; + + /** Hotkey Alt+A → toggle StatusWindow via UUIInGameFlowSubsystem. */ + void ToggleStatusWindow(); }; diff --git a/Source/ZMMO/Game/UI/InGame/ZMMOStatusWindowWidget.cpp b/Source/ZMMO/Game/UI/InGame/ZMMOStatusWindowWidget.cpp new file mode 100644 index 0000000..b32c6f0 --- /dev/null +++ b/Source/ZMMO/Game/UI/InGame/ZMMOStatusWindowWidget.cpp @@ -0,0 +1,162 @@ +#include "ZMMOStatusWindowWidget.h" + +#include "CommonInputTypeEnum.h" +#include "Components/Button.h" +#include "Components/TextBlock.h" +#include "Engine/World.h" +#include "GameFramework/PlayerController.h" +#include "GameFramework/PlayerState.h" +#include "Internationalization/Text.h" +#include "ZMMO.h" +#include "ZMMOAttributeComponent.h" +#include "UI/InGame/UIInGameFlowSubsystem.h" +#include "UI/InGameTypes.h" + +UZMMOStatusWindowWidget::UZMMOStatusWindowWidget(const FObjectInitializer& ObjectInitializer) + : Super(ObjectInitializer) +{ + bIsBackHandler = true; // Esc/B fecha + bAutoActivate = true; +} + +TOptional UZMMOStatusWindowWidget::GetDesiredInputConfig() const +{ + // Modo Menu: cursor visivel, input flui pra UI (clicks nos botões), + // movimento bloqueado. Padrao pra menus modais. + return FUIInputConfig(ECommonInputMode::Menu, EMouseCaptureMode::NoCapture); +} + +void UZMMOStatusWindowWidget::NativeOnActivated() +{ + Super::NativeOnActivated(); + BindToLocalPlayer(); + + if (PlusBtn_Str) PlusBtn_Str->OnClicked.AddDynamic(this, &UZMMOStatusWindowWidget::OnPlusStr); + if (PlusBtn_Agi) PlusBtn_Agi->OnClicked.AddDynamic(this, &UZMMOStatusWindowWidget::OnPlusAgi); + if (PlusBtn_Vit) PlusBtn_Vit->OnClicked.AddDynamic(this, &UZMMOStatusWindowWidget::OnPlusVit); + if (PlusBtn_Int) PlusBtn_Int->OnClicked.AddDynamic(this, &UZMMOStatusWindowWidget::OnPlusInt); + if (PlusBtn_Dex) PlusBtn_Dex->OnClicked.AddDynamic(this, &UZMMOStatusWindowWidget::OnPlusDex); + if (PlusBtn_Luk) PlusBtn_Luk->OnClicked.AddDynamic(this, &UZMMOStatusWindowWidget::OnPlusLuk); + if (CloseBtn) CloseBtn->OnClicked.AddDynamic(this, &UZMMOStatusWindowWidget::OnCloseClicked); + + UE_LOG(LogZMMO, Log, TEXT("ZMMOStatusWindow activated")); +} + +void UZMMOStatusWindowWidget::NativeOnDeactivated() +{ + UnbindFromComponent(); + + if (PlusBtn_Str) PlusBtn_Str->OnClicked.RemoveDynamic(this, &UZMMOStatusWindowWidget::OnPlusStr); + if (PlusBtn_Agi) PlusBtn_Agi->OnClicked.RemoveDynamic(this, &UZMMOStatusWindowWidget::OnPlusAgi); + if (PlusBtn_Vit) PlusBtn_Vit->OnClicked.RemoveDynamic(this, &UZMMOStatusWindowWidget::OnPlusVit); + if (PlusBtn_Int) PlusBtn_Int->OnClicked.RemoveDynamic(this, &UZMMOStatusWindowWidget::OnPlusInt); + if (PlusBtn_Dex) PlusBtn_Dex->OnClicked.RemoveDynamic(this, &UZMMOStatusWindowWidget::OnPlusDex); + if (PlusBtn_Luk) PlusBtn_Luk->OnClicked.RemoveDynamic(this, &UZMMOStatusWindowWidget::OnPlusLuk); + if (CloseBtn) CloseBtn->OnClicked.RemoveDynamic(this, &UZMMOStatusWindowWidget::OnCloseClicked); + + Super::NativeOnDeactivated(); +} + +void UZMMOStatusWindowWidget::BindToLocalPlayer() +{ + UWorld* World = GetWorld(); + if (!World) { return; } + APlayerController* PC = World->GetFirstPlayerController(); + if (!PC || !PC->PlayerState) { return; } + UZMMOAttributeComponent* Comp = PC->PlayerState->FindComponentByClass(); + if (!Comp || Comp == BoundComponent.Get()) { return; } + + UnbindFromComponent(); + BoundComponent = Comp; + Comp->OnAttributesChanged.AddDynamic(this, &UZMMOStatusWindowWidget::HandleAttributesChanged); + Comp->OnStatAllocReply.AddDynamic(this, &UZMMOStatusWindowWidget::HandleStatAllocReply); + RefreshFromSnapshot(Comp->GetSnapshot()); +} + +void UZMMOStatusWindowWidget::UnbindFromComponent() +{ + if (UZMMOAttributeComponent* Old = BoundComponent.Get()) + { + Old->OnAttributesChanged.RemoveDynamic(this, &UZMMOStatusWindowWidget::HandleAttributesChanged); + Old->OnStatAllocReply.RemoveDynamic(this, &UZMMOStatusWindowWidget::HandleStatAllocReply); + } + BoundComponent.Reset(); +} + +void UZMMOStatusWindowWidget::HandleAttributesChanged(const FZMMOAttributesSnapshot& Snapshot) +{ + RefreshFromSnapshot(Snapshot); +} + +void UZMMOStatusWindowWidget::HandleStatAllocReply(bool bAccepted, int32 Reason) +{ + UE_LOG(LogZMMO, Log, TEXT("StatusWindow: alloc reply accepted=%d reason=%d"), + bAccepted ? 1 : 0, Reason); + // Snapshot subsequente vai disparar HandleAttributesChanged e atualizar UI. + // Aqui poderia popup de erro visual quando bAccepted=false (TODO polish). +} + +void UZMMOStatusWindowWidget::RefreshFromSnapshot(const FZMMOAttributesSnapshot& S) +{ + if (StrText) StrText->SetText(FText::FromString(FString::Printf(TEXT("STR %d"), S.Str))); + if (AgiText) AgiText->SetText(FText::FromString(FString::Printf(TEXT("AGI %d"), S.Agi))); + if (VitText) VitText->SetText(FText::FromString(FString::Printf(TEXT("VIT %d"), S.Vit))); + if (IntText) IntText->SetText(FText::FromString(FString::Printf(TEXT("INT %d"), S.Int))); + if (DexText) DexText->SetText(FText::FromString(FString::Printf(TEXT("DEX %d"), S.Dex))); + if (LukText) LukText->SetText(FText::FromString(FString::Printf(TEXT("LUK %d"), S.Luk))); + if (StatusPointText) + { + StatusPointText->SetText(FText::FromString( + FString::Printf(TEXT("Status Points: %d"), S.StatusPoint))); + } + + // Derivados — sempre vem prontos do servidor (camadas equip/buff ja somadas). + // CritX10 vem internamente x10 (1.0 = 10); divide pra exibicao com 1 casa. + if (AtkText) AtkText->SetText(FText::FromString(FString::Printf(TEXT("ATK %d"), S.Atk))); + if (MatkText) MatkText->SetText(FText::FromString(FString::Printf(TEXT("MATK %d"), S.Matk))); + if (DefText) DefText->SetText(FText::FromString(FString::Printf(TEXT("DEF %d"), S.Def))); + if (MdefText) MdefText->SetText(FText::FromString(FString::Printf(TEXT("MDEF %d"), S.Mdef))); + if (HitText) HitText->SetText(FText::FromString(FString::Printf(TEXT("HIT %d"), S.Hit))); + if (FleeText) FleeText->SetText(FText::FromString(FString::Printf(TEXT("FLEE %d"), S.Flee))); + if (CritText) + { + const int32 CritInt = S.CritX10 / 10; + const int32 CritFrac = S.CritX10 % 10; + CritText->SetText(FText::FromString(FString::Printf(TEXT("CRIT %d.%d"), CritInt, CritFrac))); + } + if (AspdText) AspdText->SetText(FText::FromString(FString::Printf(TEXT("ASPD %d"), S.Aspd))); + // Habilita botões `+` somente se ha status_point disponível. + const bool bCanAlloc = S.StatusPoint > 0; + if (PlusBtn_Str) PlusBtn_Str->SetIsEnabled(bCanAlloc); + if (PlusBtn_Agi) PlusBtn_Agi->SetIsEnabled(bCanAlloc); + if (PlusBtn_Vit) PlusBtn_Vit->SetIsEnabled(bCanAlloc); + if (PlusBtn_Int) PlusBtn_Int->SetIsEnabled(bCanAlloc); + if (PlusBtn_Dex) PlusBtn_Dex->SetIsEnabled(bCanAlloc); + if (PlusBtn_Luk) PlusBtn_Luk->SetIsEnabled(bCanAlloc); +} + +void UZMMOStatusWindowWidget::RequestAlloc(int32 StatId) +{ + if (UZMMOAttributeComponent* Comp = BoundComponent.Get()) + { + Comp->RequestStatAlloc(StatId, /*Amount*/ 1); + } +} + +void UZMMOStatusWindowWidget::OnPlusStr() { RequestAlloc(0); } +void UZMMOStatusWindowWidget::OnPlusAgi() { RequestAlloc(1); } +void UZMMOStatusWindowWidget::OnPlusVit() { RequestAlloc(2); } +void UZMMOStatusWindowWidget::OnPlusInt() { RequestAlloc(3); } +void UZMMOStatusWindowWidget::OnPlusDex() { RequestAlloc(4); } +void UZMMOStatusWindowWidget::OnPlusLuk() { RequestAlloc(5); } + +void UZMMOStatusWindowWidget::OnCloseClicked() +{ + if (UGameInstance* GI = GetGameInstance()) + { + if (UUIInGameFlowSubsystem* Flow = GI->GetSubsystem()) + { + Flow->SetState(EZMMOInGameUIState::Playing); + } + } +} diff --git a/Source/ZMMO/Game/UI/InGame/ZMMOStatusWindowWidget.h b/Source/ZMMO/Game/UI/InGame/ZMMOStatusWindowWidget.h new file mode 100644 index 0000000..af8bcef --- /dev/null +++ b/Source/ZMMO/Game/UI/InGame/ZMMOStatusWindowWidget.h @@ -0,0 +1,106 @@ +#pragma once + +#include "CommonActivatableWidget.h" +#include "CoreMinimal.h" +#include "ZMMOAttributeTypes.h" +#include "ZMMOStatusWindowWidget.generated.h" + +class UButton; +class UTextBlock; +class UCommonButtonBase; +class UZMMOAttributeComponent; + +/** + * Tela "Status" — atributos do jogador + alocacao via botões `+`. + * + * Modo Menu: cursor visível, movimento bloqueado, Esc fecha. Push em + * UI.Layer.GameMenu pelo `UUIInGameFlowSubsystem::ToggleScreen(StatusWindow)`. + * HUD continua visível em UI.Layer.Game por baixo. + * + * Composicao do WBP_StatusWindow (BindWidgetOptional pra permitir variantes): + * - Labels de stats primarios: StrText / AgiText / VitText / IntText / DexText / LukText + * - Botões +: PlusBtn_Str / PlusBtn_Agi / PlusBtn_Vit / PlusBtn_Int / PlusBtn_Dex / PlusBtn_Luk + * - Derivados (server-authoritative): AtkText / MatkText / DefText / MdefText / + * HitText / FleeText / CritText / AspdText + * - StatusPointText (pontos disponíveis pra alocar) + * - CloseBtn (fecha — fallback do Esc/back handler) + * + * Workflow: + * 1. NativeOnActivated: busca AttributeComponent (PlayerState do player local) + * 2. Subscreve OnAttributesChanged + OnStatAllocReply + * 3. Refresh inicial via snapshot atual + * 4. Clique no `+X` chama `Comp->RequestStatAlloc(X, 1)` → server valida + * 5. Server responde com S_ATTRIBUTE_STAT_ALLOC_OK + (se ok) snapshot novo + * 6. UI atualiza via delegate + */ +UCLASS(Abstract, Blueprintable, BlueprintType) +class ZMMO_API UZMMOStatusWindowWidget : public UCommonActivatableWidget +{ + GENERATED_BODY() + +public: + UZMMOStatusWindowWidget(const FObjectInitializer& ObjectInitializer); + +protected: + virtual void NativeOnActivated() override; + virtual void NativeOnDeactivated() override; + + /// Modo Menu: cursor visível + bloqueia input de gameplay (movimento). + virtual TOptional GetDesiredInputConfig() const override; + + /// Texto dos stats (label + valor). BindWidgetOptional pra permitir + /// WBPs custom (ex.: sem display de SP em variantes). + UPROPERTY(BlueprintReadOnly, meta = (BindWidgetOptional), Category = "Zeus|Status") UTextBlock* StrText = nullptr; + UPROPERTY(BlueprintReadOnly, meta = (BindWidgetOptional), Category = "Zeus|Status") UTextBlock* AgiText = nullptr; + UPROPERTY(BlueprintReadOnly, meta = (BindWidgetOptional), Category = "Zeus|Status") UTextBlock* VitText = nullptr; + UPROPERTY(BlueprintReadOnly, meta = (BindWidgetOptional), Category = "Zeus|Status") UTextBlock* IntText = nullptr; + UPROPERTY(BlueprintReadOnly, meta = (BindWidgetOptional), Category = "Zeus|Status") UTextBlock* DexText = nullptr; + UPROPERTY(BlueprintReadOnly, meta = (BindWidgetOptional), Category = "Zeus|Status") UTextBlock* LukText = nullptr; + + /// Pontos restantes pra alocar + label "Status Points: N". + UPROPERTY(BlueprintReadOnly, meta = (BindWidgetOptional), Category = "Zeus|Status") UTextBlock* StatusPointText = nullptr; + + /// Derivados (sempre calculados pelo servidor — cliente NUNCA recalcula). + /// Mostra `base + bonus` quando aplicavel (camadas equip/buff vem do server + /// pre-somadas no snapshot). BindWidgetOptional pra suportar HUDs minimalistas. + UPROPERTY(BlueprintReadOnly, meta = (BindWidgetOptional), Category = "Zeus|Status|Derived") UTextBlock* AtkText = nullptr; + UPROPERTY(BlueprintReadOnly, meta = (BindWidgetOptional), Category = "Zeus|Status|Derived") UTextBlock* MatkText = nullptr; + UPROPERTY(BlueprintReadOnly, meta = (BindWidgetOptional), Category = "Zeus|Status|Derived") UTextBlock* DefText = nullptr; + UPROPERTY(BlueprintReadOnly, meta = (BindWidgetOptional), Category = "Zeus|Status|Derived") UTextBlock* MdefText = nullptr; + UPROPERTY(BlueprintReadOnly, meta = (BindWidgetOptional), Category = "Zeus|Status|Derived") UTextBlock* HitText = nullptr; + UPROPERTY(BlueprintReadOnly, meta = (BindWidgetOptional), Category = "Zeus|Status|Derived") UTextBlock* FleeText = nullptr; + UPROPERTY(BlueprintReadOnly, meta = (BindWidgetOptional), Category = "Zeus|Status|Derived") UTextBlock* CritText = nullptr; + UPROPERTY(BlueprintReadOnly, meta = (BindWidgetOptional), Category = "Zeus|Status|Derived") UTextBlock* AspdText = nullptr; + + /// Botoes + por stat. + UPROPERTY(BlueprintReadOnly, meta = (BindWidgetOptional), Category = "Zeus|Status") UButton* PlusBtn_Str = nullptr; + UPROPERTY(BlueprintReadOnly, meta = (BindWidgetOptional), Category = "Zeus|Status") UButton* PlusBtn_Agi = nullptr; + UPROPERTY(BlueprintReadOnly, meta = (BindWidgetOptional), Category = "Zeus|Status") UButton* PlusBtn_Vit = nullptr; + UPROPERTY(BlueprintReadOnly, meta = (BindWidgetOptional), Category = "Zeus|Status") UButton* PlusBtn_Int = nullptr; + UPROPERTY(BlueprintReadOnly, meta = (BindWidgetOptional), Category = "Zeus|Status") UButton* PlusBtn_Dex = nullptr; + UPROPERTY(BlueprintReadOnly, meta = (BindWidgetOptional), Category = "Zeus|Status") UButton* PlusBtn_Luk = nullptr; + + /// Botao Close (fallback do Esc/back). + UPROPERTY(BlueprintReadOnly, meta = (BindWidgetOptional), Category = "Zeus|Status") UButton* CloseBtn = nullptr; + +private: + void BindToLocalPlayer(); + void UnbindFromComponent(); + void RefreshFromSnapshot(const FZMMOAttributesSnapshot& Snap); + + UFUNCTION() void HandleAttributesChanged(const FZMMOAttributesSnapshot& Snapshot); + UFUNCTION() void HandleStatAllocReply(bool bAccepted, int32 Reason); + + UFUNCTION() void OnPlusStr(); + UFUNCTION() void OnPlusAgi(); + UFUNCTION() void OnPlusVit(); + UFUNCTION() void OnPlusInt(); + UFUNCTION() void OnPlusDex(); + UFUNCTION() void OnPlusLuk(); + UFUNCTION() void OnCloseClicked(); + + void RequestAlloc(int32 StatId); + + UPROPERTY(Transient) + TWeakObjectPtr BoundComponent; +}; diff --git a/Source/ZMMOAttributes/Private/ZMMOAttributeComponent.cpp b/Source/ZMMOAttributes/Private/ZMMOAttributeComponent.cpp index fc7d371..d2aa07d 100644 --- a/Source/ZMMOAttributes/Private/ZMMOAttributeComponent.cpp +++ b/Source/ZMMOAttributes/Private/ZMMOAttributeComponent.cpp @@ -1,5 +1,8 @@ #include "ZMMOAttributeComponent.h" +#include "Engine/GameInstance.h" +#include "ZeusNetworkSubsystem.h" + UZMMOAttributeComponent::UZMMOAttributeComponent() { PrimaryComponentTick.bCanEverTick = false; @@ -34,3 +37,19 @@ void UZMMOAttributeComponent::NotifyLevelUp(int32 NewBaseLevel, int32 StatusPoin // aqui apenas dispara o delegate para efeitos visuais imediatos. OnLevelUp.Broadcast(NewBaseLevel, StatusPointDelta); } + +void UZMMOAttributeComponent::NotifyStatAllocReply(bool bAccepted, int32 Reason) +{ + OnStatAllocReply.Broadcast(bAccepted, Reason); +} + +void UZMMOAttributeComponent::RequestStatAlloc(int32 StatId, int32 Amount) +{ + UWorld* World = GetWorld(); + if (!World) { return; } + UGameInstance* GI = World->GetGameInstance(); + if (!GI) { return; } + UZeusNetworkSubsystem* Net = GI->GetSubsystem(); + if (!Net) { return; } + Net->SendStatAlloc(StatId, Amount); +} diff --git a/Source/ZMMOAttributes/Private/ZMMOAttributeNetworkHandler.cpp b/Source/ZMMOAttributes/Private/ZMMOAttributeNetworkHandler.cpp index da21f17..3a20330 100644 --- a/Source/ZMMOAttributes/Private/ZMMOAttributeNetworkHandler.cpp +++ b/Source/ZMMOAttributes/Private/ZMMOAttributeNetworkHandler.cpp @@ -3,6 +3,7 @@ #include "Engine/GameInstance.h" #include "Engine/World.h" #include "GameFramework/GameStateBase.h" +#include "GameFramework/PlayerController.h" #include "GameFramework/PlayerState.h" #include "ZMMOAttributeComponent.h" #include "ZMMOAttributeTypes.h" @@ -91,6 +92,8 @@ void UZMMOAttributeNetworkHandler::Initialize(FSubsystemCollectionBase& Collecti this, &UZMMOAttributeNetworkHandler::HandleHpSpUpdate); LevelUpHandle = Net->OnLevelUp.AddUObject( this, &UZMMOAttributeNetworkHandler::HandleLevelUp); + StatAllocReplyHandle = Net->OnStatAllocReply.AddUObject( + this, &UZMMOAttributeNetworkHandler::HandleStatAllocReply); } } @@ -113,6 +116,11 @@ void UZMMOAttributeNetworkHandler::Deinitialize() Net->OnLevelUp.Remove(LevelUpHandle); LevelUpHandle.Reset(); } + if (StatAllocReplyHandle.IsValid()) + { + Net->OnStatAllocReply.Remove(StatAllocReplyHandle); + StatAllocReplyHandle.Reset(); + } } Super::Deinitialize(); } @@ -179,3 +187,22 @@ void UZMMOAttributeNetworkHandler::HandleLevelUp(int32 EntityId, int32 NewBaseLe Comp->NotifyLevelUp(NewBaseLevel, StatusPointDelta); } } + +void UZMMOAttributeNetworkHandler::HandleStatAllocReply(bool bAccepted, int32 Reason) +{ + // S_ATTRIBUTE_STAT_ALLOC_OK nao traz EntityId no payload — sempre do + // player local que fez o request. Busca o AttributeComponent via PC. + if (UZMMOAttributeComponent* Comp = FindLocalPlayerAttributeComponent()) + { + Comp->NotifyStatAllocReply(bAccepted, Reason); + } +} + +UZMMOAttributeComponent* UZMMOAttributeNetworkHandler::FindLocalPlayerAttributeComponent() const +{ + UWorld* World = GetWorld(); + if (!World) { return nullptr; } + APlayerController* PC = World->GetFirstPlayerController(); + if (!PC || !PC->PlayerState) { return nullptr; } + return PC->PlayerState->FindComponentByClass(); +} diff --git a/Source/ZMMOAttributes/Public/ZMMOAttributeComponent.h b/Source/ZMMOAttributes/Public/ZMMOAttributeComponent.h index efc5fb5..22522ed 100644 --- a/Source/ZMMOAttributes/Public/ZMMOAttributeComponent.h +++ b/Source/ZMMOAttributes/Public/ZMMOAttributeComponent.h @@ -9,6 +9,10 @@ DECLARE_DYNAMIC_MULTICAST_DELEGATE_OneParam(FZMMOOnAttributesChanged, const FZMM DECLARE_DYNAMIC_MULTICAST_DELEGATE_TwoParams(FZMMOOnHpSpChanged, int32, Hp, int32, Sp); DECLARE_DYNAMIC_MULTICAST_DELEGATE_TwoParams(FZMMOOnLevelUp, int32, NewBaseLevel, int32, StatusPointDelta); +/// Resposta server ao C_STAT_ALLOC. Reason e' EAllocRejectReason (None=0, +/// InvalidStat=1, InvalidAmount=2, NotEnoughPoints=3, StatCapped=4, NoSession=5). +DECLARE_DYNAMIC_MULTICAST_DELEGATE_TwoParams(FZMMOOnStatAllocReply, bool, bAccepted, int32, Reason); + /** * Componente de atributos do MMO ligado ao ator que o representa em-jogo * (player local, proxies remotos, NPCs futuros). @@ -44,6 +48,17 @@ public: UFUNCTION(BlueprintCallable, Category = "Zeus|Attributes") void NotifyLevelUp(int32 NewBaseLevel, int32 StatusPointDelta); + /// Notifica resposta server ao C_STAT_ALLOC. UI pode reagir (toast/SFX + /// quando aceito, erro vermelho quando rejeitado). + UFUNCTION(BlueprintCallable, Category = "Zeus|Attributes") + void NotifyStatAllocReply(bool bAccepted, int32 Reason); + + /// Envia C_STAT_ALLOC pro server. Wrapper conveniente — busca o + /// UZeusNetworkSubsystem via GameInstance + chama SendStatAlloc. + /// `StatId`: 0=STR, 1=AGI, 2=VIT, 3=INT, 4=DEX, 5=LUK. `Amount`: 1..10. + UFUNCTION(BlueprintCallable, Category = "Zeus|Attributes") + void RequestStatAlloc(int32 StatId, int32 Amount = 1); + /// Seed do EntityId vindo de S_SPAWN_PLAYER local. Chamado pelo /// `AZMMOPlayerCharacter` antes do primeiro S_ATTRIBUTE_SNAPSHOT_FULL /// chegar, garantindo que o NetworkHandler consiga rotear via lookup @@ -80,6 +95,10 @@ public: UPROPERTY(BlueprintAssignable, Category = "Zeus|Attributes") FZMMOOnLevelUp OnLevelUp; + /** Resposta ao C_STAT_ALLOC (aceito ou rejeitado). UI reage. */ + UPROPERTY(BlueprintAssignable, Category = "Zeus|Attributes") + FZMMOOnStatAllocReply OnStatAllocReply; + protected: UPROPERTY(BlueprintReadOnly, Category = "Zeus|Attributes") FZMMOAttributesSnapshot Current; diff --git a/Source/ZMMOAttributes/Public/ZMMOAttributeNetworkHandler.h b/Source/ZMMOAttributes/Public/ZMMOAttributeNetworkHandler.h index 45ba179..0f47348 100644 --- a/Source/ZMMOAttributes/Public/ZMMOAttributeNetworkHandler.h +++ b/Source/ZMMOAttributes/Public/ZMMOAttributeNetworkHandler.h @@ -36,10 +36,16 @@ private: void HandleAttributeSnapshotFull(const FZeusAttributesPayload& Payload); void HandleHpSpUpdate(int32 EntityId, int32 Hp, int32 Sp); void HandleLevelUp(int32 EntityId, int32 NewBaseLevel, int32 StatusPointDelta); + void HandleStatAllocReply(bool bAccepted, int32 Reason); UZeusNetworkSubsystem* GetZeusNetSubsystem() const; + /// Acha o AttributeComponent do player local (Pawn->PlayerState). + /// Usado pro StatAllocReply (sem EntityId no payload). + class UZMMOAttributeComponent* FindLocalPlayerAttributeComponent() const; + FDelegateHandle SnapshotFullHandle; FDelegateHandle HpSpUpdateHandle; FDelegateHandle LevelUpHandle; + FDelegateHandle StatAllocReplyHandle; }; -- 2.54.0.windows.1 From 5418da185e0a9f9433e10eafc6aaf854803560b2 Mon Sep 17 00:00:00 2001 From: Mateus Rodrigues Date: Sat, 23 May 2026 11:47:10 -0300 Subject: [PATCH 4/6] =?UTF-8?q?feat(attributes/ui):=20display=20"ATK=20bas?= =?UTF-8?q?e=20+=20bonus"=20igual=20RO=20+=20ASPD=20=C3=BAnico?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - FZMMOAttributesSnapshot ganha 7 pares (AtkBase/AtkEquipBonus, ...) + Aspd único, espelhando split do server. - ZMMOAttributeNetworkHandler::ToSnapshot mapeia campos novos do FZeusAttributesPayload. - WBP_StatusWindow / RefreshFromSnapshot agora formata derivados como "ATK 51 + 0", "CRIT 1.3 + 0.0", "ASPD 150" (este sem split — cálculo não-linear no server via aspd_base × stats). Quando InventorySystem entrar, equipar arma muda `currentWeapon` no server, Recalculate roda, e UI mostra ATK/MATK/DEF/etc com bonus > 0 e ASPD ajustado pela aspd_base da arma equipada. Sem código de migração — schema do snapshot já está estabilizado. --- .../Game/UI/InGame/ZMMOStatusWindowWidget.cpp | 28 +++++++++------- .../Private/ZMMOAttributeNetworkHandler.cpp | 23 ++++++++----- .../Public/ZMMOAttributeTypes.h | 33 ++++++++++++++----- 3 files changed, 57 insertions(+), 27 deletions(-) diff --git a/Source/ZMMO/Game/UI/InGame/ZMMOStatusWindowWidget.cpp b/Source/ZMMO/Game/UI/InGame/ZMMOStatusWindowWidget.cpp index b32c6f0..0278c7e 100644 --- a/Source/ZMMO/Game/UI/InGame/ZMMOStatusWindowWidget.cpp +++ b/Source/ZMMO/Game/UI/InGame/ZMMOStatusWindowWidget.cpp @@ -110,20 +110,26 @@ void UZMMOStatusWindowWidget::RefreshFromSnapshot(const FZMMOAttributesSnapshot& FString::Printf(TEXT("Status Points: %d"), S.StatusPoint))); } - // Derivados — sempre vem prontos do servidor (camadas equip/buff ja somadas). - // CritX10 vem internamente x10 (1.0 = 10); divide pra exibicao com 1 casa. - if (AtkText) AtkText->SetText(FText::FromString(FString::Printf(TEXT("ATK %d"), S.Atk))); - if (MatkText) MatkText->SetText(FText::FromString(FString::Printf(TEXT("MATK %d"), S.Matk))); - if (DefText) DefText->SetText(FText::FromString(FString::Printf(TEXT("DEF %d"), S.Def))); - if (MdefText) MdefText->SetText(FText::FromString(FString::Printf(TEXT("MDEF %d"), S.Mdef))); - if (HitText) HitText->SetText(FText::FromString(FString::Printf(TEXT("HIT %d"), S.Hit))); - if (FleeText) FleeText->SetText(FText::FromString(FString::Printf(TEXT("FLEE %d"), S.Flee))); + // Derivados — formato RO "STAT base + equip" (ex.: "ATK 51 + 0"). + // Servidor manda os dois separados; cliente NUNCA recalcula. + if (AtkText) AtkText->SetText(FText::FromString(FString::Printf(TEXT("ATK %d + %d"), S.AtkBase, S.AtkEquipBonus))); + if (MatkText) MatkText->SetText(FText::FromString(FString::Printf(TEXT("MATK %d + %d"), S.MatkBase, S.MatkEquipBonus))); + if (DefText) DefText->SetText(FText::FromString(FString::Printf(TEXT("DEF %d + %d"), S.DefBase, S.DefEquipBonus))); + if (MdefText) MdefText->SetText(FText::FromString(FString::Printf(TEXT("MDEF %d + %d"), S.MdefBase, S.MdefEquipBonus))); + if (HitText) HitText->SetText(FText::FromString(FString::Printf(TEXT("HIT %d + %d"), S.HitBase, S.HitEquipBonus))); + if (FleeText) FleeText->SetText(FText::FromString(FString::Printf(TEXT("FLEE %d + %d"), S.FleeBase, S.FleeEquipBonus))); if (CritText) { - const int32 CritInt = S.CritX10 / 10; - const int32 CritFrac = S.CritX10 % 10; - CritText->SetText(FText::FromString(FString::Printf(TEXT("CRIT %d.%d"), CritInt, CritFrac))); + // CRIT vem ×10 em ambas as camadas — soma X10 antes de converter pra + // decimal (10 = 1.0). Display: "CRIT base + equip" com 1 casa cada. + const int32 BaseInt = S.CritBaseX10 / 10; + const int32 BaseFrac = S.CritBaseX10 % 10; + const int32 EquipInt = S.CritEquipBonusX10 / 10; + const int32 EquipFrac = S.CritEquipBonusX10 % 10; + CritText->SetText(FText::FromString(FString::Printf( + TEXT("CRIT %d.%d + %d.%d"), BaseInt, BaseFrac, EquipInt, EquipFrac))); } + // ASPD nao splita (calculo nao-linear server-side via aspd_base × stats). if (AspdText) AspdText->SetText(FText::FromString(FString::Printf(TEXT("ASPD %d"), S.Aspd))); // Habilita botões `+` somente se ha status_point disponível. const bool bCanAlloc = S.StatusPoint > 0; diff --git a/Source/ZMMOAttributes/Private/ZMMOAttributeNetworkHandler.cpp b/Source/ZMMOAttributes/Private/ZMMOAttributeNetworkHandler.cpp index 3a20330..8512a5f 100644 --- a/Source/ZMMOAttributes/Private/ZMMOAttributeNetworkHandler.cpp +++ b/Source/ZMMOAttributes/Private/ZMMOAttributeNetworkHandler.cpp @@ -35,14 +35,21 @@ namespace 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; + S.AtkBase = P.AtkBase; + S.AtkEquipBonus = P.AtkEquipBonus; + S.MatkBase = P.MatkBase; + S.MatkEquipBonus = P.MatkEquipBonus; + S.DefBase = P.DefBase; + S.DefEquipBonus = P.DefEquipBonus; + S.MdefBase = P.MdefBase; + S.MdefEquipBonus = P.MdefEquipBonus; + S.HitBase = P.HitBase; + S.HitEquipBonus = P.HitEquipBonus; + S.FleeBase = P.FleeBase; + S.FleeEquipBonus = P.FleeEquipBonus; + S.CritBaseX10 = P.CritBaseX10; + S.CritEquipBonusX10 = P.CritEquipBonusX10; + S.Aspd = P.Aspd; return S; } diff --git a/Source/ZMMOAttributes/Public/ZMMOAttributeTypes.h b/Source/ZMMOAttributes/Public/ZMMOAttributeTypes.h index 339525c..3d893e8 100644 --- a/Source/ZMMOAttributes/Public/ZMMOAttributeTypes.h +++ b/Source/ZMMOAttributes/Public/ZMMOAttributeTypes.h @@ -50,14 +50,31 @@ struct ZMMOATTRIBUTES_API FZMMOAttributesSnapshot // === 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; + // === Derivados split em pares (base, equipBonus) === + // Padrao RO: display mostra "ATK 51 + 0" (base + equip). Cliente NUNCA + // recalcula — servidor envia ambos prontos no S_ATTRIBUTE_SNAPSHOT_FULL. + UPROPERTY(BlueprintReadOnly, Category = "Zeus|Attributes|Derived") int32 AtkBase = 0; + UPROPERTY(BlueprintReadOnly, Category = "Zeus|Attributes|Derived") int32 AtkEquipBonus = 0; + + UPROPERTY(BlueprintReadOnly, Category = "Zeus|Attributes|Derived") int32 MatkBase = 0; + UPROPERTY(BlueprintReadOnly, Category = "Zeus|Attributes|Derived") int32 MatkEquipBonus = 0; + + UPROPERTY(BlueprintReadOnly, Category = "Zeus|Attributes|Derived") int32 DefBase = 0; + UPROPERTY(BlueprintReadOnly, Category = "Zeus|Attributes|Derived") int32 DefEquipBonus = 0; + + UPROPERTY(BlueprintReadOnly, Category = "Zeus|Attributes|Derived") int32 MdefBase = 0; + UPROPERTY(BlueprintReadOnly, Category = "Zeus|Attributes|Derived") int32 MdefEquipBonus = 0; + + UPROPERTY(BlueprintReadOnly, Category = "Zeus|Attributes|Derived") int32 HitBase = 0; + UPROPERTY(BlueprintReadOnly, Category = "Zeus|Attributes|Derived") int32 HitEquipBonus = 0; + + UPROPERTY(BlueprintReadOnly, Category = "Zeus|Attributes|Derived") int32 FleeBase = 0; + UPROPERTY(BlueprintReadOnly, Category = "Zeus|Attributes|Derived") int32 FleeEquipBonus = 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 CritBaseX10 = 0; + UPROPERTY(BlueprintReadOnly, Category = "Zeus|Attributes|Derived") int32 CritEquipBonusX10 = 0; + + /** ASPD nao splita — calculo nao-linear via aspd_base × stats. Display direto. */ UPROPERTY(BlueprintReadOnly, Category = "Zeus|Attributes|Derived") int32 Aspd = 0; }; -- 2.54.0.windows.1 From dbc2898a4b076cdd16e4ea75e4ffe5916d135170 Mon Sep 17 00:00:00 2001 From: Mateus Rodrigues Date: Sat, 23 May 2026 12:55:57 -0300 Subject: [PATCH 5/6] feat(attributes/ui): toast "LEVEL UP!" no HUD via S_LEVEL_UP delegate MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - UZMMOHudWidget ganha LevelUpToastContainer (BindWidgetOptional UWidget) e LevelUpToastText (BindWidgetOptional UTextBlock) — both Collapsed por default; HandleLevelUp os mostra com text "LEVEL UP! Lv N" e dispara timer FTimerHandle pra esconder após LevelUpToastDurationSec (2.5s). - WBP_HUD: Border escuro semi-transparente (R=0.05,G=0.04,B=0.10,A=0.85) com TextBlock dourado (R=1.0,G=0.85,B=0.30) fonte 32, slot top-center com padding top=120, visibility default Collapsed. - Snapshot pós-level-up (vem logo após S_LEVEL_UP) atualiza HP/SP/level via HpSpBar automaticamente — toast é só feedback visual instantâneo. Polish futuro (não desta frente): SFX cue + particle aura dourada + animação de fade in/out via UMG. --- Content/ZMMO/UI/HUD/WBP_HUD.uasset | Bin 28757 -> 30345 bytes Source/ZMMO/Game/UI/InGame/ZMMOHudWidget.cpp | 44 +++++++++++++++++-- Source/ZMMO/Game/UI/InGame/ZMMOHudWidget.h | 24 ++++++++++ 3 files changed, 64 insertions(+), 4 deletions(-) diff --git a/Content/ZMMO/UI/HUD/WBP_HUD.uasset b/Content/ZMMO/UI/HUD/WBP_HUD.uasset index b3bc8f539d6b63578573d5df1e471d1a7cacd0c5..2c72fabc385e61837be16f613d0c79e10bb0a7a2 100644 GIT binary patch literal 30345 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 delta 8484 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 OFlC@5ECrm?H2fE&2KpiZ diff --git a/Source/ZMMO/Game/UI/InGame/ZMMOHudWidget.cpp b/Source/ZMMO/Game/UI/InGame/ZMMOHudWidget.cpp index 378ca3b..1e7c69e 100644 --- a/Source/ZMMO/Game/UI/InGame/ZMMOHudWidget.cpp +++ b/Source/ZMMO/Game/UI/InGame/ZMMOHudWidget.cpp @@ -1,9 +1,12 @@ #include "ZMMOHudWidget.h" +#include "Components/TextBlock.h" +#include "Components/Widget.h" #include "CommonInputTypeEnum.h" // ECommonInputMode #include "Engine/World.h" #include "GameFramework/PlayerController.h" #include "GameFramework/PlayerState.h" +#include "TimerManager.h" #include "ZMMO.h" #include "ZMMOAttributeComponent.h" #include "ZMMOHudHpSpWidget.h" @@ -32,6 +35,8 @@ void UZMMOHudWidget::NativeOnActivated() // HUD nao intercepta cliques — mouse passa direto pro Pawn (camera, etc.). // Sub-widgets que precisam de click (futuro: hotkeys) podem override. SetVisibility(ESlateVisibility::HitTestInvisible); + // Toast comeca escondido — so' aparece em HandleLevelUp. + HideLevelUpToast(); BindToLocalPlayer(); UE_LOG(LogZMMO, Log, TEXT("ZMMOHudWidget activated (HpSpBar=%s)"), HpSpBar ? *HpSpBar->GetName() : TEXT("none")); @@ -115,8 +120,39 @@ void UZMMOHudWidget::HandleHpSpChanged(int32 Hp, int32 Sp) void UZMMOHudWidget::HandleLevelUp(int32 NewBaseLevel, int32 StatusPointDelta) { - // Snapshot subsequente do AttributeSystem vai disparar HandleAttributesChanged - // e atualizar HpSpBar->LevelText. Aqui e' o ponto pra efeitos visuais - // instantaneos (toast "LEVEL UP", confetti, sound). V1 noop — UI pode - // hook via Blueprint depois. + // Snapshot subsequente atualiza HpSpBar (level/maxhp/maxsp). Aqui mostra + // toast visual instantaneo. StatusPointDelta nao usado no texto (UI mostra + // no StatusWindow). Polish futuro: SFX + particle aura. + UE_LOG(LogZMMO, Log, TEXT("[HUD] LEVEL UP! newLevel=%d +sp=%d"), NewBaseLevel, StatusPointDelta); + ShowLevelUpToast(NewBaseLevel); +} + +void UZMMOHudWidget::ShowLevelUpToast(int32 NewBaseLevel) +{ + if (LevelUpToastText) + { + LevelUpToastText->SetText(FText::FromString( + FString::Printf(TEXT("LEVEL UP! Lv %d"), NewBaseLevel))); + } + if (LevelUpToastContainer) + { + LevelUpToastContainer->SetVisibility(ESlateVisibility::HitTestInvisible); + } + + // Cancela timer anterior (caso level up duplo em sequencia) + agenda hide. + if (UWorld* World = GetWorld()) + { + World->GetTimerManager().ClearTimer(LevelUpToastHideTimer); + World->GetTimerManager().SetTimer(LevelUpToastHideTimer, + FTimerDelegate::CreateUObject(this, &UZMMOHudWidget::HideLevelUpToast), + LevelUpToastDurationSec, /*loop*/ false); + } +} + +void UZMMOHudWidget::HideLevelUpToast() +{ + if (LevelUpToastContainer) + { + LevelUpToastContainer->SetVisibility(ESlateVisibility::Collapsed); + } } diff --git a/Source/ZMMO/Game/UI/InGame/ZMMOHudWidget.h b/Source/ZMMO/Game/UI/InGame/ZMMOHudWidget.h index 28d6de6..39f3553 100644 --- a/Source/ZMMO/Game/UI/InGame/ZMMOHudWidget.h +++ b/Source/ZMMO/Game/UI/InGame/ZMMOHudWidget.h @@ -2,9 +2,12 @@ #include "CommonActivatableWidget.h" #include "CoreMinimal.h" +#include "Engine/TimerHandle.h" #include "ZMMOAttributeTypes.h" #include "ZMMOHudWidget.generated.h" +class UTextBlock; +class UWidget; class UZMMOAttributeComponent; class UZMMOHudHpSpWidget; @@ -71,6 +74,22 @@ protected: UPROPERTY(BlueprintReadOnly, meta = (BindWidgetOptional), Category = "Zeus|HUD") UZMMOHudHpSpWidget* HpSpBar = nullptr; + /// Container do toast "LEVEL UP!" (Fase 3). Hidden por default; em + /// `HandleLevelUp` vira visivel por `LevelUpToastDurationSec` segundos + /// (ver `Show/HideLevelUpToast`). WBP_HUD coloca um Border + Image + TextBlock + /// dentro deste container; este C++ controla apenas o lifecycle visivel/hidden. + UPROPERTY(BlueprintReadOnly, meta = (BindWidgetOptional), Category = "Zeus|HUD|LevelUp") + UWidget* LevelUpToastContainer = nullptr; + + /// Texto exibido no toast — formato "LEVEL UP! Lv X" (X = novo level). + /// Tambem BindWidgetOptional; WBP_HUD pode usar texto estatico no design. + UPROPERTY(BlueprintReadOnly, meta = (BindWidgetOptional), Category = "Zeus|HUD|LevelUp") + UTextBlock* LevelUpToastText = nullptr; + + /// Duracao do toast em segundos. Configuravel no BP filho. + UPROPERTY(EditDefaultsOnly, Category = "Zeus|HUD|LevelUp") + float LevelUpToastDurationSec = 2.5f; + // === Futuros sub-widgets === // UPROPERTY(BlueprintReadOnly, meta = (BindWidgetOptional)) UZMMOHudPlayerInfoWidget* PlayerInfo = nullptr; // UPROPERTY(BlueprintReadOnly, meta = (BindWidgetOptional)) UZMMOHudMinimapWidget* Minimap = nullptr; @@ -99,4 +118,9 @@ private: UPROPERTY(Transient) TWeakObjectPtr BoundComponent; + + void ShowLevelUpToast(int32 NewBaseLevel); + void HideLevelUpToast(); + + FTimerHandle LevelUpToastHideTimer; }; -- 2.54.0.windows.1 From bacf987e0b8d308829fd487de14cc336b1a6235d Mon Sep 17 00:00:00 2001 From: Mateus Rodrigues Date: Sat, 23 May 2026 19:19:41 -0300 Subject: [PATCH 6/6] =?UTF-8?q?feat(jobs):=20novo=20m=C3=B3dulo=20ZMMOJobs?= =?UTF-8?q?=20(peer=20de=20ZMMOAttributes)=20+=20DA=5FJob=5FNovice?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Sub-módulo cliente do JobsSystem do server. Pavimenta dados visuais/UI por job (DisplayName localizado, ícone, anim override, som de level up) sem acoplar ao ZMMOAttributes. ARQUITETURA — espelha o peer-module pattern do server (PR #9): Server: Game/MMO/Modules/JobsSystem (módulo núcleo, LoadOrder=50) Client: Source/ZMMOJobs/ (LoadingPhase=PreDefault, peer de ZMMOAttributes) ZMMOAttributes NÃO depende de ZMMOJobs — server envia classId no snapshot, cliente faz lookup local via Library/Subsystem. NOVOS ARQUIVOS C++ module (Source/ZMMOJobs/): - module.json + ZMMOJobs.Build.cs (deps: Core/Engine/AssetRegistry) - ZMMOJobsModule.h/.cpp — IMPLEMENT_MODULE - ZMMOJobDataAsset.h/.cpp — UPrimaryDataAsset (BlueprintType) campos: ClassId, TechnicalName, DisplayName, JobIcon (soft), DefaultAnimInstanceClass, JobChangeMontage (soft), LevelUpSoundCue (soft) - ZMMOJobsSubsystem.h/.cpp — UGameInstanceSubsystem scan AssetRegistry no Initialize + cache TMap por ClassId O(1) GetJobData(ClassId) - ZMMOJobsLibrary.h/.cpp — UBlueprintFunctionLibrary GetJobDisplayName/GetJobData/IsJobRegistered fallback "Classe N" se ClassId desconhecido Content: - Content/ZMMO/Data/Jobs/DA_Job_Novice.uasset (ClassId=0, TechnicalName="Novice", DisplayName="Aprendiz") MODIFICADOS - ZMMO.uproject: + Module "ZMMOJobs" (LoadingPhase PreDefault) - Source/ZMMO/ZMMO.Build.cs: + "ZMMOJobs" em PublicDependencyModuleNames - Source/ZMMO/Game/UI/FrontEnd/UICharCard_Base.cpp:51 — primeiro consumer do Library: "Classe %d" hardcoded → UZMMOJobsLibrary::GetJobDisplayName(this, ClassId) Card da seleção de personagem agora mostra "Aprendiz" via DA lookup. BLUEPRINT ACCESS Toda a API exposta pra BP via UFUNCTION(BlueprintPure): - UZMMOJobsLibrary::GetJobDisplayName(Self, ClassId) → FText - UZMMOJobsLibrary::GetJobData(Self, ClassId) → UZMMOJobDataAsset* - UZMMOJobsSubsystem::GetJobData(ClassId) - UZMMOJobDataAsset propriedades BlueprintReadOnly (UMG binding direto) LOOKUP AssetRegistry varre /Game/ por UZMMOJobDataAsset no boot do GameInstance. Chave = campo ClassId do asset (não nome do arquivo — renomear .uasset OK). Lookup O(1) via TMap. Custo de scan amortizado em 1 vez por sessão (~50ms pra 50 jobs futuro). VALIDAÇÃO Editor: [Log] DA class found: [Log] Subsystem class found: [Log] Library class found: [Log] AssetRegistry encontrou 1 ZMMOJobDataAsset(s): - DA_Job_Novice @ /Game/ZMMO/Data/Jobs/DA_Job_Novice Próximo módulo cliente: 1 linha no .uproject + .Build.cs + ZMMOJobs.Build.cs não muda. Pattern estabelecido. Co-Authored-By: Claude Opus 4.7 --- Content/ZMMO/Data/Jobs/DA_Job_Novice.uasset | Bin 0 -> 1422 bytes .../ZMMO/Game/UI/FrontEnd/UICharCard_Base.cpp | 3 +- Source/ZMMO/ZMMO.Build.cs | 3 +- Source/ZMMOJobs/Private/ZMMOJobDataAsset.cpp | 5 ++ Source/ZMMOJobs/Private/ZMMOJobsLibrary.cpp | 54 +++++++++++++ Source/ZMMOJobs/Private/ZMMOJobsModule.cpp | 13 +++ Source/ZMMOJobs/Private/ZMMOJobsSubsystem.cpp | 67 ++++++++++++++++ Source/ZMMOJobs/Public/ZMMOJobDataAsset.h | 75 ++++++++++++++++++ Source/ZMMOJobs/Public/ZMMOJobsLibrary.h | 39 +++++++++ Source/ZMMOJobs/Public/ZMMOJobsModule.h | 23 ++++++ Source/ZMMOJobs/Public/ZMMOJobsSubsystem.h | 52 ++++++++++++ Source/ZMMOJobs/ZMMOJobs.Build.cs | 26 ++++++ Source/ZMMOJobs/module.json | 18 +++++ ZMMO.uproject | 5 ++ 14 files changed, 381 insertions(+), 2 deletions(-) create mode 100644 Content/ZMMO/Data/Jobs/DA_Job_Novice.uasset create mode 100644 Source/ZMMOJobs/Private/ZMMOJobDataAsset.cpp create mode 100644 Source/ZMMOJobs/Private/ZMMOJobsLibrary.cpp create mode 100644 Source/ZMMOJobs/Private/ZMMOJobsModule.cpp create mode 100644 Source/ZMMOJobs/Private/ZMMOJobsSubsystem.cpp create mode 100644 Source/ZMMOJobs/Public/ZMMOJobDataAsset.h create mode 100644 Source/ZMMOJobs/Public/ZMMOJobsLibrary.h create mode 100644 Source/ZMMOJobs/Public/ZMMOJobsModule.h create mode 100644 Source/ZMMOJobs/Public/ZMMOJobsSubsystem.h create mode 100644 Source/ZMMOJobs/ZMMOJobs.Build.cs create mode 100644 Source/ZMMOJobs/module.json diff --git a/Content/ZMMO/Data/Jobs/DA_Job_Novice.uasset b/Content/ZMMO/Data/Jobs/DA_Job_Novice.uasset new file mode 100644 index 0000000000000000000000000000000000000000..e6a0629d9c2c27ed60b863f85cf8c1f8f3802177 GIT binary patch literal 1422 zcma)6O-K|`9Dl8vr73CJ$quDpxws$cre+`P?(9mTtELi(rQ`U>$#rL&nKsu`AcfbU zkS-mfZb4B8p$@rdmmr}-hk`oQLQ4^ZRCsXv|7K@w`~v^*=Dq*td+-1I%}Y(y-T#x# zW{2_^D{?TlOf%%e`LPTA_b%@1E?T^G;**q#q#dc{e8zT=@5kuVFW&-pmWn@mANXH} zU5Ck5O~c(Lj&pZ^M@Oey5OvXgRvFgZf`5?a!HANOB~C$%mC%?*9g-KcC6KU6I77JSpn35<{>S7pb-7dU6_7dUUl*RpMOLldLtR)|8U1mrJ8DnX-)9 zSCcbo+_#rc=&PX%$MxB2GHwIJ`L8Q_1wwlyRgUZKprZ1=&fzOu(%IiL^{2{gKCl5i zVBzl#30yiD6*Z0N*C%e{U~AdY&Yo56BCB~%rnOC1qL^6(r1EPT*V$vQqmJwbawZA3 zN25Yyk?hV*Y=z)J(;%RMC01OqWuO@+gV^H$XU>}Y7MTk2hRikK-byU`!N0ziJ!inK ztXreTrd`_D;8|j1#yk$vHbxvGtA(qDoohq?QDQF*1C7A7*hrKE9I`Nmc1DPs37&)c zB%u!kSJALb;&XF% N{+m}vJf;!x{{U4~EIj}K literal 0 HcmV?d00001 diff --git a/Source/ZMMO/Game/UI/FrontEnd/UICharCard_Base.cpp b/Source/ZMMO/Game/UI/FrontEnd/UICharCard_Base.cpp index 8263248..853bd01 100644 --- a/Source/ZMMO/Game/UI/FrontEnd/UICharCard_Base.cpp +++ b/Source/ZMMO/Game/UI/FrontEnd/UICharCard_Base.cpp @@ -5,6 +5,7 @@ #include "Engine/World.h" #include "TimerManager.h" #include "UI/Widgets/UIButton_Base.h" +#include "ZMMOJobsLibrary.h" void UUICharCard_Base::NativeConstruct() { @@ -48,7 +49,7 @@ void UUICharCard_Base::SetFromSummary(const FZMMOCharSummary& InSummary) } if (Text_Class) { - Text_Class->SetText(FText::FromString(FString::Printf(TEXT("Classe %d"), Summary.ClassId))); + Text_Class->SetText(UZMMOJobsLibrary::GetJobDisplayName(this, Summary.ClassId)); } const ESlateVisibility ActionVis = bDeleteScheduled ? ESlateVisibility::Collapsed : ESlateVisibility::Visible; diff --git a/Source/ZMMO/ZMMO.Build.cs b/Source/ZMMO/ZMMO.Build.cs index 80ae3e4..f911655 100644 --- a/Source/ZMMO/ZMMO.Build.cs +++ b/Source/ZMMO/ZMMO.Build.cs @@ -19,7 +19,8 @@ public class ZMMO : ModuleRules "CommonInput", "GameplayTags", "ZeusNetwork", - "ZMMOAttributes" + "ZMMOAttributes", + "ZMMOJobs" }); PrivateDependencyModuleNames.AddRange(new string[] { diff --git a/Source/ZMMOJobs/Private/ZMMOJobDataAsset.cpp b/Source/ZMMOJobs/Private/ZMMOJobDataAsset.cpp new file mode 100644 index 0000000..2631308 --- /dev/null +++ b/Source/ZMMOJobs/Private/ZMMOJobDataAsset.cpp @@ -0,0 +1,5 @@ +#include "ZMMOJobDataAsset.h" + +// Implementacao vazia — todas as props sao UPROPERTY EditDefaultsOnly e +// vivem inline no .uasset. Override de GetPrimaryAssetId() esta no header +// (constexpr-friendly). diff --git a/Source/ZMMOJobs/Private/ZMMOJobsLibrary.cpp b/Source/ZMMOJobs/Private/ZMMOJobsLibrary.cpp new file mode 100644 index 0000000..0f4b861 --- /dev/null +++ b/Source/ZMMOJobs/Private/ZMMOJobsLibrary.cpp @@ -0,0 +1,54 @@ +#include "ZMMOJobsLibrary.h" + +#include "Engine/GameInstance.h" +#include "Engine/World.h" +#include "ZMMOJobDataAsset.h" +#include "ZMMOJobsSubsystem.h" + +namespace +{ +/// Resolve Subsystem via WorldContextObject. Retorna nullptr em contextos +/// invalidos (CDO em editor, world em destruicao). +UZMMOJobsSubsystem* ResolveSubsystem(const UObject* WorldContextObject) +{ + if (WorldContextObject == nullptr) { return nullptr; } + const UWorld* World = WorldContextObject->GetWorld(); + if (World == nullptr) { return nullptr; } + UGameInstance* GI = World->GetGameInstance(); + if (GI == nullptr) { return nullptr; } + return GI->GetSubsystem(); +} +} // namespace + +FText UZMMOJobsLibrary::GetJobDisplayName(const UObject* WorldContextObject, int32 ClassId) +{ + if (UZMMOJobsSubsystem* Sub = ResolveSubsystem(WorldContextObject)) + { + if (UZMMOJobDataAsset* Job = Sub->GetJobData(ClassId)) + { + return Job->DisplayName.IsEmpty() + ? FText::FromString(Job->TechnicalName) + : Job->DisplayName; + } + } + // Fallback: nao crasha UI se ClassId desconhecido. + return FText::FromString(FString::Printf(TEXT("Classe %d"), ClassId)); +} + +UZMMOJobDataAsset* UZMMOJobsLibrary::GetJobData(const UObject* WorldContextObject, int32 ClassId) +{ + if (UZMMOJobsSubsystem* Sub = ResolveSubsystem(WorldContextObject)) + { + return Sub->GetJobData(ClassId); + } + return nullptr; +} + +bool UZMMOJobsLibrary::IsJobRegistered(const UObject* WorldContextObject, int32 ClassId) +{ + if (UZMMOJobsSubsystem* Sub = ResolveSubsystem(WorldContextObject)) + { + return Sub->GetJobData(ClassId) != nullptr; + } + return false; +} diff --git a/Source/ZMMOJobs/Private/ZMMOJobsModule.cpp b/Source/ZMMOJobs/Private/ZMMOJobsModule.cpp new file mode 100644 index 0000000..4bfc787 --- /dev/null +++ b/Source/ZMMOJobs/Private/ZMMOJobsModule.cpp @@ -0,0 +1,13 @@ +#include "ZMMOJobsModule.h" + +#include "Modules/ModuleManager.h" + +void FZMMOJobsModule::StartupModule() +{ +} + +void FZMMOJobsModule::ShutdownModule() +{ +} + +IMPLEMENT_MODULE(FZMMOJobsModule, ZMMOJobs) diff --git a/Source/ZMMOJobs/Private/ZMMOJobsSubsystem.cpp b/Source/ZMMOJobs/Private/ZMMOJobsSubsystem.cpp new file mode 100644 index 0000000..bcdac0a --- /dev/null +++ b/Source/ZMMOJobs/Private/ZMMOJobsSubsystem.cpp @@ -0,0 +1,67 @@ +#include "ZMMOJobsSubsystem.h" + +#include "AssetRegistry/AssetRegistryModule.h" +#include "Modules/ModuleManager.h" +#include "ZMMOJobDataAsset.h" + +DEFINE_LOG_CATEGORY_STATIC(LogZMMOJobs, Log, All); + +void UZMMOJobsSubsystem::Initialize(FSubsystemCollectionBase& Collection) +{ + Super::Initialize(Collection); + LoadJobsFromAssetRegistry(); + UE_LOG(LogZMMOJobs, Log, TEXT("[ZMMOJobsSubsystem] Initialize: %d job(s) carregados"), CachedJobs.Num()); +} + +void UZMMOJobsSubsystem::Deinitialize() +{ + CachedJobs.Empty(); + Super::Deinitialize(); +} + +void UZMMOJobsSubsystem::LoadJobsFromAssetRegistry() +{ + FAssetRegistryModule& AssetRegistryModule = + FModuleManager::LoadModuleChecked("AssetRegistry"); + IAssetRegistry& AR = AssetRegistryModule.Get(); + + // Editor pode chamar antes de scan inicial. Standalone/PIE scan ja' rodou. + // SearchAllAssets(false) = nao-blocking; assume cache populado em runtime. +#if WITH_EDITOR + AR.SearchAllAssets(true); // bloqueia editor; ok em Initialize +#endif + + TArray Assets; + const FTopLevelAssetPath ClassPath(UZMMOJobDataAsset::StaticClass()->GetClassPathName()); + AR.GetAssetsByClass(ClassPath, Assets, /*bSearchSubClasses*/ true); + + for (const FAssetData& AssetData : Assets) + { + UZMMOJobDataAsset* Job = Cast(AssetData.GetAsset()); + if (Job == nullptr) { continue; } + + if (CachedJobs.Contains(Job->ClassId)) + { + UE_LOG(LogZMMOJobs, Warning, + TEXT("[ZMMOJobsSubsystem] Duplicate ClassId=%d (existing=%s, new=%s) — ignorando segundo"), + Job->ClassId, + *CachedJobs[Job->ClassId]->GetName(), + *Job->GetName()); + continue; + } + + CachedJobs.Add(Job->ClassId, Job); + UE_LOG(LogZMMOJobs, Verbose, + TEXT("[ZMMOJobsSubsystem] Cached classId=%d name=%s display=%s"), + Job->ClassId, *Job->TechnicalName, *Job->DisplayName.ToString()); + } +} + +UZMMOJobDataAsset* UZMMOJobsSubsystem::GetJobData(int32 ClassId) const +{ + if (const TObjectPtr* Found = CachedJobs.Find(ClassId)) + { + return Found->Get(); + } + return nullptr; +} diff --git a/Source/ZMMOJobs/Public/ZMMOJobDataAsset.h b/Source/ZMMOJobs/Public/ZMMOJobDataAsset.h new file mode 100644 index 0000000..dcb6c3a --- /dev/null +++ b/Source/ZMMOJobs/Public/ZMMOJobDataAsset.h @@ -0,0 +1,75 @@ +#pragma once + +#include "CoreMinimal.h" +#include "Engine/DataAsset.h" +#include "ZMMOJobDataAsset.generated.h" + +class UTexture2D; +class USoundBase; +class UAnimInstance; +class UAnimMontage; + +/** + * Dados visuais/UI de UM job (classe). 1 .uasset por job em + * `Content/ZMMO/Data/Jobs/DA_Job_.uasset`. + * + * ClassId eh a CHAVE — bate com `classId` do snapshot do server (rathena + * pc.hpp `job_class`, ex: Novice=0, Swordsman=1, Mage=2, ...). + * + * Soft pointers (TSoftObjectPtr) sao usados pra assets pesados (textures, + * sounds) — lazy-loaded quando precisar (level up, hover icon). DataAsset + * em si fica em memoria depois do scan do Subsystem. + * + * Async pointers (TSubclassOf) sao sync — anim BP precisa estar carregada + * quando o player spawna. + */ +UCLASS(BlueprintType) +class ZMMOJOBS_API UZMMOJobDataAsset : public UPrimaryDataAsset +{ + GENERATED_BODY() + +public: + /// Chave estavel (bate com `Config/Jobs/.json:Id` no server e com + /// `FZMMOAttributesSnapshot::ClassId` recebido do server). + UPROPERTY(EditDefaultsOnly, BlueprintReadOnly, Category = "Zeus|Job") + int32 ClassId = 0; + + /// Nome tecnico estavel (en-US). Ex: "Novice", "Swordsman". Aparece em + /// logs e telemetria — NUNCA exibido pro player. + UPROPERTY(EditDefaultsOnly, BlueprintReadOnly, Category = "Zeus|Job") + FString TechnicalName; + + /// Nome localizado exibido na UI (pt-BR no projeto base). Ex: "Aprendiz". + /// Pode usar FText Localization tables pra multi-idioma futuro. + UPROPERTY(EditDefaultsOnly, BlueprintReadOnly, Category = "Zeus|Job") + FText DisplayName; + + /// Icone do job — usado em StatusWindow, CharacterCard, JobChangeUI. + /// Soft ptr: nao carrega ate alguem chamar LoadSynchronous() ou async load. + UPROPERTY(EditDefaultsOnly, BlueprintReadOnly, Category = "Zeus|Job|Assets") + TSoftObjectPtr JobIcon; + + /// AnimInstance class override (opcional). Quando nao-null, AnimBP + /// padrao do PlayerCharacter eh substituido por esta classe no spawn + /// (jobs com move set diferente — Monge bate, Assassin furtivo, etc.). + UPROPERTY(EditDefaultsOnly, BlueprintReadOnly, Category = "Zeus|Job|Anim") + TSubclassOf DefaultAnimInstanceClass; + + /// Montage de "job change" (animacao de transformacao). Tocada quando + /// player promove de Novice → 1a classe. V1 nao implementado. + UPROPERTY(EditDefaultsOnly, BlueprintReadOnly, Category = "Zeus|Job|Anim") + TSoftObjectPtr JobChangeMontage; + + /// Som de level up override. Quando null, HUD usa som generico do projeto. + /// Soft ptr: lazy load no primeiro level up. + UPROPERTY(EditDefaultsOnly, BlueprintReadOnly, Category = "Zeus|Job|Audio") + TSoftObjectPtr LevelUpSoundCue; + + /// PrimaryAssetType pro AssetManager (precisa pra GetPrimaryAssetIdList). + /// Hardcoded "ZMMOJob" — registry classifica todos os DAs deste tipo. + virtual FPrimaryAssetId GetPrimaryAssetId() const override + { + return FPrimaryAssetId(FPrimaryAssetType(TEXT("ZMMOJob")), + *FString::Printf(TEXT("Job_%d_%s"), ClassId, *TechnicalName)); + } +}; diff --git a/Source/ZMMOJobs/Public/ZMMOJobsLibrary.h b/Source/ZMMOJobs/Public/ZMMOJobsLibrary.h new file mode 100644 index 0000000..825b348 --- /dev/null +++ b/Source/ZMMOJobs/Public/ZMMOJobsLibrary.h @@ -0,0 +1,39 @@ +#pragma once + +#include "CoreMinimal.h" +#include "Kismet/BlueprintFunctionLibrary.h" +#include "ZMMOJobsLibrary.generated.h" + +class UZMMOJobDataAsset; + +/** + * Helpers Blueprint pra UI/HUD acessar dados de job sem precisar pegar o + * Subsystem manualmente. Todos usam WorldContextObject pra resolver + * GameInstance → Subsystem internamente. + * + * Padrao de uso em UMG: + * - Text binding: `JobsLibrary::GetJobDisplayName(Self, AttrComp.Current.ClassId)` + * - Image binding: `JobsLibrary::GetJobIcon(Self, ClassId).LoadSynchronous()` + */ +UCLASS() +class ZMMOJOBS_API UZMMOJobsLibrary : public UBlueprintFunctionLibrary +{ + GENERATED_BODY() + +public: + /// Nome localizado do job. Fallback "Classe N" se ClassId nao tem DA + /// correspondente (job nao registrado no Content/ZMMO/Data/Jobs/). + UFUNCTION(BlueprintPure, Category = "Zeus|Jobs", + meta = (WorldContext = "WorldContextObject")) + static FText GetJobDisplayName(const UObject* WorldContextObject, int32 ClassId); + + /// DataAsset completo (pode ser nullptr — caller checa). + UFUNCTION(BlueprintPure, Category = "Zeus|Jobs", + meta = (WorldContext = "WorldContextObject")) + static UZMMOJobDataAsset* GetJobData(const UObject* WorldContextObject, int32 ClassId); + + /// Helper de UI: retorna true se ClassId tem DA registrado. + UFUNCTION(BlueprintPure, Category = "Zeus|Jobs", + meta = (WorldContext = "WorldContextObject")) + static bool IsJobRegistered(const UObject* WorldContextObject, int32 ClassId); +}; diff --git a/Source/ZMMOJobs/Public/ZMMOJobsModule.h b/Source/ZMMOJobs/Public/ZMMOJobsModule.h new file mode 100644 index 0000000..89817bb --- /dev/null +++ b/Source/ZMMOJobs/Public/ZMMOJobsModule.h @@ -0,0 +1,23 @@ +#pragma once + +#include "CoreMinimal.h" +#include "Modules/ModuleInterface.h" + +/** + * Sub-modulo cliente do JobsSystem. + * + * Espelho de `Server/ZeusServerEngine/Game/MMO/Modules/JobsSystem/`. Roda + * como modulo Runtime em LoadingPhase PreDefault para que o + * `UZMMOJobsSubsystem` esteja registrado antes do ZMMO core spawnar a HUD. + * + * Responsabilidades: + * - Dados visuais por job: DisplayName localizado, JobIcon, anim override + * - Lookup classId → UZMMOJobDataAsset via Subsystem + * - Library Blueprint pra HUD/UI consumir + */ +class FZMMOJobsModule : public IModuleInterface +{ +public: + virtual void StartupModule() override; + virtual void ShutdownModule() override; +}; diff --git a/Source/ZMMOJobs/Public/ZMMOJobsSubsystem.h b/Source/ZMMOJobs/Public/ZMMOJobsSubsystem.h new file mode 100644 index 0000000..7f8a103 --- /dev/null +++ b/Source/ZMMOJobs/Public/ZMMOJobsSubsystem.h @@ -0,0 +1,52 @@ +#pragma once + +#include "CoreMinimal.h" +#include "Subsystems/GameInstanceSubsystem.h" +#include "ZMMOJobsSubsystem.generated.h" + +class UZMMOJobDataAsset; + +/** + * Cache central de UZMMOJobDataAsset por ClassId. Vive como GameInstance + * Subsystem — startup automatico quando GameInstance inicializa (PIE, + * standalone, packaged). Sobrevive a map transitions. + * + * Initialize: + * 1. Carrega via AssetRegistry: scan de UZMMOJobDataAsset em todo Content + * 2. Cacheia em TMap por ClassId + * 3. Log: quantos jobs encontrou + * + * Uso: + * - C++: GameInstance->GetSubsystem()->GetJobData(0) + * - BP: UZMMOJobsLibrary::GetJobData(WorldContext, 0) + * + * Fase 7+ (Mod support): SCANEAR PrimaryAssetType="ZMMOJob" do AssetManager + * permite mods adicionarem DAs proprios sem mexer no source. + */ +UCLASS() +class ZMMOJOBS_API UZMMOJobsSubsystem : public UGameInstanceSubsystem +{ + GENERATED_BODY() + +public: + virtual void Initialize(FSubsystemCollectionBase& Collection) override; + virtual void Deinitialize() override; + + /// Lookup por classId. Retorna nullptr se ClassId nao tem DA correspondente + /// (caller usa Library com fallback FText). + UFUNCTION(BlueprintPure, Category = "Zeus|Jobs") + UZMMOJobDataAsset* GetJobData(int32 ClassId) const; + + /// Total de jobs cacheados (diagnostico). + UFUNCTION(BlueprintPure, Category = "Zeus|Jobs") + int32 GetJobCount() const { return CachedJobs.Num(); } + +private: + /// Scan AssetRegistry + popula CachedJobs. Sincronizo (V1) — chamado UMA + /// vez no Initialize. Pra 50 jobs eh aceitavel; se crescer pra 200+ pode + /// virar async no futuro. + void LoadJobsFromAssetRegistry(); + + UPROPERTY() + TMap> CachedJobs; +}; diff --git a/Source/ZMMOJobs/ZMMOJobs.Build.cs b/Source/ZMMOJobs/ZMMOJobs.Build.cs new file mode 100644 index 0000000..f5f200f --- /dev/null +++ b/Source/ZMMOJobs/ZMMOJobs.Build.cs @@ -0,0 +1,26 @@ +using UnrealBuildTool; + +public class ZMMOJobs : ModuleRules +{ + public ZMMOJobs(ReadOnlyTargetRules Target) : base(Target) + { + PCHUsage = PCHUsageMode.UseExplicitOrSharedPCHs; + + PublicDependencyModuleNames.AddRange(new string[] { + "Core", + "CoreUObject", + "Engine" + }); + + PrivateDependencyModuleNames.AddRange(new string[] { + "AssetRegistry" // FAssetRegistryModule pra scan de DA_Job_* + }); + + // Espelho do `Server/.../JobsSystem/module.json` (modulo nucleo MMO). + // Cliente roda em LoadingPhase PreDefault — Subsystem inicializa + // ANTES de o ZMMO core criar a player character / HUD. + // + // Sem dependencia em ZMMOAttributes: server envia classId no snapshot, + // cliente faz lookup local. Mantem o peer-module pattern. + } +} diff --git a/Source/ZMMOJobs/module.json b/Source/ZMMOJobs/module.json new file mode 100644 index 0000000..9afae77 --- /dev/null +++ b/Source/ZMMOJobs/module.json @@ -0,0 +1,18 @@ +{ + "name": "ZMMOJobs", + "gameType": "MMO", + "version": "0.1.0", + "side": "client", + "dependencies": { + "engine": ["Core", "CoreUObject", "Engine", "AssetRegistry"], + "modules": [] + }, + "publicHeaders": [ + "ZMMOJobsModule.h", + "ZMMOJobDataAsset.h", + "ZMMOJobsSubsystem.h", + "ZMMOJobsLibrary.h" + ], + "loadingPhase": "PreDefault", + "description": "Sub-modulo cliente do GameType MMO — dados visuais/UI por job (DisplayName localizado, ícone, anim override, som de level up). Espelha JobsSystem do server mas focado em apresentacao. NAO depende de ZMMOAttributes — server envia classId no snapshot, cliente faz lookup local. Pattern peer-module estabelecido no PR #9." +} diff --git a/ZMMO.uproject b/ZMMO.uproject index 7d4ee6e..a06e717 100644 --- a/ZMMO.uproject +++ b/ZMMO.uproject @@ -13,6 +13,11 @@ "Name": "ZMMOAttributes", "Type": "Runtime", "LoadingPhase": "PreDefault" + }, + { + "Name": "ZMMOJobs", + "Type": "Runtime", + "LoadingPhase": "PreDefault" } ], "Plugins": [ -- 2.54.0.windows.1