feat(jobs): novo módulo ZMMOJobs (peer de ZMMOAttributes) + DA_Job_Novice

Sub-módulo cliente do JobsSystem do server. Pavimenta dados visuais/UI por
job (DisplayName localizado, ícone, anim override, som de level up) sem
acoplar ao ZMMOAttributes.

ARQUITETURA — espelha o peer-module pattern do server (PR #9):
  Server: Game/MMO/Modules/JobsSystem (módulo núcleo, LoadOrder=50)
  Client: Source/ZMMOJobs/ (LoadingPhase=PreDefault, peer de ZMMOAttributes)

ZMMOAttributes NÃO depende de ZMMOJobs — server envia classId no snapshot,
cliente faz lookup local via Library/Subsystem.

NOVOS ARQUIVOS

C++ module (Source/ZMMOJobs/):
- module.json + ZMMOJobs.Build.cs (deps: Core/Engine/AssetRegistry)
- ZMMOJobsModule.h/.cpp                 — IMPLEMENT_MODULE
- ZMMOJobDataAsset.h/.cpp               — UPrimaryDataAsset (BlueprintType)
                                          campos: ClassId, TechnicalName,
                                          DisplayName, JobIcon (soft),
                                          DefaultAnimInstanceClass,
                                          JobChangeMontage (soft),
                                          LevelUpSoundCue (soft)
- ZMMOJobsSubsystem.h/.cpp              — UGameInstanceSubsystem
                                          scan AssetRegistry no Initialize +
                                          cache TMap<int32, DA*> por ClassId
                                          O(1) GetJobData(ClassId)
- ZMMOJobsLibrary.h/.cpp                — UBlueprintFunctionLibrary
                                          GetJobDisplayName/GetJobData/IsJobRegistered
                                          fallback "Classe N" se ClassId desconhecido

Content:
- Content/ZMMO/Data/Jobs/DA_Job_Novice.uasset
  (ClassId=0, TechnicalName="Novice", DisplayName="Aprendiz")

MODIFICADOS

- ZMMO.uproject: + Module "ZMMOJobs" (LoadingPhase PreDefault)
- Source/ZMMO/ZMMO.Build.cs: + "ZMMOJobs" em PublicDependencyModuleNames
- Source/ZMMO/Game/UI/FrontEnd/UICharCard_Base.cpp:51 — primeiro consumer
  do Library: "Classe %d" hardcoded → UZMMOJobsLibrary::GetJobDisplayName(this, ClassId)
  Card da seleção de personagem agora mostra "Aprendiz" via DA lookup.

BLUEPRINT ACCESS

Toda a API exposta pra BP via UFUNCTION(BlueprintPure):
- UZMMOJobsLibrary::GetJobDisplayName(Self, ClassId) → FText
- UZMMOJobsLibrary::GetJobData(Self, ClassId) → UZMMOJobDataAsset*
- UZMMOJobsSubsystem::GetJobData(ClassId)
- UZMMOJobDataAsset propriedades BlueprintReadOnly (UMG binding direto)

LOOKUP

AssetRegistry varre /Game/ por UZMMOJobDataAsset no boot do GameInstance.
Chave = campo ClassId do asset (não nome do arquivo — renomear .uasset OK).
Lookup O(1) via TMap. Custo de scan amortizado em 1 vez por sessão (~50ms
pra 50 jobs futuro).

VALIDAÇÃO

Editor:
  [Log] DA class found: <class 'ZMMOJobDataAsset'>
  [Log] Subsystem class found: <class 'ZMMOJobsSubsystem'>
  [Log] Library class found: <class 'ZMMOJobsLibrary'>
  [Log] AssetRegistry encontrou 1 ZMMOJobDataAsset(s):
    - DA_Job_Novice @ /Game/ZMMO/Data/Jobs/DA_Job_Novice

Próximo módulo cliente: 1 linha no .uproject + .Build.cs + ZMMOJobs.Build.cs
não muda. Pattern estabelecido.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
2026-05-23 19:19:41 -03:00
parent dbc2898a4b
commit bacf987e0b
14 changed files with 381 additions and 2 deletions

Binary file not shown.

View File

@@ -5,6 +5,7 @@
#include "Engine/World.h"
#include "TimerManager.h"
#include "UI/Widgets/UIButton_Base.h"
#include "ZMMOJobsLibrary.h"
void UUICharCard_Base::NativeConstruct()
{
@@ -48,7 +49,7 @@ void UUICharCard_Base::SetFromSummary(const FZMMOCharSummary& InSummary)
}
if (Text_Class)
{
Text_Class->SetText(FText::FromString(FString::Printf(TEXT("Classe %d"), Summary.ClassId)));
Text_Class->SetText(UZMMOJobsLibrary::GetJobDisplayName(this, Summary.ClassId));
}
const ESlateVisibility ActionVis = bDeleteScheduled ? ESlateVisibility::Collapsed : ESlateVisibility::Visible;

View File

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

View File

@@ -0,0 +1,5 @@
#include "ZMMOJobDataAsset.h"
// Implementacao vazia — todas as props sao UPROPERTY EditDefaultsOnly e
// vivem inline no .uasset. Override de GetPrimaryAssetId() esta no header
// (constexpr-friendly).

View File

@@ -0,0 +1,54 @@
#include "ZMMOJobsLibrary.h"
#include "Engine/GameInstance.h"
#include "Engine/World.h"
#include "ZMMOJobDataAsset.h"
#include "ZMMOJobsSubsystem.h"
namespace
{
/// Resolve Subsystem via WorldContextObject. Retorna nullptr em contextos
/// invalidos (CDO em editor, world em destruicao).
UZMMOJobsSubsystem* ResolveSubsystem(const UObject* WorldContextObject)
{
if (WorldContextObject == nullptr) { return nullptr; }
const UWorld* World = WorldContextObject->GetWorld();
if (World == nullptr) { return nullptr; }
UGameInstance* GI = World->GetGameInstance();
if (GI == nullptr) { return nullptr; }
return GI->GetSubsystem<UZMMOJobsSubsystem>();
}
} // namespace
FText UZMMOJobsLibrary::GetJobDisplayName(const UObject* WorldContextObject, int32 ClassId)
{
if (UZMMOJobsSubsystem* Sub = ResolveSubsystem(WorldContextObject))
{
if (UZMMOJobDataAsset* Job = Sub->GetJobData(ClassId))
{
return Job->DisplayName.IsEmpty()
? FText::FromString(Job->TechnicalName)
: Job->DisplayName;
}
}
// Fallback: nao crasha UI se ClassId desconhecido.
return FText::FromString(FString::Printf(TEXT("Classe %d"), ClassId));
}
UZMMOJobDataAsset* UZMMOJobsLibrary::GetJobData(const UObject* WorldContextObject, int32 ClassId)
{
if (UZMMOJobsSubsystem* Sub = ResolveSubsystem(WorldContextObject))
{
return Sub->GetJobData(ClassId);
}
return nullptr;
}
bool UZMMOJobsLibrary::IsJobRegistered(const UObject* WorldContextObject, int32 ClassId)
{
if (UZMMOJobsSubsystem* Sub = ResolveSubsystem(WorldContextObject))
{
return Sub->GetJobData(ClassId) != nullptr;
}
return false;
}

View File

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

View File

@@ -0,0 +1,67 @@
#include "ZMMOJobsSubsystem.h"
#include "AssetRegistry/AssetRegistryModule.h"
#include "Modules/ModuleManager.h"
#include "ZMMOJobDataAsset.h"
DEFINE_LOG_CATEGORY_STATIC(LogZMMOJobs, Log, All);
void UZMMOJobsSubsystem::Initialize(FSubsystemCollectionBase& Collection)
{
Super::Initialize(Collection);
LoadJobsFromAssetRegistry();
UE_LOG(LogZMMOJobs, Log, TEXT("[ZMMOJobsSubsystem] Initialize: %d job(s) carregados"), CachedJobs.Num());
}
void UZMMOJobsSubsystem::Deinitialize()
{
CachedJobs.Empty();
Super::Deinitialize();
}
void UZMMOJobsSubsystem::LoadJobsFromAssetRegistry()
{
FAssetRegistryModule& AssetRegistryModule =
FModuleManager::LoadModuleChecked<FAssetRegistryModule>("AssetRegistry");
IAssetRegistry& AR = AssetRegistryModule.Get();
// Editor pode chamar antes de scan inicial. Standalone/PIE scan ja' rodou.
// SearchAllAssets(false) = nao-blocking; assume cache populado em runtime.
#if WITH_EDITOR
AR.SearchAllAssets(true); // bloqueia editor; ok em Initialize
#endif
TArray<FAssetData> Assets;
const FTopLevelAssetPath ClassPath(UZMMOJobDataAsset::StaticClass()->GetClassPathName());
AR.GetAssetsByClass(ClassPath, Assets, /*bSearchSubClasses*/ true);
for (const FAssetData& AssetData : Assets)
{
UZMMOJobDataAsset* Job = Cast<UZMMOJobDataAsset>(AssetData.GetAsset());
if (Job == nullptr) { continue; }
if (CachedJobs.Contains(Job->ClassId))
{
UE_LOG(LogZMMOJobs, Warning,
TEXT("[ZMMOJobsSubsystem] Duplicate ClassId=%d (existing=%s, new=%s) — ignorando segundo"),
Job->ClassId,
*CachedJobs[Job->ClassId]->GetName(),
*Job->GetName());
continue;
}
CachedJobs.Add(Job->ClassId, Job);
UE_LOG(LogZMMOJobs, Verbose,
TEXT("[ZMMOJobsSubsystem] Cached classId=%d name=%s display=%s"),
Job->ClassId, *Job->TechnicalName, *Job->DisplayName.ToString());
}
}
UZMMOJobDataAsset* UZMMOJobsSubsystem::GetJobData(int32 ClassId) const
{
if (const TObjectPtr<UZMMOJobDataAsset>* Found = CachedJobs.Find(ClassId))
{
return Found->Get();
}
return nullptr;
}

View File

@@ -0,0 +1,75 @@
#pragma once
#include "CoreMinimal.h"
#include "Engine/DataAsset.h"
#include "ZMMOJobDataAsset.generated.h"
class UTexture2D;
class USoundBase;
class UAnimInstance;
class UAnimMontage;
/**
* Dados visuais/UI de UM job (classe). 1 .uasset por job em
* `Content/ZMMO/Data/Jobs/DA_Job_<Nome>.uasset`.
*
* ClassId eh a CHAVE — bate com `classId` do snapshot do server (rathena
* pc.hpp `job_class`, ex: Novice=0, Swordsman=1, Mage=2, ...).
*
* Soft pointers (TSoftObjectPtr) sao usados pra assets pesados (textures,
* sounds) — lazy-loaded quando precisar (level up, hover icon). DataAsset
* em si fica em memoria depois do scan do Subsystem.
*
* Async pointers (TSubclassOf) sao sync — anim BP precisa estar carregada
* quando o player spawna.
*/
UCLASS(BlueprintType)
class ZMMOJOBS_API UZMMOJobDataAsset : public UPrimaryDataAsset
{
GENERATED_BODY()
public:
/// Chave estavel (bate com `Config/Jobs/<Name>.json:Id` no server e com
/// `FZMMOAttributesSnapshot::ClassId` recebido do server).
UPROPERTY(EditDefaultsOnly, BlueprintReadOnly, Category = "Zeus|Job")
int32 ClassId = 0;
/// Nome tecnico estavel (en-US). Ex: "Novice", "Swordsman". Aparece em
/// logs e telemetria — NUNCA exibido pro player.
UPROPERTY(EditDefaultsOnly, BlueprintReadOnly, Category = "Zeus|Job")
FString TechnicalName;
/// Nome localizado exibido na UI (pt-BR no projeto base). Ex: "Aprendiz".
/// Pode usar FText Localization tables pra multi-idioma futuro.
UPROPERTY(EditDefaultsOnly, BlueprintReadOnly, Category = "Zeus|Job")
FText DisplayName;
/// Icone do job — usado em StatusWindow, CharacterCard, JobChangeUI.
/// Soft ptr: nao carrega ate alguem chamar LoadSynchronous() ou async load.
UPROPERTY(EditDefaultsOnly, BlueprintReadOnly, Category = "Zeus|Job|Assets")
TSoftObjectPtr<UTexture2D> JobIcon;
/// AnimInstance class override (opcional). Quando nao-null, AnimBP
/// padrao do PlayerCharacter eh substituido por esta classe no spawn
/// (jobs com move set diferente — Monge bate, Assassin furtivo, etc.).
UPROPERTY(EditDefaultsOnly, BlueprintReadOnly, Category = "Zeus|Job|Anim")
TSubclassOf<UAnimInstance> DefaultAnimInstanceClass;
/// Montage de "job change" (animacao de transformacao). Tocada quando
/// player promove de Novice → 1a classe. V1 nao implementado.
UPROPERTY(EditDefaultsOnly, BlueprintReadOnly, Category = "Zeus|Job|Anim")
TSoftObjectPtr<UAnimMontage> JobChangeMontage;
/// Som de level up override. Quando null, HUD usa som generico do projeto.
/// Soft ptr: lazy load no primeiro level up.
UPROPERTY(EditDefaultsOnly, BlueprintReadOnly, Category = "Zeus|Job|Audio")
TSoftObjectPtr<USoundBase> LevelUpSoundCue;
/// PrimaryAssetType pro AssetManager (precisa pra GetPrimaryAssetIdList).
/// Hardcoded "ZMMOJob" — registry classifica todos os DAs deste tipo.
virtual FPrimaryAssetId GetPrimaryAssetId() const override
{
return FPrimaryAssetId(FPrimaryAssetType(TEXT("ZMMOJob")),
*FString::Printf(TEXT("Job_%d_%s"), ClassId, *TechnicalName));
}
};

View File

@@ -0,0 +1,39 @@
#pragma once
#include "CoreMinimal.h"
#include "Kismet/BlueprintFunctionLibrary.h"
#include "ZMMOJobsLibrary.generated.h"
class UZMMOJobDataAsset;
/**
* Helpers Blueprint pra UI/HUD acessar dados de job sem precisar pegar o
* Subsystem manualmente. Todos usam WorldContextObject pra resolver
* GameInstance → Subsystem internamente.
*
* Padrao de uso em UMG:
* - Text binding: `JobsLibrary::GetJobDisplayName(Self, AttrComp.Current.ClassId)`
* - Image binding: `JobsLibrary::GetJobIcon(Self, ClassId).LoadSynchronous()`
*/
UCLASS()
class ZMMOJOBS_API UZMMOJobsLibrary : public UBlueprintFunctionLibrary
{
GENERATED_BODY()
public:
/// Nome localizado do job. Fallback "Classe N" se ClassId nao tem DA
/// correspondente (job nao registrado no Content/ZMMO/Data/Jobs/).
UFUNCTION(BlueprintPure, Category = "Zeus|Jobs",
meta = (WorldContext = "WorldContextObject"))
static FText GetJobDisplayName(const UObject* WorldContextObject, int32 ClassId);
/// DataAsset completo (pode ser nullptr — caller checa).
UFUNCTION(BlueprintPure, Category = "Zeus|Jobs",
meta = (WorldContext = "WorldContextObject"))
static UZMMOJobDataAsset* GetJobData(const UObject* WorldContextObject, int32 ClassId);
/// Helper de UI: retorna true se ClassId tem DA registrado.
UFUNCTION(BlueprintPure, Category = "Zeus|Jobs",
meta = (WorldContext = "WorldContextObject"))
static bool IsJobRegistered(const UObject* WorldContextObject, int32 ClassId);
};

View File

@@ -0,0 +1,23 @@
#pragma once
#include "CoreMinimal.h"
#include "Modules/ModuleInterface.h"
/**
* Sub-modulo cliente do JobsSystem.
*
* Espelho de `Server/ZeusServerEngine/Game/MMO/Modules/JobsSystem/`. Roda
* como modulo Runtime em LoadingPhase PreDefault para que o
* `UZMMOJobsSubsystem` esteja registrado antes do ZMMO core spawnar a HUD.
*
* Responsabilidades:
* - Dados visuais por job: DisplayName localizado, JobIcon, anim override
* - Lookup classId → UZMMOJobDataAsset via Subsystem
* - Library Blueprint pra HUD/UI consumir
*/
class FZMMOJobsModule : public IModuleInterface
{
public:
virtual void StartupModule() override;
virtual void ShutdownModule() override;
};

View File

@@ -0,0 +1,52 @@
#pragma once
#include "CoreMinimal.h"
#include "Subsystems/GameInstanceSubsystem.h"
#include "ZMMOJobsSubsystem.generated.h"
class UZMMOJobDataAsset;
/**
* Cache central de UZMMOJobDataAsset por ClassId. Vive como GameInstance
* Subsystem — startup automatico quando GameInstance inicializa (PIE,
* standalone, packaged). Sobrevive a map transitions.
*
* Initialize:
* 1. Carrega via AssetRegistry: scan de UZMMOJobDataAsset em todo Content
* 2. Cacheia em TMap<int32, UZMMOJobDataAsset*> por ClassId
* 3. Log: quantos jobs encontrou
*
* Uso:
* - C++: GameInstance->GetSubsystem<UZMMOJobsSubsystem>()->GetJobData(0)
* - BP: UZMMOJobsLibrary::GetJobData(WorldContext, 0)
*
* Fase 7+ (Mod support): SCANEAR PrimaryAssetType="ZMMOJob" do AssetManager
* permite mods adicionarem DAs proprios sem mexer no source.
*/
UCLASS()
class ZMMOJOBS_API UZMMOJobsSubsystem : public UGameInstanceSubsystem
{
GENERATED_BODY()
public:
virtual void Initialize(FSubsystemCollectionBase& Collection) override;
virtual void Deinitialize() override;
/// Lookup por classId. Retorna nullptr se ClassId nao tem DA correspondente
/// (caller usa Library com fallback FText).
UFUNCTION(BlueprintPure, Category = "Zeus|Jobs")
UZMMOJobDataAsset* GetJobData(int32 ClassId) const;
/// Total de jobs cacheados (diagnostico).
UFUNCTION(BlueprintPure, Category = "Zeus|Jobs")
int32 GetJobCount() const { return CachedJobs.Num(); }
private:
/// Scan AssetRegistry + popula CachedJobs. Sincronizo (V1) — chamado UMA
/// vez no Initialize. Pra 50 jobs eh aceitavel; se crescer pra 200+ pode
/// virar async no futuro.
void LoadJobsFromAssetRegistry();
UPROPERTY()
TMap<int32, TObjectPtr<UZMMOJobDataAsset>> CachedJobs;
};

View File

@@ -0,0 +1,26 @@
using UnrealBuildTool;
public class ZMMOJobs : ModuleRules
{
public ZMMOJobs(ReadOnlyTargetRules Target) : base(Target)
{
PCHUsage = PCHUsageMode.UseExplicitOrSharedPCHs;
PublicDependencyModuleNames.AddRange(new string[] {
"Core",
"CoreUObject",
"Engine"
});
PrivateDependencyModuleNames.AddRange(new string[] {
"AssetRegistry" // FAssetRegistryModule pra scan de DA_Job_*
});
// Espelho do `Server/.../JobsSystem/module.json` (modulo nucleo MMO).
// Cliente roda em LoadingPhase PreDefault — Subsystem inicializa
// ANTES de o ZMMO core criar a player character / HUD.
//
// Sem dependencia em ZMMOAttributes: server envia classId no snapshot,
// cliente faz lookup local. Mantem o peer-module pattern.
}
}

View File

@@ -0,0 +1,18 @@
{
"name": "ZMMOJobs",
"gameType": "MMO",
"version": "0.1.0",
"side": "client",
"dependencies": {
"engine": ["Core", "CoreUObject", "Engine", "AssetRegistry"],
"modules": []
},
"publicHeaders": [
"ZMMOJobsModule.h",
"ZMMOJobDataAsset.h",
"ZMMOJobsSubsystem.h",
"ZMMOJobsLibrary.h"
],
"loadingPhase": "PreDefault",
"description": "Sub-modulo cliente do GameType MMO — dados visuais/UI por job (DisplayName localizado, ícone, anim override, som de level up). Espelha JobsSystem do server mas focado em apresentacao. NAO depende de ZMMOAttributes — server envia classId no snapshot, cliente faz lookup local. Pattern peer-module estabelecido no PR #9."
}

View File

@@ -13,6 +13,11 @@
"Name": "ZMMOAttributes",
"Type": "Runtime",
"LoadingPhase": "PreDefault"
},
{
"Name": "ZMMOJobs",
"Type": "Runtime",
"LoadingPhase": "PreDefault"
}
],
"Plugins": [