Merge pull request 'feat(client): AttributeSystem (Fase 1-3) + ZMMOJobs + UI in-game' (#3) from feat/attribute-system-mmo into main

Reviewed-on: #3
This commit was merged in pull request #3.
This commit is contained in:
2026-05-23 21:06:47 -03:00
50 changed files with 2418 additions and 14 deletions

View File

@@ -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/<Module>.<ClassName> (sem o "U" do prefixo
; C++; UClass resolve pelo nome curto).
; -----------------------------------------------------------------------------
[/Script/ZMMO.ZMMOPlayerState]
+ComponentClasses=/Script/ZMMOAttributes.ZMMOAttributeComponent

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

View File

@@ -0,0 +1,34 @@
#pragma once
#include "CoreMinimal.h"
#include "InGameTypes.generated.h"
/**
* Estados da UI in-game. Dirigidos pelo UUIInGameFlowSubsystem; cada estado
* resolve uma tela (UCommonActivatableWidget) via DA_InGameScreenSet.
*
* Espelha simetricamente EZMMOFrontEndState (Data/UI/FrontEndTypes.h):
* - Front-end usa estados pra Login/ServerSelect/Lobby (UI.Layer.Menu)
* - In-game usa estados pra HUD/Status/Inventory/Skills (UI.Layer.Game + GameMenu)
*
* Convencao de camadas (resolvida pelo subsystem):
* - Playing -> 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).
};

View File

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

View File

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

View File

@@ -13,9 +13,11 @@
#include "GameFramework/SpringArmComponent.h"
#include "InputAction.h"
#include "InputActionValue.h"
#include "GameFramework/PlayerState.h"
#include "Subsystems/WorldSubsystem.h"
#include "UObject/ConstructorHelpers.h"
#include "ZMMO.h"
#include "ZMMOAttributeComponent.h"
#include "ZMMOWorldSubsystem.h"
#include "ZeusNetworkSubsystem.h"
#include "UI/FrontEnd/UIFrontEndFlowSubsystem.h"
@@ -52,6 +54,9 @@ AZMMOPlayerCharacter::AZMMOPlayerCharacter()
FollowCamera->SetupAttachment(CameraBoom, USpringArmComponent::SocketName);
FollowCamera->bUsePawnControlRotation = false;
// 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
// AZMMOGameMode sem exigir um BP filho. Um BP filho continua opcional para
@@ -343,6 +348,24 @@ void AZMMOPlayerCharacter::HandleLocalSpawnReady(const int32 InEntityId, const F
WorldSubsystem->RegisterLocalEntity(EntityIdAsInt64, this);
}
}
// 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())
{
if (UZMMOAttributeComponent* AttrComp = PS->FindComponentByClass<UZMMOAttributeComponent>())
{
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)

View File

@@ -47,6 +47,9 @@ class ZMMO_API AZMMOPlayerCharacter : public ACharacter, public IZMMOEntityInter
UPROPERTY(VisibleAnywhere, BlueprintReadOnly, Category = "Components", meta = (AllowPrivateAccess = "true"))
UCameraComponent* FollowCamera;
// AttributeComponent vive no PlayerState (AZMMOPlayerState + Component Registry).
// Acesso: GetPlayerState<AZMMOPlayerState>()->GetZMMOComponent<UZMMOAttributeComponent>().
protected:
UPROPERTY(EditAnywhere, Category = "Input")
UInputAction* JumpAction;

View File

@@ -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();
}

View File

@@ -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<UUIInGameFlowSubsystem>())
{
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);
}

View File

@@ -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;
};

View File

@@ -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<FString> ClassPaths;
GConfig->GetArray(TEXT("/Script/ZMMO.ZMMOPlayerState"),
TEXT("ComponentClasses"),
ClassPaths,
GGameIni);
for (const FString& Path : ClassPaths)
{
if (UClass* C = LoadClass<UActorComponent>(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<UActorComponent>& 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<UActorComponent>(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;
}

View File

@@ -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<UZMMOAttributeComponent>()`
* ou o helper `GetZMMOComponent<T>()`.
*/
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<UZMMOAttributeComponent>()
template <typename T>
T* GetZMMOComponent() const
{
return Cast<T>(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<TSubclassOf<UActorComponent>> ComponentClasses;
};

View File

@@ -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;

View File

@@ -41,18 +41,7 @@ void UUIFrontEndFlowSubsystem::Deinitialize()
void UUIFrontEndFlowSubsystem::StartFrontEnd()
{
UUIFrontEndScreenSet* SS = GetScreenSet();
if (UUIManagerSubsystem* Mgr = GetUIManager())
{
TSubclassOf<UUIPrimaryGameLayout_Base> 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<UUIPrimaryGameLayout_Base> LayoutClass = SS->RootLayoutClass.LoadSynchronous();
Mgr->CreateAndAddRootLayout(LayoutClass);
UE_LOG(LogZMMO, Log, TEXT("FrontEndFlow::EnsureRootLayout: RootLayout recriado"));
}
void UUIFrontEndFlowSubsystem::SetState(EZMMOFrontEndState NewState)
{
if (NewState == CurrentState)

View File

@@ -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);

View File

@@ -0,0 +1,210 @@
#include "UIInGameFlowSubsystem.h"
#include "CommonActivatableWidget.h" // Necessario pra TSoftClassPtr<UCommonActivatableWidget>::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<UUIFrontEndFlowSubsystem>())
{
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<UUIManagerSubsystem>();
}
}
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<UCommonActivatableWidget> 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"));
}));
}

