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>
- UZMMOHudWidget ganha LevelUpToastContainer (BindWidgetOptional UWidget)
e LevelUpToastText (BindWidgetOptional UTextBlock) — both Collapsed por
default; HandleLevelUp os mostra com text "LEVEL UP! Lv N" e dispara
timer FTimerHandle pra esconder após LevelUpToastDurationSec (2.5s).
- WBP_HUD: Border escuro semi-transparente (R=0.05,G=0.04,B=0.10,A=0.85)
com TextBlock dourado (R=1.0,G=0.85,B=0.30) fonte 32, slot top-center
com padding top=120, visibility default Collapsed.
- Snapshot pós-level-up (vem logo após S_LEVEL_UP) atualiza HP/SP/level via
HpSpBar automaticamente — toast é só feedback visual instantâneo.
Polish futuro (não desta frente): SFX cue + particle aura dourada +
animação de fade in/out via UMG.
- FZMMOAttributesSnapshot ganha 7 pares (AtkBase/AtkEquipBonus, ...) +
Aspd único, espelhando split do server.
- ZMMOAttributeNetworkHandler::ToSnapshot mapeia campos novos do
FZeusAttributesPayload.
- WBP_StatusWindow / RefreshFromSnapshot agora formata derivados como
"ATK 51 + 0", "CRIT 1.3 + 0.0", "ASPD 150" (este sem split — cálculo
não-linear no server via aspd_base × stats).
Quando InventorySystem entrar, equipar arma muda `currentWeapon` no server,
Recalculate roda, e UI mostra ATK/MATK/DEF/etc com bonus > 0 e ASPD
ajustado pela aspd_base da arma equipada. Sem código de migração — schema
do snapshot já está estabilizado.
- ZMMOStatusWindowWidget (UCommonActivatableWidget, modo Menu): grid 2 colunas
estilo RO. Esquerda: stats primários com botões + por stat (RequestStatAlloc).
Direita: derivados ATK/MATK/DEF/MDEF/HIT/FLEE/CRIT/ASPD vindos do snapshot
do server — cliente NUNCA recalcula (anti-cheat foundation).
- Botão + envia C_STAT_ALLOC → server valida cost RO (floor((stat-1)/10)+2),
aplica + recalcula derivados + SaveCharFull async, manda
S_ATTRIBUTE_SNAPSHOT_FULL + reply. UI atualiza em tempo real via
OnAttributesChanged.
- ZMMOAttributeComponent ganha RequestStatAlloc + NotifyStatAllocReply +
delegate OnStatAllocReply para feedback de rejeição.
- ZMMOAttributeNetworkHandler: HandleStatAllocReply roteia pro componente do
player local (reply não traz EntityId — sempre quem fez o request).
- PlayerController: hotkey Alt+A (FInputChord legacy + BindKey, sem precisar
de IA asset). ToggleStatusWindow via UUIInGameFlowSubsystem.
- WBP_StatusWindow: layout 2 colunas via MCP set_widget_tree (43 widgets) +
BgBorder escuro semi-transparente + centralizado no overlay.
- DA_InGameScreenSet: StatusWindow → WBP_StatusWindow.
Espelho simétrico do front-end pattern (UUIFrontEndFlowSubsystem +
DA_FrontEndScreenSet + UUIPrimaryGameLayout_Base) para a UI in-game.
Pavimenta caminho pra Inventory, Skills, StatusWindow, EscapeMenu, etc.
## UI in-game system (Source/ZMMO/Game/UI/InGame/)
- EZMMOInGameUIState enum (None, Playing, StatusWindow, Inventory,
SkillTree, EscapeMenu, Loading) em Data/UI/InGameTypes.h
- UUIInGameScreenSet DataAsset: TMap<EZMMOInGameUIState, TSoftClassPtr<...>>
- UUIInGameFlowSubsystem (GameInstanceSubsystem) com SetState/ToggleScreen
- DA_InGameScreenSet em Content/ZMMO/UI/InGame/ mapeia Playing->WBP_HUD
- Layer routing: Playing -> UI.Layer.Game | menus -> GameMenu | loading -> Modal
## AHUD pattern (idiomatic UE5)
- AZMMOHUD : AHUD em Source/ZMMO/Game/Modes/
- AZMMOGameMode::HUDClass = AZMMOHUD::StaticClass()
- AZMMOHUD::BeginPlay chama UIInGameFlowSubsystem::StartInGame
- Pawn não cria mais HUD (separação de responsabilidades)
## PlayerState + Component Registry (Open-Closed)
- AZMMOPlayerState : APlayerState em Source/ZMMO/Game/Modes/
- AZMMOGameMode::PlayerStateClass = AZMMOPlayerState::StaticClass()
- UPROPERTY Config TArray<TSubclassOf<UActorComponent>> ComponentClasses
- DefaultGame.ini: +ComponentClasses=/Script/ZMMOAttributes.ZMMOAttributeComponent
- Ctor lê via GConfig (mais robusto que UPROPERTY Config + parsing) e
instancia cada componente como CreateDefaultSubobject
Por que componentes no PlayerState e não no Pawn:
- Sobrevive ao despawn (morte/respawn)
- Spectator-friendly
- 1:1 com player (Pawn pode trocar: vehicle, mount, possession)
- Padrão MMO clássico (rathena, TrinityCore)
Adicionar novo módulo (Inventory, Skills, Guild) = 1 linha no
DefaultGame.ini, sem editar AZMMOPlayerState.cpp.
## HUD composite
- UZMMOHudWidget : UCommonActivatableWidget em ZMMO core (UI/InGame/)
- WBP_HUD em Content/ZMMO/UI/HUD/ herda UZMMOHudWidget
- HpSpBar (UZMMOHudHpSpWidget — sub-modulo ZMMOAttributes) embedded via
BindWidgetOptional. Sub-widgets futuros: PlayerInfo, Minimap, QuickBar,
Buffs, Chat, TargetInfo
- UZMMOHudHpSpWidget volta a ser UUserWidget puro (sub-widget, não root)
- UZMMOHudWidget::NativeOnActivated:
- SetVisibility(HitTestInvisible) — mouse passa direto pro Pawn
- BindToLocalPlayer: busca AttributeComponent no PC->PlayerState
- Subscreve OnAttributesChanged/OnHpSpChanged/OnLevelUp
- Propaga snapshots pra sub-widgets via HpSpBar->ApplySnapshot
- GetDesiredInputConfig override: ECommonInputMode::Game (sem cursor,
movimento livre). Menus override pra GameAndMenu/Menu.
## NetworkHandler refatorado (AttributeComponent agora no PlayerState)
- ZMMOAttributeNetworkHandler::FindComponentForEntity: busca via
GameState->PlayerArray (O(N_players)), não TActorIterator (O(N_actors))
- ZMMOAttributes.Build.cs += CommonUI, CommonInput
## EnsureRootLayout (fix crítico)
- OpenLevel invalida widgets no viewport ("InvalidateAllWidgets")
- RootLayout (criado pelo FrontEndFlow no boot) sobrevive como UObject
(LocalPlayerSubsystem) mas fica órfão fora do viewport
- Push do WBP_HUD no Stack_Game funcionava mas widget pai (RootLayout)
estava fora do viewport → invisível
- UUIFrontEndFlowSubsystem::EnsureRootLayout (público) recria via
UIManagerSubsystem (idempotente)
- UUIInGameFlowSubsystem::StartInGame chama EnsureRootLayout antes do push
## PlayerCharacter simplificado
- Removido AttributeComponent (vai pro PlayerState via Registry)
- Removido HudHpSpWidgetClass + spawn manual de HUD
- HandleLocalSpawnReady só faz seed do EntityId via PlayerState
- UI lifecycle agora é responsabilidade do AZMMOHUD::BeginPlay
## Verificação (smoke test)
PIE: Login -> CharSelect -> Lobby -> EnterWorld
↓
LogZMMO: AZMMOPlayerState ctor: 1 componente(s) no registry
LogZMMO: ZMMOPlayerState: componente registrado [0] ZMMOAttributeComponent
LogZMMO: FrontEndFlow::EnsureRootLayout: RootLayout recriado
LogZMMO: InGameFlow: state None -> Playing
LogZMMO: InGameFlow: tela Playing pushed em UI.Layer.Game (widget=WBP_HUD_C_0)
LogZMMO: ZMMOHudWidget activated (HpSpBar=WBP_HUD_HpSpBar_C_0)
↓
HUD aparece com Lv 5 / HP 10/200 / SP 5/20 (valores do DB)
Regen funciona (HP/SP enchem a cada 6s/8s)
Movimento normal (input flui pro Pawn)
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>