diff --git a/Content/ZMMO/Data/UI/DT_UI_Styles.uasset b/Content/ZMMO/Data/UI/DT_UI_Styles.uasset index 2406533..2a65db1 100644 Binary files a/Content/ZMMO/Data/UI/DT_UI_Styles.uasset and b/Content/ZMMO/Data/UI/DT_UI_Styles.uasset differ diff --git a/Content/ZMMO/UI/HUD/WBP_StatusWindow.uasset b/Content/ZMMO/UI/HUD/WBP_StatusWindow.uasset index 0311ba8..53f54bc 100644 Binary files a/Content/ZMMO/UI/HUD/WBP_StatusWindow.uasset and b/Content/ZMMO/UI/HUD/WBP_StatusWindow.uasset differ diff --git a/Source/ZMMO/Data/UI/InGameTypes.h b/Source/ZMMO/Data/UI/InGameTypes.h index d5bc63c..a48125b 100644 --- a/Source/ZMMO/Data/UI/InGameTypes.h +++ b/Source/ZMMO/Data/UI/InGameTypes.h @@ -27,6 +27,7 @@ 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). + JobChangePanel, ///< Painel de promocao de classe (Jobs.1). Toggle tecla J. Inventory, ///< (futuro) Bag/equipamentos. SkillTree, ///< (futuro) Arvore de skills. EscapeMenu, ///< (futuro) Pausa, settings, logout. diff --git a/Source/ZMMO/Game/Controller/ZMMOPlayerController.cpp b/Source/ZMMO/Game/Controller/ZMMOPlayerController.cpp index 801f186..0433b8a 100644 --- a/Source/ZMMO/Game/Controller/ZMMOPlayerController.cpp +++ b/Source/ZMMO/Game/Controller/ZMMOPlayerController.cpp @@ -104,6 +104,14 @@ void AZMMOPlayerController::SetupInputComponent() this, &AZMMOPlayerController::ToggleStatusWindow); Binding.bConsumeInput = true; Binding.bExecuteWhenPaused = true; // dispara mesmo com StatusWindow aberto (CommonUI menu mode) + + // Hotkey J -> toggle JobChangePanel (Jobs.1). + FInputKeyBinding& JobBinding = InputComponent->BindKey( + FInputChord(EKeys::J, /*bShift*/ false, /*bCtrl*/ false, /*bAlt*/ false, /*bCmd*/ false), + IE_Pressed, + this, &AZMMOPlayerController::ToggleJobChangePanel); + JobBinding.bConsumeInput = true; + JobBinding.bExecuteWhenPaused = true; } } @@ -118,6 +126,17 @@ void AZMMOPlayerController::ToggleStatusWindow() } } +void AZMMOPlayerController::ToggleJobChangePanel() +{ + if (UGameInstance* GI = GetGameInstance()) + { + if (UUIInGameFlowSubsystem* Flow = GI->GetSubsystem()) + { + Flow->ToggleScreen(EZMMOInGameUIState::JobChangePanel); + } + } +} + bool AZMMOPlayerController::ShouldUseTouchControls() const { return SVirtualJoystick::ShouldDisplayTouchInterface() || bForceTouchControls; diff --git a/Source/ZMMO/Game/Controller/ZMMOPlayerController.h b/Source/ZMMO/Game/Controller/ZMMOPlayerController.h index 1f2c057..689db8d 100644 --- a/Source/ZMMO/Game/Controller/ZMMOPlayerController.h +++ b/Source/ZMMO/Game/Controller/ZMMOPlayerController.h @@ -50,4 +50,7 @@ protected: /** Hotkey Alt+A → toggle StatusWindow via UUIInGameFlowSubsystem. */ void ToggleStatusWindow(); + + /** Hotkey J → toggle JobChangePanel (Jobs.1 — promocao de classe). */ + void ToggleJobChangePanel(); }; diff --git a/Source/ZMMOJobs/Private/ZMMOJobChangeNetworkHandler.cpp b/Source/ZMMOJobs/Private/ZMMOJobChangeNetworkHandler.cpp new file mode 100644 index 0000000..ab3a458 --- /dev/null +++ b/Source/ZMMOJobs/Private/ZMMOJobChangeNetworkHandler.cpp @@ -0,0 +1,67 @@ +#include "ZMMOJobChangeNetworkHandler.h" + +#include "Engine/GameInstance.h" +#include "Engine/World.h" +#include "ZeusNetworkSubsystem.h" + +DEFINE_LOG_CATEGORY_STATIC(LogZMMOJobChange, Log, All); + +bool UZMMOJobChangeNetworkHandler::ShouldCreateSubsystem(UObject* Outer) const +{ + // Mesmo criterio do AttributeNetworkHandler — so' em mundos de gameplay + // (PIE / standalone in-game). Editor preview / front-end menu nao precisa. + const UWorld* World = Cast(Outer); + if (World == nullptr) { return false; } + return World->WorldType == EWorldType::Game + || World->WorldType == EWorldType::PIE; +} + +void UZMMOJobChangeNetworkHandler::Initialize(FSubsystemCollectionBase& Collection) +{ + Super::Initialize(Collection); + + if (UZeusNetworkSubsystem* Net = GetZeusNetSubsystem()) + { + JobChangeResultHandle = Net->OnJobChangeResult.AddUObject( + this, &UZMMOJobChangeNetworkHandler::HandleJobChangeResult); + UE_LOG(LogZMMOJobChange, Log, + TEXT("[ZMMOJobChangeNetworkHandler] Bind OnJobChangeResult OK")); + } + else + { + UE_LOG(LogZMMOJobChange, Warning, + TEXT("[ZMMOJobChangeNetworkHandler] ZeusNetworkSubsystem indisponivel — bind pulado")); + } +} + +void UZMMOJobChangeNetworkHandler::Deinitialize() +{ + if (UZeusNetworkSubsystem* Net = GetZeusNetSubsystem()) + { + if (JobChangeResultHandle.IsValid()) + { + Net->OnJobChangeResult.Remove(JobChangeResultHandle); + JobChangeResultHandle.Reset(); + } + } + OnJobChangeResult.Clear(); + Super::Deinitialize(); +} + +void UZMMOJobChangeNetworkHandler::HandleJobChangeResult( + bool bAccepted, int32 Reason, int32 NewClassId) +{ + UE_LOG(LogZMMOJobChange, Log, + TEXT("[ZMMOJobChangeNetworkHandler] S_JOB_CHANGE_RESULT accepted=%d reason=%d newClassId=%d"), + bAccepted ? 1 : 0, Reason, NewClassId); + OnJobChangeResult.Broadcast(bAccepted, Reason, NewClassId); +} + +UZeusNetworkSubsystem* UZMMOJobChangeNetworkHandler::GetZeusNetSubsystem() const +{ + const UWorld* World = GetWorld(); + if (World == nullptr) { return nullptr; } + UGameInstance* GI = World->GetGameInstance(); + if (GI == nullptr) { return nullptr; } + return GI->GetSubsystem(); +} diff --git a/Source/ZMMOJobs/Private/ZMMOJobsLibrary.cpp b/Source/ZMMOJobs/Private/ZMMOJobsLibrary.cpp index 0f4b861..82c67d5 100644 --- a/Source/ZMMOJobs/Private/ZMMOJobsLibrary.cpp +++ b/Source/ZMMOJobs/Private/ZMMOJobsLibrary.cpp @@ -4,6 +4,7 @@ #include "Engine/World.h" #include "ZMMOJobDataAsset.h" #include "ZMMOJobsSubsystem.h" +#include "ZeusNetworkSubsystem.h" namespace { @@ -52,3 +53,37 @@ bool UZMMOJobsLibrary::IsJobRegistered(const UObject* WorldContextObject, int32 } return false; } + +TArray UZMMOJobsLibrary::GetJobsByTier( + const UObject* WorldContextObject, EZMMOJobTier Tier) +{ + if (UZMMOJobsSubsystem* Sub = ResolveSubsystem(WorldContextObject)) + { + return Sub->GetJobsByTier(Tier); + } + return {}; +} + +TArray UZMMOJobsLibrary::GetEligibleNextJobs( + const UObject* WorldContextObject, int32 CurrentClassId) +{ + if (UZMMOJobsSubsystem* Sub = ResolveSubsystem(WorldContextObject)) + { + return Sub->GetEligibleNextJobs(CurrentClassId); + } + return {}; +} + +void UZMMOJobsLibrary::SendJobChangeRequest( + const UObject* WorldContextObject, int32 TargetClassId) +{ + if (WorldContextObject == nullptr) { return; } + const UWorld* World = WorldContextObject->GetWorld(); + if (World == nullptr) { return; } + UGameInstance* GI = World->GetGameInstance(); + if (GI == nullptr) { return; } + if (UZeusNetworkSubsystem* Net = GI->GetSubsystem()) + { + Net->SendJobChangeRequest(TargetClassId); + } +} diff --git a/Source/ZMMOJobs/Private/ZMMOJobsSubsystem.cpp b/Source/ZMMOJobs/Private/ZMMOJobsSubsystem.cpp index bcdac0a..e2e782b 100644 --- a/Source/ZMMOJobs/Private/ZMMOJobsSubsystem.cpp +++ b/Source/ZMMOJobs/Private/ZMMOJobsSubsystem.cpp @@ -65,3 +65,32 @@ UZMMOJobDataAsset* UZMMOJobsSubsystem::GetJobData(int32 ClassId) const } return nullptr; } + +TArray UZMMOJobsSubsystem::GetJobsByTier(EZMMOJobTier Tier) const +{ + TArray Out; + Out.Reserve(CachedJobs.Num()); + for (const TPair>& Pair : CachedJobs) + { + if (Pair.Value && Pair.Value->Tier == Tier) + { + Out.Add(Pair.Value.Get()); + } + } + return Out; +} + +TArray UZMMOJobsSubsystem::GetEligibleNextJobs(int32 CurrentClassId) const +{ + TArray Out; + Out.Reserve(4); // tipico: 2-6 destinos + for (const TPair>& Pair : CachedJobs) + { + if (Pair.Value && Pair.Value->ParentClassId == CurrentClassId + && Pair.Value->ClassId != CurrentClassId) + { + Out.Add(Pair.Value.Get()); + } + } + return Out; +} diff --git a/Source/ZMMOJobs/Public/ZMMOJobChangeNetworkHandler.h b/Source/ZMMOJobs/Public/ZMMOJobChangeNetworkHandler.h new file mode 100644 index 0000000..b2b9ea0 --- /dev/null +++ b/Source/ZMMOJobs/Public/ZMMOJobChangeNetworkHandler.h @@ -0,0 +1,54 @@ +#pragma once + +#include "CoreMinimal.h" +#include "Subsystems/WorldSubsystem.h" +#include "ZMMOJobChangeNetworkHandler.generated.h" + +class UZeusNetworkSubsystem; + +/** + * Multicast dinamico assignable em Blueprint. WBP de promocao binda no + * OnJobChangeResult pra reagir ao S_JOB_CHANGE_RESULT (1520) do server. + * + * Parametros: + * - bAccepted : true = promocao aplicada (S_ATTRIBUTE_SNAPSHOT_FULL + * chega logo apos com state novo). + * - Reason : EJobChangeRejectReason (None/NoSession/UnknownJob/ + * NotParent/WrongTier/JobLevelTooLow/AlreadyAtTarget/ + * JobsDbUnavailable). Significativo apenas se !bAccepted. + * - NewClassId : significativo apenas se bAccepted=true (redundante com + * snapshot que vem junto, mas util pra triggers/toasts). + */ +DECLARE_DYNAMIC_MULTICAST_DELEGATE_ThreeParams(FZMMOOnJobChangeResultBP, + bool, bAccepted, int32, Reason, int32, NewClassId); + +/** + * Bridge entre o `UZeusNetworkSubsystem` (do plugin ZeusNetwork) e a UI + * Blueprint (WBP de promocao). Mesmo pattern do `UZMMOAttributeNetworkHandler` + * — converte o delegate C++-only `FZeusOnJobChangeResult` num dynamic + * multicast (`FZMMOOnJobChangeResultBP`) assignavel em BP. + * + * Por que UWorldSubsystem? Player so' faz promocao quando ja' esta no + * WorldServer (com mundo carregado). No front-end / lobby, esse subsystem + * nao existe — economiza memoria. + */ +UCLASS() +class ZMMOJOBS_API UZMMOJobChangeNetworkHandler : public UWorldSubsystem +{ + GENERATED_BODY() + +public: + virtual void Initialize(FSubsystemCollectionBase& Collection) override; + virtual void Deinitialize() override; + virtual bool ShouldCreateSubsystem(UObject* Outer) const override; + + /// Assignavel em BP: WBP_JobChangePanel.OnEvent_OnJobChangeResult. + UPROPERTY(BlueprintAssignable, Category = "Zeus|Jobs") + FZMMOOnJobChangeResultBP OnJobChangeResult; + +private: + void HandleJobChangeResult(bool bAccepted, int32 Reason, int32 NewClassId); + UZeusNetworkSubsystem* GetZeusNetSubsystem() const; + + FDelegateHandle JobChangeResultHandle; +}; diff --git a/Source/ZMMOJobs/Public/ZMMOJobsLibrary.h b/Source/ZMMOJobs/Public/ZMMOJobsLibrary.h index 825b348..03cf59d 100644 --- a/Source/ZMMOJobs/Public/ZMMOJobsLibrary.h +++ b/Source/ZMMOJobs/Public/ZMMOJobsLibrary.h @@ -2,6 +2,7 @@ #include "CoreMinimal.h" #include "Kismet/BlueprintFunctionLibrary.h" +#include "ZMMOJobTypes.h" #include "ZMMOJobsLibrary.generated.h" class UZMMOJobDataAsset; @@ -36,4 +37,26 @@ public: UFUNCTION(BlueprintPure, Category = "Zeus|Jobs", meta = (WorldContext = "WorldContextObject")) static bool IsJobRegistered(const UObject* WorldContextObject, int32 ClassId); + + /// Lista jobs num tier (UI dropdown). Vazio se subsystem indisponivel. + UFUNCTION(BlueprintPure, Category = "Zeus|Jobs", + meta = (WorldContext = "WorldContextObject")) + static TArray GetJobsByTier( + const UObject* WorldContextObject, EZMMOJobTier Tier); + + /// Destinos de promocao a partir do char atual. WBP de promocao consome + /// direto. Vazio se nao houver filhos (ex: char ja' esta em Tier 3 + /// Mastery — Jobs.4 introduzira terminal de progressao). + UFUNCTION(BlueprintPure, Category = "Zeus|Jobs", + meta = (WorldContext = "WorldContextObject")) + static TArray GetEligibleNextJobs( + const UObject* WorldContextObject, int32 CurrentClassId); + + /// Helper de send via ZeusNetworkSubsystem. Conveniencia pra WBP nao + /// precisar pegar o subsystem manualmente. Idempotente: rejeitado + /// silenciosamente se nao conectado. + UFUNCTION(BlueprintCallable, Category = "Zeus|Jobs", + meta = (WorldContext = "WorldContextObject")) + static void SendJobChangeRequest( + const UObject* WorldContextObject, int32 TargetClassId); }; diff --git a/Source/ZMMOJobs/Public/ZMMOJobsSubsystem.h b/Source/ZMMOJobs/Public/ZMMOJobsSubsystem.h index 7f8a103..c1c0ee6 100644 --- a/Source/ZMMOJobs/Public/ZMMOJobsSubsystem.h +++ b/Source/ZMMOJobs/Public/ZMMOJobsSubsystem.h @@ -2,6 +2,7 @@ #include "CoreMinimal.h" #include "Subsystems/GameInstanceSubsystem.h" +#include "ZMMOJobTypes.h" #include "ZMMOJobsSubsystem.generated.h" class UZMMOJobDataAsset; @@ -41,6 +42,22 @@ public: UFUNCTION(BlueprintPure, Category = "Zeus|Jobs") int32 GetJobCount() const { return CachedJobs.Num(); } + /// Lista jobs num tier especifico (UI agrupa por tier — ex: dropdown + /// "Vocacoes possiveis"). Ordem do retorno e' a do AssetRegistry scan + /// — UI pode reordenar por ClassId/DisplayName se precisar. + UFUNCTION(BlueprintPure, Category = "Zeus|Jobs") + TArray GetJobsByTier(EZMMOJobTier Tier) const; + + /// Destinos de promocao validos a partir do `CurrentClassId`. Filtra + /// pelos jobs que tem `ParentClassId == CurrentClassId`. WBP de + /// promocao consome direto pra montar botoes "Tornar-se X". + /// + /// NOTA: condicao de elegibilidade real (jobLevel >= cap, quest, item) + /// e' do server — UI apenas LISTA opcoes possiveis pelo schema. Server + /// reponde rejeicao via S_JOB_CHANGE_RESULT (Jobs::EJobChangeRejectReason). + UFUNCTION(BlueprintPure, Category = "Zeus|Jobs") + TArray GetEligibleNextJobs(int32 CurrentClassId) const; + private: /// Scan AssetRegistry + popula CachedJobs. Sincronizo (V1) — chamado UMA /// vez no Initialize. Pra 50 jobs eh aceitavel; se crescer pra 200+ pode diff --git a/Source/ZMMOJobs/ZMMOJobs.Build.cs b/Source/ZMMOJobs/ZMMOJobs.Build.cs index f5f200f..00a0924 100644 --- a/Source/ZMMOJobs/ZMMOJobs.Build.cs +++ b/Source/ZMMOJobs/ZMMOJobs.Build.cs @@ -13,7 +13,8 @@ public class ZMMOJobs : ModuleRules }); PrivateDependencyModuleNames.AddRange(new string[] { - "AssetRegistry" // FAssetRegistryModule pra scan de DA_Job_* + "AssetRegistry", // FAssetRegistryModule pra scan de DA_Job_* + "ZeusNetwork" // UZeusNetworkSubsystem (SendJobChangeRequest) }); // Espelho do `Server/.../JobsSystem/module.json` (modulo nucleo MMO).