View File

@@ -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<UUIInGameScreenSet> 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<UUIInGameScreenSet> ScreenSet;
};

View File

@@ -0,0 +1,7 @@
#include "UIInGameScreenSet.h"
TSoftClassPtr<UCommonActivatableWidget> UUIInGameScreenSet::GetScreenForState(EZMMOInGameUIState State) const
{
const TSoftClassPtr<UCommonActivatableWidget>* Found = StateScreens.Find(State);
return Found ? *Found : TSoftClassPtr<UCommonActivatableWidget>();
}

View File

@@ -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<EZMMOInGameUIState, TSoftClassPtr<UCommonActivatableWidget>> StateScreens;
/** Soft class da tela do estado (vazio se nao configurado). */
UFUNCTION(BlueprintCallable, Category = "InGame")
TSoftClassPtr<UCommonActivatableWidget> GetScreenForState(EZMMOInGameUIState State) const;
};

View File

@@ -0,0 +1,158 @@
#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"
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<FUIInputConfig> 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);
// Toast comeca escondido — so' aparece em HandleLevelUp.
HideLevelUpToast();
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<UZMMOAttributeComponent>();
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 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);
}
}

View File

@@ -0,0 +1,126 @@
#pragma once
#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;
/**
* 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<FUIInputConfig>(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<FUIInputConfig> 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;
/// 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;
// 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<UZMMOAttributeComponent> BoundComponent;
void ShowLevelUpToast(int32 NewBaseLevel);
void HideLevelUpToast();
FTimerHandle LevelUpToastHideTimer;
};

View File

@@ -0,0 +1,168 @@
#include "ZMMOStatusWindowWidget.h"
#include "CommonInputTypeEnum.h"
#include "Components/Button.h"
#include "Components/TextBlock.h"
#include "Engine/World.h"
#include "GameFramework/PlayerController.h"
#include "GameFramework/PlayerState.h"
#include "Internationalization/Text.h"
#include "ZMMO.h"
#include "ZMMOAttributeComponent.h"
#include "UI/InGame/UIInGameFlowSubsystem.h"
#include "UI/InGameTypes.h"
UZMMOStatusWindowWidget::UZMMOStatusWindowWidget(const FObjectInitializer& ObjectInitializer)
: Super(ObjectInitializer)
{
bIsBackHandler = true; // Esc/B fecha
bAutoActivate = true;
}
TOptional<FUIInputConfig> UZMMOStatusWindowWidget::GetDesiredInputConfig() const
{
// Modo Menu: cursor visivel, input flui pra UI (clicks nos botões),
// movimento bloqueado. Padrao pra menus modais.
return FUIInputConfig(ECommonInputMode::Menu, EMouseCaptureMode::NoCapture);
}
void UZMMOStatusWindowWidget::NativeOnActivated()
{
Super::NativeOnActivated();
BindToLocalPlayer();
if (PlusBtn_Str) PlusBtn_Str->OnClicked.AddDynamic(this, &UZMMOStatusWindowWidget::OnPlusStr);
if (PlusBtn_Agi) PlusBtn_Agi->OnClicked.AddDynamic(this, &UZMMOStatusWindowWidget::OnPlusAgi);
if (PlusBtn_Vit) PlusBtn_Vit->OnClicked.AddDynamic(this, &UZMMOStatusWindowWidget::OnPlusVit);
if (PlusBtn_Int) PlusBtn_Int->OnClicked.AddDynamic(this, &UZMMOStatusWindowWidget::OnPlusInt);
if (PlusBtn_Dex) PlusBtn_Dex->OnClicked.AddDynamic(this, &UZMMOStatusWindowWidget::OnPlusDex);
if (PlusBtn_Luk) PlusBtn_Luk->OnClicked.AddDynamic(this, &UZMMOStatusWindowWidget::OnPlusLuk);
if (CloseBtn) CloseBtn->OnClicked.AddDynamic(this, &UZMMOStatusWindowWidget::OnCloseClicked);
UE_LOG(LogZMMO, Log, TEXT("ZMMOStatusWindow activated"));
}
void UZMMOStatusWindowWidget::NativeOnDeactivated()
{
UnbindFromComponent();
if (PlusBtn_Str) PlusBtn_Str->OnClicked.RemoveDynamic(this, &UZMMOStatusWindowWidget::OnPlusStr);
if (PlusBtn_Agi) PlusBtn_Agi->OnClicked.RemoveDynamic(this, &UZMMOStatusWindowWidget::OnPlusAgi);
if (PlusBtn_Vit) PlusBtn_Vit->OnClicked.RemoveDynamic(this, &UZMMOStatusWindowWidget::OnPlusVit);
if (PlusBtn_Int) PlusBtn_Int->OnClicked.RemoveDynamic(this, &UZMMOStatusWindowWidget::OnPlusInt);
if (PlusBtn_Dex) PlusBtn_Dex->OnClicked.RemoveDynamic(this, &UZMMOStatusWindowWidget::OnPlusDex);
if (PlusBtn_Luk) PlusBtn_Luk->OnClicked.RemoveDynamic(this, &UZMMOStatusWindowWidget::OnPlusLuk);
if (CloseBtn) CloseBtn->OnClicked.RemoveDynamic(this, &UZMMOStatusWindowWidget::OnCloseClicked);
Super::NativeOnDeactivated();
}
void UZMMOStatusWindowWidget::BindToLocalPlayer()
{
UWorld* World = GetWorld();
if (!World) { return; }
APlayerController* PC = World->GetFirstPlayerController();
if (!PC || !PC->PlayerState) { return; }
UZMMOAttributeComponent* Comp = PC->PlayerState->FindComponentByClass<UZMMOAttributeComponent>();
if (!Comp || Comp == BoundComponent.Get()) { return; }
UnbindFromComponent();
BoundComponent = Comp;
Comp->OnAttributesChanged.AddDynamic(this, &UZMMOStatusWindowWidget::HandleAttributesChanged);
Comp->OnStatAllocReply.AddDynamic(this, &UZMMOStatusWindowWidget::HandleStatAllocReply);
RefreshFromSnapshot(Comp->GetSnapshot());
}
void UZMMOStatusWindowWidget::UnbindFromComponent()
{
if (UZMMOAttributeComponent* Old = BoundComponent.Get())
{
Old->OnAttributesChanged.RemoveDynamic(this, &UZMMOStatusWindowWidget::HandleAttributesChanged);
Old->OnStatAllocReply.RemoveDynamic(this, &UZMMOStatusWindowWidget::HandleStatAllocReply);
}
BoundComponent.Reset();
}
void UZMMOStatusWindowWidget::HandleAttributesChanged(const FZMMOAttributesSnapshot& Snapshot)
{
RefreshFromSnapshot(Snapshot);
}
void UZMMOStatusWindowWidget::HandleStatAllocReply(bool bAccepted, int32 Reason)
{
UE_LOG(LogZMMO, Log, TEXT("StatusWindow: alloc reply accepted=%d reason=%d"),
bAccepted ? 1 : 0, Reason);
// Snapshot subsequente vai disparar HandleAttributesChanged e atualizar UI.
// Aqui poderia popup de erro visual quando bAccepted=false (TODO polish).
}
void UZMMOStatusWindowWidget::RefreshFromSnapshot(const FZMMOAttributesSnapshot& S)
{
if (StrText) StrText->SetText(FText::FromString(FString::Printf(TEXT("STR %d"), S.Str)));
if (AgiText) AgiText->SetText(FText::FromString(FString::Printf(TEXT("AGI %d"), S.Agi)));
if (VitText) VitText->SetText(FText::FromString(FString::Printf(TEXT("VIT %d"), S.Vit)));
if (IntText) IntText->SetText(FText::FromString(FString::Printf(TEXT("INT %d"), S.Int)));
if (DexText) DexText->SetText(FText::FromString(FString::Printf(TEXT("DEX %d"), S.Dex)));
if (LukText) LukText->SetText(FText::FromString(FString::Printf(TEXT("LUK %d"), S.Luk)));
if (StatusPointText)
{
StatusPointText->SetText(FText::FromString(
FString::Printf(TEXT("Status Points: %d"), S.StatusPoint)));
}
// Derivados — 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)
{
// 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;
if (PlusBtn_Str) PlusBtn_Str->SetIsEnabled(bCanAlloc);
if (PlusBtn_Agi) PlusBtn_Agi->SetIsEnabled(bCanAlloc);
if (PlusBtn_Vit) PlusBtn_Vit->SetIsEnabled(bCanAlloc);
if (PlusBtn_Int) PlusBtn_Int->SetIsEnabled(bCanAlloc);
if (PlusBtn_Dex) PlusBtn_Dex->SetIsEnabled(bCanAlloc);
if (PlusBtn_Luk) PlusBtn_Luk->SetIsEnabled(bCanAlloc);
}
void UZMMOStatusWindowWidget::RequestAlloc(int32 StatId)
{
if (UZMMOAttributeComponent* Comp = BoundComponent.Get())
{
Comp->RequestStatAlloc(StatId, /*Amount*/ 1);
}
}
void UZMMOStatusWindowWidget::OnPlusStr() { RequestAlloc(0); }
void UZMMOStatusWindowWidget::OnPlusAgi() { RequestAlloc(1); }
void UZMMOStatusWindowWidget::OnPlusVit() { RequestAlloc(2); }
void UZMMOStatusWindowWidget::OnPlusInt() { RequestAlloc(3); }
void UZMMOStatusWindowWidget::OnPlusDex() { RequestAlloc(4); }
void UZMMOStatusWindowWidget::OnPlusLuk() { RequestAlloc(5); }
void UZMMOStatusWindowWidget::OnCloseClicked()
{
if (UGameInstance* GI = GetGameInstance())
{
if (UUIInGameFlowSubsystem* Flow = GI->GetSubsystem<UUIInGameFlowSubsystem>())
{
Flow->SetState(EZMMOInGameUIState::Playing);
}
}
}

