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/Data/Jobs/DA_Job_Novice.uasset b/Content/ZMMO/Data/Jobs/DA_Job_Novice.uasset new file mode 100644 index 0000000..e6a0629 Binary files /dev/null and b/Content/ZMMO/Data/Jobs/DA_Job_Novice.uasset differ diff --git a/Content/ZMMO/UI/HUD/WBP_HUD.uasset b/Content/ZMMO/UI/HUD/WBP_HUD.uasset new file mode 100644 index 0000000..2c72fab Binary files /dev/null and b/Content/ZMMO/UI/HUD/WBP_HUD.uasset differ diff --git a/Content/ZMMO/UI/HUD/WBP_HUD_HpSpBar.uasset b/Content/ZMMO/UI/HUD/WBP_HUD_HpSpBar.uasset new file mode 100644 index 0000000..eb05158 Binary files /dev/null and b/Content/ZMMO/UI/HUD/WBP_HUD_HpSpBar.uasset differ diff --git a/Content/ZMMO/UI/HUD/WBP_StatusWindow.uasset b/Content/ZMMO/UI/HUD/WBP_StatusWindow.uasset new file mode 100644 index 0000000..0311ba8 Binary files /dev/null and b/Content/ZMMO/UI/HUD/WBP_StatusWindow.uasset differ diff --git a/Content/ZMMO/UI/InGame/DA_InGameScreenSet.uasset b/Content/ZMMO/UI/InGame/DA_InGameScreenSet.uasset new file mode 100644 index 0000000..1c5275b Binary files /dev/null and b/Content/ZMMO/UI/InGame/DA_InGameScreenSet.uasset differ diff --git a/Source/ZMMO/Data/UI/InGameTypes.h b/Source/ZMMO/Data/UI/InGameTypes.h new file mode 100644 index 0000000..d5bc63c --- /dev/null +++ b/Source/ZMMO/Data/UI/InGameTypes.h @@ -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). +}; 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/Entity/ZMMOPlayerCharacter.cpp b/Source/ZMMO/Game/Entity/ZMMOPlayerCharacter.cpp index fcc9ac7..a4a2832 100644 --- a/Source/ZMMO/Game/Entity/ZMMOPlayerCharacter.cpp +++ b/Source/ZMMO/Game/Entity/ZMMOPlayerCharacter.cpp @@ -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()) + { + 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 4523c42..dc6d166 100644 --- a/Source/ZMMO/Game/Entity/ZMMOPlayerCharacter.h +++ b/Source/ZMMO/Game/Entity/ZMMOPlayerCharacter.h @@ -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()->GetZMMOComponent(). + protected: UPROPERTY(EditAnywhere, Category = "Input") UInputAction* JumpAction; 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/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/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..1e7c69e --- /dev/null +++ b/Source/ZMMO/Game/UI/InGame/ZMMOHudWidget.cpp @@ -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 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(); + 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); + } +} diff --git a/Source/ZMMO/Game/UI/InGame/ZMMOHudWidget.h b/Source/ZMMO/Game/UI/InGame/ZMMOHudWidget.h new file mode 100644 index 0000000..39f3553 --- /dev/null +++ b/Source/ZMMO/Game/UI/InGame/ZMMOHudWidget.h @@ -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(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; + + /// 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 BoundComponent; + + void ShowLevelUpToast(int32 NewBaseLevel); + void HideLevelUpToast(); + + FTimerHandle LevelUpToastHideTimer; +}; diff --git a/Source/ZMMO/Game/UI/InGame/ZMMOStatusWindowWidget.cpp b/Source/ZMMO/Game/UI/InGame/ZMMOStatusWindowWidget.cpp new file mode 100644 index 0000000..0278c7e --- /dev/null +++ b/Source/ZMMO/Game/UI/InGame/ZMMOStatusWindowWidget.cpp @@ -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 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 — 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()) + { + 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/ZMMO/ZMMO.Build.cs b/Source/ZMMO/ZMMO.Build.cs index 1550ba0..f911655 100644 --- a/Source/ZMMO/ZMMO.Build.cs +++ b/Source/ZMMO/ZMMO.Build.cs @@ -18,7 +18,9 @@ public class ZMMO : ModuleRules "CommonUI", "CommonInput", "GameplayTags", - "ZeusNetwork" + "ZeusNetwork", + "ZMMOAttributes", + "ZMMOJobs" }); PrivateDependencyModuleNames.AddRange(new string[] { diff --git a/Source/ZMMOAttributes/Private/ZMMOAttributeComponent.cpp b/Source/ZMMOAttributes/Private/ZMMOAttributeComponent.cpp new file mode 100644 index 0000000..d2aa07d --- /dev/null +++ b/Source/ZMMOAttributes/Private/ZMMOAttributeComponent.cpp @@ -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(); + if (!Net) { return; } + Net->SendStatAlloc(StatId, Amount); +} diff --git a/Source/ZMMOAttributes/Private/ZMMOAttributeNetworkHandler.cpp b/Source/ZMMOAttributes/Private/ZMMOAttributeNetworkHandler.cpp new file mode 100644 index 0000000..8512a5f --- /dev/null +++ b/Source/ZMMOAttributes/Private/ZMMOAttributeNetworkHandler.cpp @@ -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(); + 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); + 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(); +} + +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(); + 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(); +} 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..543ea80 --- /dev/null +++ b/Source/ZMMOAttributes/Private/ZMMOHudHpSpWidget.cpp @@ -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(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) +{ + ApplyHpSp(Snapshot.Hp, Snapshot.MaxHp, Snapshot.Sp, Snapshot.MaxSp); + if (LevelText) + { + LevelText->SetText(FText::FromString(FString::Printf(TEXT("Lv %d"), Snapshot.BaseLevel))); + } +} diff --git a/Source/ZMMOAttributes/Public/ZMMOAttributeComponent.h b/Source/ZMMOAttributes/Public/ZMMOAttributeComponent.h new file mode 100644 index 0000000..22522ed --- /dev/null +++ b/Source/ZMMOAttributes/Public/ZMMOAttributeComponent.h @@ -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(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; + + /** 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 new file mode 100644 index 0000000..0f47348 --- /dev/null +++ b/Source/ZMMOAttributes/Public/ZMMOAttributeNetworkHandler.h @@ -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; +}; diff --git a/Source/ZMMOAttributes/Public/ZMMOAttributeTypes.h b/Source/ZMMOAttributes/Public/ZMMOAttributeTypes.h new file mode 100644 index 0000000..3d893e8 --- /dev/null +++ b/Source/ZMMOAttributes/Public/ZMMOAttributeTypes.h @@ -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; +}; 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..2713625 --- /dev/null +++ b/Source/ZMMOAttributes/Public/ZMMOHudHpSpWidget.h @@ -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; +}; diff --git a/Source/ZMMOAttributes/ZMMOAttributes.Build.cs b/Source/ZMMOAttributes/ZMMOAttributes.Build.cs new file mode 100644 index 0000000..85eba18 --- /dev/null +++ b/Source/ZMMOAttributes/ZMMOAttributes.Build.cs @@ -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 + } +} diff --git a/Source/ZMMOAttributes/module.json b/Source/ZMMOAttributes/module.json new file mode 100644 index 0000000..73f678f --- /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", "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." +} 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 b4a7881..a06e717 100644 --- a/ZMMO.uproject +++ b/ZMMO.uproject @@ -8,6 +8,16 @@ "Name": "ZMMO", "Type": "Runtime", "LoadingPhase": "Default" + }, + { + "Name": "ZMMOAttributes", + "Type": "Runtime", + "LoadingPhase": "PreDefault" + }, + { + "Name": "ZMMOJobs", + "Type": "Runtime", + "LoadingPhase": "PreDefault" } ], "Plugins": [