View File

@@ -0,0 +1,106 @@
#pragma once
#include "CommonActivatableWidget.h"
#include "CoreMinimal.h"
#include "ZMMOAttributeTypes.h"
#include "ZMMOStatusWindowWidget.generated.h"
class UButton;
class UTextBlock;
class UCommonButtonBase;
class UZMMOAttributeComponent;
/**
* Tela "Status" — atributos do jogador + alocacao via botões `+`.
*
* Modo Menu: cursor visível, movimento bloqueado, Esc fecha. Push em
* UI.Layer.GameMenu pelo `UUIInGameFlowSubsystem::ToggleScreen(StatusWindow)`.
* HUD continua visível em UI.Layer.Game por baixo.
*
* Composicao do WBP_StatusWindow (BindWidgetOptional pra permitir variantes):
* - Labels de stats primarios: StrText / AgiText / VitText / IntText / DexText / LukText
* - Botões +: PlusBtn_Str / PlusBtn_Agi / PlusBtn_Vit / PlusBtn_Int / PlusBtn_Dex / PlusBtn_Luk
* - Derivados (server-authoritative): AtkText / MatkText / DefText / MdefText /
* HitText / FleeText / CritText / AspdText
* - StatusPointText (pontos disponíveis pra alocar)
* - CloseBtn (fecha — fallback do Esc/back handler)
*
* Workflow:
* 1. NativeOnActivated: busca AttributeComponent (PlayerState do player local)
* 2. Subscreve OnAttributesChanged + OnStatAllocReply
* 3. Refresh inicial via snapshot atual
* 4. Clique no `+X` chama `Comp->RequestStatAlloc(X, 1)` → server valida
* 5. Server responde com S_ATTRIBUTE_STAT_ALLOC_OK + (se ok) snapshot novo
* 6. UI atualiza via delegate
*/
UCLASS(Abstract, Blueprintable, BlueprintType)
class ZMMO_API UZMMOStatusWindowWidget : public UCommonActivatableWidget
{
GENERATED_BODY()
public:
UZMMOStatusWindowWidget(const FObjectInitializer& ObjectInitializer);
protected:
virtual void NativeOnActivated() override;
virtual void NativeOnDeactivated() override;
/// Modo Menu: cursor visível + bloqueia input de gameplay (movimento).
virtual TOptional<FUIInputConfig> GetDesiredInputConfig() const override;
/// Texto dos stats (label + valor). BindWidgetOptional pra permitir
/// WBPs custom (ex.: sem display de SP em variantes).
UPROPERTY(BlueprintReadOnly, meta = (BindWidgetOptional), Category = "Zeus|Status") UTextBlock* StrText = nullptr;
UPROPERTY(BlueprintReadOnly, meta = (BindWidgetOptional), Category = "Zeus|Status") UTextBlock* AgiText = nullptr;
UPROPERTY(BlueprintReadOnly, meta = (BindWidgetOptional), Category = "Zeus|Status") UTextBlock* VitText = nullptr;
UPROPERTY(BlueprintReadOnly, meta = (BindWidgetOptional), Category = "Zeus|Status") UTextBlock* IntText = nullptr;
UPROPERTY(BlueprintReadOnly, meta = (BindWidgetOptional), Category = "Zeus|Status") UTextBlock* DexText = nullptr;
UPROPERTY(BlueprintReadOnly, meta = (BindWidgetOptional), Category = "Zeus|Status") UTextBlock* LukText = nullptr;
/// Pontos restantes pra alocar + label "Status Points: N".
UPROPERTY(BlueprintReadOnly, meta = (BindWidgetOptional), Category = "Zeus|Status") UTextBlock* StatusPointText = nullptr;
/// Derivados (sempre calculados pelo servidor — cliente NUNCA recalcula).
/// Mostra `base + bonus` quando aplicavel (camadas equip/buff vem do server
/// pre-somadas no snapshot). BindWidgetOptional pra suportar HUDs minimalistas.
UPROPERTY(BlueprintReadOnly, meta = (BindWidgetOptional), Category = "Zeus|Status|Derived") UTextBlock* AtkText = nullptr;
UPROPERTY(BlueprintReadOnly, meta = (BindWidgetOptional), Category = "Zeus|Status|Derived") UTextBlock* MatkText = nullptr;
UPROPERTY(BlueprintReadOnly, meta = (BindWidgetOptional), Category = "Zeus|Status|Derived") UTextBlock* DefText = nullptr;
UPROPERTY(BlueprintReadOnly, meta = (BindWidgetOptional), Category = "Zeus|Status|Derived") UTextBlock* MdefText = nullptr;
UPROPERTY(BlueprintReadOnly, meta = (BindWidgetOptional), Category = "Zeus|Status|Derived") UTextBlock* HitText = nullptr;
UPROPERTY(BlueprintReadOnly, meta = (BindWidgetOptional), Category = "Zeus|Status|Derived") UTextBlock* FleeText = nullptr;
UPROPERTY(BlueprintReadOnly, meta = (BindWidgetOptional), Category = "Zeus|Status|Derived") UTextBlock* CritText = nullptr;
UPROPERTY(BlueprintReadOnly, meta = (BindWidgetOptional), Category = "Zeus|Status|Derived") UTextBlock* AspdText = nullptr;
/// Botoes + por stat.
UPROPERTY(BlueprintReadOnly, meta = (BindWidgetOptional), Category = "Zeus|Status") UButton* PlusBtn_Str = nullptr;
UPROPERTY(BlueprintReadOnly, meta = (BindWidgetOptional), Category = "Zeus|Status") UButton* PlusBtn_Agi = nullptr;
UPROPERTY(BlueprintReadOnly, meta = (BindWidgetOptional), Category = "Zeus|Status") UButton* PlusBtn_Vit = nullptr;
UPROPERTY(BlueprintReadOnly, meta = (BindWidgetOptional), Category = "Zeus|Status") UButton* PlusBtn_Int = nullptr;
UPROPERTY(BlueprintReadOnly, meta = (BindWidgetOptional), Category = "Zeus|Status") UButton* PlusBtn_Dex = nullptr;
UPROPERTY(BlueprintReadOnly, meta = (BindWidgetOptional), Category = "Zeus|Status") UButton* PlusBtn_Luk = nullptr;
/// Botao Close (fallback do Esc/back).
UPROPERTY(BlueprintReadOnly, meta = (BindWidgetOptional), Category = "Zeus|Status") UButton* CloseBtn = nullptr;
private:
void BindToLocalPlayer();
void UnbindFromComponent();
void RefreshFromSnapshot(const FZMMOAttributesSnapshot& Snap);
UFUNCTION() void HandleAttributesChanged(const FZMMOAttributesSnapshot& Snapshot);
UFUNCTION() void HandleStatAllocReply(bool bAccepted, int32 Reason);
UFUNCTION() void OnPlusStr();
UFUNCTION() void OnPlusAgi();
UFUNCTION() void OnPlusVit();
UFUNCTION() void OnPlusInt();
UFUNCTION() void OnPlusDex();
UFUNCTION() void OnPlusLuk();
UFUNCTION() void OnCloseClicked();
void RequestAlloc(int32 StatId);
UPROPERTY(Transient)
TWeakObjectPtr<UZMMOAttributeComponent> BoundComponent;
};

View File

@@ -18,7 +18,9 @@ public class ZMMO : ModuleRules
"CommonUI",
"CommonInput",
"GameplayTags",
"ZeusNetwork"
"ZeusNetwork",
"ZMMOAttributes",
"ZMMOJobs"
});
PrivateDependencyModuleNames.AddRange(new string[] {

View File

@@ -0,0 +1,55 @@
#include "ZMMOAttributeComponent.h"
#include "Engine/GameInstance.h"
#include "ZeusNetworkSubsystem.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);
}
void UZMMOAttributeComponent::NotifyStatAllocReply(bool bAccepted, int32 Reason)
{
OnStatAllocReply.Broadcast(bAccepted, Reason);
}
void UZMMOAttributeComponent::RequestStatAlloc(int32 StatId, int32 Amount)
{
UWorld* World = GetWorld();
if (!World) { return; }
UGameInstance* GI = World->GetGameInstance();
if (!GI) { return; }
UZeusNetworkSubsystem* Net = GI->GetSubsystem<UZeusNetworkSubsystem>();
if (!Net) { return; }
Net->SendStatAlloc(StatId, Amount);
}

View File

@@ -0,0 +1,215 @@
#include "ZMMOAttributeNetworkHandler.h"
#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"
#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.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;
}
// 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).
//
// 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; }
const AGameStateBase* GS = World->GetGameState();
if (!GS) { return nullptr; }
for (APlayerState* PS : GS->PlayerArray)
{
if (!PS) { continue; }
UZMMOAttributeComponent* Comp = PS->FindComponentByClass<UZMMOAttributeComponent>();
if (!Comp) { continue; }
if (Comp->GetSnapshot().EntityId == EntityId)
{
return Comp;
}
}
return nullptr;
}
}
bool UZMMOAttributeNetworkHandler::ShouldCreateSubsystem(UObject* Outer) const
{
// Cria apenas para mundos de gameplay (PIE/Game), nao para editor preview.
UWorld* World = Cast<UWorld>(Outer);
if (!World) { return false; }
return World->WorldType == EWorldType::Game || World->WorldType == EWorldType::PIE;
}
void UZMMOAttributeNetworkHandler::Initialize(FSubsystemCollectionBase& Collection)
{
Super::Initialize(Collection);
if (UZeusNetworkSubsystem* Net = GetZeusNetSubsystem())
{
SnapshotFullHandle = Net->OnAttributeSnapshotFull.AddUObject(
this, &UZMMOAttributeNetworkHandler::HandleAttributeSnapshotFull);
HpSpUpdateHandle = Net->OnHpSpUpdate.AddUObject(
this, &UZMMOAttributeNetworkHandler::HandleHpSpUpdate);
LevelUpHandle = Net->OnLevelUp.AddUObject(
this, &UZMMOAttributeNetworkHandler::HandleLevelUp);
StatAllocReplyHandle = Net->OnStatAllocReply.AddUObject(
this, &UZMMOAttributeNetworkHandler::HandleStatAllocReply);
}
}
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();
}
if (StatAllocReplyHandle.IsValid())
{
Net->OnStatAllocReply.Remove(StatAllocReplyHandle);
StatAllocReplyHandle.Reset();
}
}
Super::Deinitialize();
}
UZeusNetworkSubsystem* UZMMOAttributeNetworkHandler::GetZeusNetSubsystem() const
{
const UWorld* World = GetWorld();
if (!World) { return nullptr; }
UGameInstance* GI = World->GetGameInstance();
if (!GI) { return nullptr; }
return GI->GetSubsystem<UZeusNetworkSubsystem>();
}
void UZMMOAttributeNetworkHandler::HandleAttributeSnapshotFull(const FZeusAttributesPayload& Payload)
{
UWorld* World = GetWorld();
if (!World) { return; }
UZMMOAttributeComponent* Comp = FindComponentForEntity(World, Payload.EntityId);
if (!Comp)
{
// 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)
{
for (APlayerState* PS : GS->PlayerArray)
{
if (!PS) { continue; }
UZMMOAttributeComponent* Candidate = PS->FindComponentByClass<UZMMOAttributeComponent>();
if (Candidate && Candidate->GetSnapshot().EntityId == 0)
{
Comp = Candidate;
break;
}
}
}
}
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);
}
}
void UZMMOAttributeNetworkHandler::HandleStatAllocReply(bool bAccepted, int32 Reason)
{
// S_ATTRIBUTE_STAT_ALLOC_OK nao traz EntityId no payload — sempre do
// player local que fez o request. Busca o AttributeComponent via PC.
if (UZMMOAttributeComponent* Comp = FindLocalPlayerAttributeComponent())
{
Comp->NotifyStatAllocReply(bAccepted, Reason);
}
}
UZMMOAttributeComponent* UZMMOAttributeNetworkHandler::FindLocalPlayerAttributeComponent() const
{
UWorld* World = GetWorld();
if (!World) { return nullptr; }
APlayerController* PC = World->GetFirstPlayerController();
if (!PC || !PC->PlayerState) { return nullptr; }
return PC->PlayerState->FindComponentByClass<UZMMOAttributeComponent>();
}

View File

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

View File

@@ -0,0 +1,45 @@
#include "ZMMOHudHpSpWidget.h"
#include "Components/ProgressBar.h"
#include "Components/TextBlock.h"
#include "Internationalization/Text.h"
void UZMMOHudHpSpWidget::ApplySnapshot(const FZMMOAttributesSnapshot& Snapshot)
{
LastSnapshot = Snapshot;
OnSnapshotApplied(Snapshot);
}
void UZMMOHudHpSpWidget::ApplyHpSp(int32 Hp, int32 MaxHp, int32 Sp, int32 MaxSp)
{
LastSnapshot.Hp = Hp;
LastSnapshot.MaxHp = MaxHp;
LastSnapshot.Sp = Sp;
LastSnapshot.MaxSp = MaxSp;
if (HpBar)
{
HpBar->SetPercent(MaxHp > 0 ? static_cast<float>(Hp) / static_cast<float>(MaxHp) : 0.f);
}
if (SpBar)
{
SpBar->SetPercent(MaxSp > 0 ? static_cast<float>(Sp) / static_cast<float>(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)
{
ApplyHpSp(Snapshot.Hp, Snapshot.MaxHp, Snapshot.Sp, Snapshot.MaxSp);
if (LevelText)
{
LevelText->SetText(FText::FromString(FString::Printf(TEXT("Lv %d"), Snapshot.BaseLevel)));
}
}

View File

@@ -0,0 +1,105 @@
#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);
/// 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).
*
* 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);
/// 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
/// por EntityId desde o inicio.
UFUNCTION(BlueprintCallable, Category = "Zeus|Attributes")
void SeedEntityId(int32 InEntityId) { Current.EntityId = InEntityId; }
UFUNCTION(BlueprintPure, Category = "Zeus|Attributes")
const FZMMOAttributesSnapshot& GetSnapshot() const { return Current; }
UFUNCTION(BlueprintPure, Category = "Zeus|Attributes")
float GetHpRatio() const
{
return Current.MaxHp > 0 ? static_cast<float>(Current.Hp) / static_cast<float>(Current.MaxHp) : 0.f;
}
UFUNCTION(BlueprintPure, Category = "Zeus|Attributes")
float GetSpRatio() const
{
return Current.MaxSp > 0 ? static_cast<float>(Current.Sp) / static_cast<float>(Current.MaxSp) : 0.f;
}
// === Delegates Blueprint ===
/** Snapshot novo aplicado. Use isto na UI para refresh geral. */
UPROPERTY(BlueprintAssignable, Category = "Zeus|Attributes")
FZMMOOnAttributesChanged OnAttributesChanged;
/** HP ou SP mudou (efêmero ou via snapshot). Para barras animadas. */
UPROPERTY(BlueprintAssignable, Category = "Zeus|Attributes")
FZMMOOnHpSpChanged OnHpSpChanged;
/** Subiu de nível. Para efeitos visuais ("LEVEL UP!" toast). */
UPROPERTY(BlueprintAssignable, Category = "Zeus|Attributes")
FZMMOOnLevelUp OnLevelUp;
/** Resposta ao C_STAT_ALLOC (aceito ou rejeitado). UI reage. */
UPROPERTY(BlueprintAssignable, Category = "Zeus|Attributes")
FZMMOOnStatAllocReply OnStatAllocReply;
protected:
UPROPERTY(BlueprintReadOnly, Category = "Zeus|Attributes")
FZMMOAttributesSnapshot Current;
};

View File

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

View File

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

View File

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

View File

@@ -0,0 +1,76 @@
#pragma once
#include "Blueprint/UserWidget.h"
#include "CoreMinimal.h"
#include "ZMMOAttributeTypes.h"
#include "ZMMOHudHpSpWidget.generated.h"
class UProgressBar;
class UTextBlock;
/**
* 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.
*
* 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)
*
* 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
{
GENERATED_BODY()
public:
/** Atualiza todos os campos a partir do snapshot. Chamado pelo UZMMOHudWidget. */
UFUNCTION(BlueprintCallable, Category = "Zeus|HUD")
void ApplySnapshot(const FZMMOAttributesSnapshot& Snapshot);
/** 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:
/**
* Hook BlueprintNativeEvent para skin visual reagir a um snapshot novo
* (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);
// === 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:
/// Ultimo snapshot recebido (cache pra ApplyHpSp manter MaxHp/MaxSp). */
UPROPERTY(Transient)
FZMMOAttributesSnapshot LastSnapshot;
};

View File

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

View File

@@ -0,0 +1,19 @@
{
"name": "ZMMOAttributes",
"gameType": "MMO",
"version": "0.1.0",
"side": "client",
"dependencies": {
"engine": ["Core", "CoreUObject", "Engine", "UMG", "Slate", "CommonUI", "CommonInput"],
"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."
}

View File

@@ -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).

View File

@@ -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<UZMMOJobsSubsystem>();
}
} // 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;
}

View File

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

View File

@@ -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<FAssetRegistryModule>("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<FAssetData> Assets;
const FTopLevelAssetPath ClassPath(UZMMOJobDataAsset::StaticClass()->GetClassPathName());
AR.GetAssetsByClass(ClassPath, Assets, /*bSearchSubClasses*/ true);
for (const FAssetData& AssetData : Assets)
{
UZMMOJobDataAsset* Job = Cast<UZMMOJobDataAsset>(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<UZMMOJobDataAsset>* Found = CachedJobs.Find(ClassId))
{
return Found->Get();
}
return nullptr;
}

View File

@@ -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_<Nome>.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/<Name>.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<UTexture2D> 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<UAnimInstance> 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<UAnimMontage> 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<USoundBase> 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));
}
};

View File

@@ -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);
};

View File

@@ -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;
};

View File

@@ -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<int32, UZMMOJobDataAsset*> por ClassId
* 3. Log: quantos jobs encontrou
*
* Uso:
* - C++: GameInstance->GetSubsystem<UZMMOJobsSubsystem>()->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<int32, TObjectPtr<UZMMOJobDataAsset>> CachedJobs;
};

View File

@@ -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.
}
}

View File

@@ -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."
}

View File

@@ -8,6 +8,16 @@
"Name": "ZMMO",
"Type": "Runtime",
"LoadingPhase": "Default"
},
{
"Name": "ZMMOAttributes",
"Type": "Runtime",
"LoadingPhase": "PreDefault"
},
{
"Name": "ZMMOJobs",
"Type": "Runtime",
"LoadingPhase": "PreDefault"
}
],
"Plugins": [