7 Commits

Author SHA1 Message Date
9390ed9da9 feat(ui/status): nova janela com sub-widgets StatRow/BonusRow + cleanup
- 3 novas classes C++ (UUIPlayerStatus_Window/StatRow/BonusRow) substituem
  UZMMOStatusWindowWidget (removida); StatRow/BonusRow sao reutilizaveis
  via BindWidgetOptional + delegate OnPlusClicked(StatId).
- Window bindea no UZMMOAttributeComponent (PlayerState) e popula header
  (CharName, Job via UZMMOJobsSubsystem, BaseLevel/JobLevel) + 14 rows
  (6 primarios em StatRow com botao "+", 8 derivados em BonusRow).
- Fix spam-click do "+": bPlusButtonsLocked desabilita botoes ate
  OnStatAllocReply do server ou proximo snapshot.
- WBPs movidos pra /UI/Screens/PlayerStatus/, material pra /UI/Materials/.
- 58 widgets renomeados seguindo convencao
  (Frame_/Layout_/Panel_/Container_/Border_/Text_/Image_/Spacer_/List_).
- DA_InGameScreenSet auto-atualizado pelo move.
- Remove WBP_StatusWindow inline (legado).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-26 00:09:28 -03:00
28bfa9a567 feat(attributes): MATK range min~max no snapshot + display
Espelha breaking change do server (S_ATTRIBUTE_SNAPSHOT_FULL agora envia
matkBaseMin/matkBaseMax em vez de matkBase unico) para suportar MATK
pre-renewal puro (rathena status_base_matk_min/max).

- FZeusAttributesPayload: MatkBaseMin/Max em vez de MatkBase
- FZMMOAttributesSnapshot: idem (BlueprintReadOnly)
- ZMMOStatusWindowWidget: exibe "MATK min ~ max + equip"

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-25 01:52:48 -03:00
6391372f00 feat(jobs): Jobs.1 client UI infra (handler + library + tecla J) + UI tweaks
Infraestrutura cliente da promocao de classe (Jobs.1). WBP_JobChangePanel
NAO esta neste commit — foi exploracao descartada (criar UI dinamica de
botoes via MCP BP graph esbarrou em limites do CreateWidget node). A
fundacao C++ fica pronta pra ser usada quando voltarmos pra UI.

INFRA C++ NOVA (Source/ZMMOJobs/)
* UZMMOJobChangeNetworkHandler (WorldSubsystem novo) — bind no
  OnJobChangeResult do plugin e re-broadcast via FZMMOOnJobChangeResultBP
  (dynamic multicast assignavel em BP). WBP futura escuta direto.
* UZMMOJobsSubsystem helpers: GetJobsByTier(EZMMOJobTier) +
  GetEligibleNextJobs(currentClassId) — filtra por ParentClassId. Pure.
* UZMMOJobsLibrary BPFL wrappers + SendJobChangeRequest convenience
  (delega pro UZeusNetworkSubsystem). Dep ZeusNetwork adicionada no
  Build.cs.

ENUM + INPUT
* EZMMOInGameUIState ganha JobChangePanel (entre StatusWindow e
  Inventory). DA_InGameScreenSet pendente de receber entry quando
  WBP for criada (tecla J vai falhar silenciosamente ate la).
* ZMMOPlayerController: tecla J -> ToggleJobChangePanel ->
  Flow->ToggleScreen(JobChangePanel). Espelha pattern do Alt+A do
  StatusWindow (InputComponent legacy BindKey + FInputChord).

UI TWEAKS
* WBP_StatusWindow + DT_UI_Styles: ajustes visuais no Editor durante a
  sessao (sem mudanca funcional documentavel — provavelmente fontes/
  spacing/cores).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-24 01:37:04 -03:00
f832c1a4df fix(log): remove HandleZeusNetworkLog forwarder (duplicava LogZMMO)
UZMMOGameInstance assinava OnNetworkLog do plugin e re-logava em LogZMMO
com prefix [Zeus]. Resultado: cada linha do plugin aparecia 2x no console
(LogZeusNetwork + LogZMMO). Era diagnostico inicial da fase de bring-up
(sessao 2026-05-07); agora so' polui o log.

Bind removido em BindZeusDelegates. Funcao HandleZeusNetworkLog mantida no
.h/.cpp (UFUNCTION dynamic delegate target) com comentario explicando como
reativar via descomentar a linha do AddDynamic.

Plugin continua logando em LogZeusNetwork (console) + Saved/Logs/ZeusNetwork/
(arquivo proprio) — diagnostico nao foi perdido, so' deduplicado.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-24 01:36:34 -03:00
c3df705a7a feat(jobs): Jobs.1 client — DA expandido + 7 DA_Job assets organizados por tier
* UZMMOJobDataAsset (Source/ZMMOJobs/Public/) ganha 3 campos novos
  espelhando o JobDef do server:
    - Tier             (EZMMOJobTier)
    - ParentClassId    (int32)
    - AllowedWeapons   (TArray<EZMMOWeaponType>)

* Novo header ZMMOJobTypes.h com 2 UENUM(BlueprintType):
    - EZMMOJobTier      (6 valores: Beginner..Mastery)
    - EZMMOWeaponType   (19 valores: Fist..Gun)
  Todos com UMETA(DisplayName="pt-BR") pro Editor mostrar nomes
  amigaveis em dropdowns. Ordem casa byte-a-byte com EJobTier/
  EWeaponType do server (necessario pq snapshot carrega tier u8 + mask).

* 7 DA_Job_*.uasset criados via MCP Python, organizados por tier:
    Content/ZMMO/Data/Jobs/Beginner/
      DA_Job_Novice    (ClassId=0, AllowedWeapons=[Fist,Dagger])
    Content/ZMMO/Data/Jobs/Vocation/
      DA_Job_Warrior   (ClassId=1, 8 armas: Sword+Spear+Axe variations)
      DA_Job_Arcanist  (ClassId=2, 5 armas: Staff+Book)
      DA_Job_Ranger    (ClassId=3, 5 armas: Bow+Musical+Whip)
      DA_Job_Artisan   (ClassId=4, 5 armas: Sword1H+Mace1H+Axe1H)
      DA_Job_Rogue     (ClassId=5, 5 armas: Sword1H+Bow+Katar)
      DA_Job_Devout    (ClassId=6, 5 armas: Mace+Staff1H+Knuckle)
  ParentClassId=0 (Novice) em todas Vocacoes. Tier=Vocation.
  JobIcon/Sound/Anim ficam None — preencher conforme arte for produzida.

  ZMMOJobsSubsystem busca por classe (GetAssetsByClass), entao a
  reorganizacao em subpastas NAO requer mudanca em .cpp/.h.

* ZMMOAttributeTypes.h: comentario de opcode 1200 -> 1500 (espelha
  refactor Open-Closed do server ranges; valores wire format mudaram).

Boot esperado: [ZMMOJobsSubsystem] Initialize: 7 job(s) carregados.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-23 23:53:34 -03:00
a354d9eefc chore(jobs): renomeia display "Aprendiz" -> "Novato"
DA_Job_Novice DisplayName e docs de arquitetura (3 ARQUITETURA*.md)
sincronizados com a nova terminologia PT-BR. Server (Novice.json +
JobsDatabase.cpp) sera commitado separado no repo do ZeusServerEngine.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-23 21:29:12 -03:00
ca304a99dd Merge pull request 'feat(client): AttributeSystem (Fase 1-3) + ZMMOJobs + UI in-game' (#3) from feat/attribute-system-mmo into main
Reviewed-on: #3
2026-05-23 21:06:47 -03:00
41 changed files with 911 additions and 295 deletions

View File

@@ -9,7 +9,7 @@
> **Extensões da arquitetura** (documentos normativos paralelos, escopo específico):
> - [`ARQUITETURA_SERVER_SELECT.md`](ARQUITETURA_SERVER_SELECT.md) — Server Select, lista de mundos, handoff CharServer↔WorldServer, multi-region, queue, Valkey. Roadmap em 7 fases.
> - [`ARQUITETURA_CHARACTER_MODEL.md`](ARQUITETURA_CHARACTER_MODEL.md) — Stats primários (STR/AGI/VIT/INT/DEX/LUK), classes (Aprendiz → especializações), fórmulas de stats derivados (ATK/MATK/DEF/...), `jobs.yml` data-driven, stat allocation server-side. Espelha rathena.
> - [`ARQUITETURA_CHARACTER_MODEL.md`](ARQUITETURA_CHARACTER_MODEL.md) — Stats primários (STR/AGI/VIT/INT/DEX/LUK), classes (Novato → especializações), fórmulas de stats derivados (ATK/MATK/DEF/...), `jobs.yml` data-driven, stat allocation server-side. Espelha rathena.
> **Sumário de mudanças (2026-05-11):** adicionada secção de **temas sazonais
> de UI** (`UI/Themes/`), com tutorial §4.7, regra de "no hard ref a textura

View File

@@ -6,7 +6,7 @@
## Context
O ZMMO usa modelo de atributos estilo Ragnarok Online — 6 stats primários que o jogador aloca pontos (STR/AGI/VIT/INT/DEX/LUK), com stats derivados calculados a partir deles + level + classe + equip. Jogador nasce **Aprendiz** (Novice) e especializa em outras classes ao chegar a critérios (job level + quest de mudança).
O ZMMO usa modelo de atributos estilo Ragnarok Online — 6 stats primários que o jogador aloca pontos (STR/AGI/VIT/INT/DEX/LUK), com stats derivados calculados a partir deles + level + classe + equip. Jogador nasce **Novato** (Novice) e especializa em outras classes ao chegar a critérios (job level + quest de mudança).
Este doc fixa:
1. O que persiste no MySQL (CharServer) vs o que vive em memória (WorldServer).
@@ -58,10 +58,10 @@ Padrão rathena: `db/re/job_stats.yml` carregado no boot. Hot-reloadable.
| Componente | Quem decide | Quem persiste |
|---|---|---|
| Criar char (stats iniciais, class=Aprendiz) | CharServer (valida) | CharServer → MySQL |
| Criar char (stats iniciais, class=Novato) | CharServer (valida) | CharServer → MySQL |
| Stat allocation (gastar `status_point` em STR/etc.) | CharServer **ou** WorldServer | CharServer → MySQL (via writeback) |
| Subir de level / ganhar EXP | WorldServer (kill mob, quest) | WorldServer → writeback → CharServer → MySQL |
| Mudar classe (Aprendiz → Espadachim) | WorldServer (cumpre quest) | WorldServer → writeback → CharServer → MySQL |
| Mudar classe (Novato → Espadachim) | WorldServer (cumpre quest) | WorldServer → writeback → CharServer → MySQL |
| Recalcular ATK/MATK/DEF/etc. | WorldServer (memória, toda hora) | NÃO persiste |
| Casting de skill | WorldServer | NÃO persiste (efêmero) |
| Resultado de skill (HP perdido, item dropado) | WorldServer | WorldServer → writeback |
@@ -260,7 +260,7 @@ version: 1
jobs:
- id: 0
name: Novice # nome técnico (inglês, estável)
display_name_ptbr: Aprendiz
display_name_ptbr: Novato
parent_job: null
max_base_level: 99
max_job_level: 10
@@ -322,7 +322,7 @@ jobs:
## 5. Fluxos
### 5.1 Criação de personagem (Aprendiz)
### 5.1 Criação de personagem (Novato)
```
Cliente → C_CHAR_CREATE { name, world_id, class_id=NOVICE, appearance }
@@ -342,7 +342,7 @@ CharServer:
4. → S_CHAR_CREATE_OK
```
**Nota:** o player **sempre** nasce Aprendiz. Outras classes só via job change in-game (Fase pós-Fase 3).
**Nota:** o player **sempre** nasce Novato. Outras classes só via job change in-game (Fase pós-Fase 3).
### 5.2 Stat allocation (após level up)
@@ -377,7 +377,7 @@ WorldServer (em kill mob / complete quest):
Mesmo padrão pra job_exp/job_level (com skill_point em vez de status_point).
### 5.4 Job change (Aprendiz → Espadachim, etc.)
### 5.4 Job change (Novato → Espadachim, etc.)
```
WorldServer (player cumpriu quest):
@@ -571,13 +571,13 @@ Audit log entries (Fase 3 do ServerSelect doc): toda mudança de zeny>1000, leve
## 8. Roadmap de implementação
### Fase A — Schema + criação de char Aprendiz (parte da Fase 1 do ServerSelect)
### Fase A — Schema + criação de char Novato (parte da Fase 1 do ServerSelect)
- Schema `characters` com todos os campos (já especificado no `ARQUITETURA_SERVER_SELECT.md`).
- `jobs.yml` apenas com `Novice` (Aprendiz) — outras classes ficam pra B.
- `jobs.yml` apenas com `Novice` (Novato) — outras classes ficam pra B.
- CharServer carrega `jobs.yml` no boot.
- `C_CHAR_CREATE` valida `class_id == NOVICE` e usa defaults do yml.
- Cliente UI: tela de criação com nome + appearance (sem stat allocation aqui — Aprendiz nasce com todos os stats=1).
- Cliente UI: tela de criação com nome + appearance (sem stat allocation aqui — Novato nasce com todos os stats=1).
### Fase B — Classes adicionais + job change
@@ -628,7 +628,7 @@ Audit log entries (Fase 3 do ServerSelect doc): toda mudança de zeny>1000, leve
1. **Stats primários flat** na tabela `characters` (str/agi/vit/int/dex/luk + level/exp + hp/sp/max_hp/max_sp + status_point/skill_point + zeny + class_id). **Sem entidade separada.**
2. **Stats derivados NUNCA persistidos** — sempre recalculados em runtime no WorldServer (`StatusCalc::ComputeAll`).
3. **Jobs data-driven** via `Server/ZeusCharServer/data/jobs.yml` (versionado no git, hot-reloadable). `characters.class_id` é lookup lógico.
4. **Player nasce Aprendiz** (`class_id=NOVICE`); especialização via quest in-game (Fase B).
4. **Player nasce Novato** (`class_id=NOVICE`); especialização via quest in-game (Fase B).
5. **Stat allocation acontece no WorldServer** (não CharServer). Validação `cost = (s-1)/10 + 2` server-side.
6. **Anti-cheat:** toda mudança de estado é server-authoritative. Cliente apenas renderiza.
7. **Fórmulas espelham rathena** (`status_calc_pc_` em `src/map/status.cpp:4996`) — adapta valores pra balanceamento Zeus depois.

View File

@@ -311,11 +311,11 @@ ALTER TABLE characters
**Notas:**
- `world_id` é nullable (sem chars em prod hoje — evita downtime futuro). Validação no service: novos chars devem ter `world_id`.
- Estrutura flat segue rathena (`.bases/rathena/rathena-master/sql-files/main.sql:209-296`) — padrão da indústria emuladora. Sem entidade `character_stats` separada (1:1 FK seria JOIN inútil).
- Defaults vêm da row Aprendiz no `jobs.yml` (ver doc Character Model).
- Defaults vêm da row Novato no `jobs.yml` (ver doc Character Model).
- **Stats derivados (ATK/MATK/DEF/MDEF/hit/flee/crit/aspd) NÃO entram aqui** — são sempre recalculados em runtime no WorldServer via fórmulas. Persistir é bug fest (esquece atualizar quando muda equip/buff/level).
- `version` (optimistic locking) usado em writeback da Fase 3.
> **Para o detalhamento do modelo de personagem** (stats primários vs derivados, fórmulas de ATK/MATK/DEF, jobs.yml, stat allocation, leveling curve, classes Aprendiz → especialização), ver doc dedicado [`ARQUITETURA_CHARACTER_MODEL.md`](ARQUITETURA_CHARACTER_MODEL.md).
> **Para o detalhamento do modelo de personagem** (stats primários vs derivados, fórmulas de ATK/MATK/DEF, jobs.yml, stat allocation, leveling curve, classes Novato → especialização), ver doc dedicado [`ARQUITETURA_CHARACTER_MODEL.md`](ARQUITETURA_CHARACTER_MODEL.md).
### Schema Valkey (Fase 2+)

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

View File

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

View File

@@ -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<UUIInGameFlowSubsystem>())
{
Flow->ToggleScreen(EZMMOInGameUIState::JobChangePanel);
}
}
}
bool AZMMOPlayerController::ShouldUseTouchControls() const
{
return SVirtualJoystick::ShouldDisplayTouchInterface() || bForceTouchControls;

View File

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

View File

@@ -57,7 +57,11 @@ void UZMMOGameInstance::BindZeusDelegates(UZeusNetworkSubsystem* Zeus)
Zeus->OnConnected.AddDynamic(this, &UZMMOGameInstance::HandleZeusConnected);
Zeus->OnConnectionFailed.AddDynamic(this, &UZMMOGameInstance::HandleZeusConnectionFailed);
Zeus->OnDisconnected.AddDynamic(this, &UZMMOGameInstance::HandleZeusDisconnected);
Zeus->OnNetworkLog.AddDynamic(this, &UZMMOGameInstance::HandleZeusNetworkLog);
// OnNetworkLog: NAO assinamos. Plugin ja loga em LogZeusNetwork (console)
// + arquivo proprio em Saved/Logs/ZeusNetwork/. Forwarding em LogZMMO era
// diagnostico inicial (sessao 2026-05-07); agora gera duplicacao de cada
// linha. Remova esta nota e descomente abaixo se quiser ver no LogZMMO:
// Zeus->OnNetworkLog.AddDynamic(this, &UZMMOGameInstance::HandleZeusNetworkLog);
}
void UZMMOGameInstance::UnbindZeusDelegates(UZeusNetworkSubsystem* Zeus)
@@ -69,7 +73,7 @@ void UZMMOGameInstance::UnbindZeusDelegates(UZeusNetworkSubsystem* Zeus)
Zeus->OnConnected.RemoveDynamic(this, &UZMMOGameInstance::HandleZeusConnected);
Zeus->OnConnectionFailed.RemoveDynamic(this, &UZMMOGameInstance::HandleZeusConnectionFailed);
Zeus->OnDisconnected.RemoveDynamic(this, &UZMMOGameInstance::HandleZeusDisconnected);
Zeus->OnNetworkLog.RemoveDynamic(this, &UZMMOGameInstance::HandleZeusNetworkLog);
// OnNetworkLog: nao foi assinado (ver BindZeusDelegates) — nada a remover.
}
void UZMMOGameInstance::HandleZeusConnected()

View File

@@ -1,168 +0,0 @@
#include "ZMMOStatusWindowWidget.h"
#include "CommonInputTypeEnum.h"
#include "Components/Button.h"
#include "Components/TextBlock.h"
#include "Engine/World.h"
#include "GameFramework/PlayerController.h"
#include "GameFramework/PlayerState.h"
#include "Internationalization/Text.h"
#include "ZMMO.h"
#include "ZMMOAttributeComponent.h"
#include "UI/InGame/UIInGameFlowSubsystem.h"
#include "UI/InGameTypes.h"
UZMMOStatusWindowWidget::UZMMOStatusWindowWidget(const FObjectInitializer& ObjectInitializer)
: Super(ObjectInitializer)
{
bIsBackHandler = true; // Esc/B fecha
bAutoActivate = true;
}
TOptional<FUIInputConfig> UZMMOStatusWindowWidget::GetDesiredInputConfig() const
{
// Modo Menu: cursor visivel, input flui pra UI (clicks nos botões),
// movimento bloqueado. Padrao pra menus modais.
return FUIInputConfig(ECommonInputMode::Menu, EMouseCaptureMode::NoCapture);
}
void UZMMOStatusWindowWidget::NativeOnActivated()
{
Super::NativeOnActivated();
BindToLocalPlayer();
if (PlusBtn_Str) PlusBtn_Str->OnClicked.AddDynamic(this, &UZMMOStatusWindowWidget::OnPlusStr);
if (PlusBtn_Agi) PlusBtn_Agi->OnClicked.AddDynamic(this, &UZMMOStatusWindowWidget::OnPlusAgi);
if (PlusBtn_Vit) PlusBtn_Vit->OnClicked.AddDynamic(this, &UZMMOStatusWindowWidget::OnPlusVit);
if (PlusBtn_Int) PlusBtn_Int->OnClicked.AddDynamic(this, &UZMMOStatusWindowWidget::OnPlusInt);
if (PlusBtn_Dex) PlusBtn_Dex->OnClicked.AddDynamic(this, &UZMMOStatusWindowWidget::OnPlusDex);
if (PlusBtn_Luk) PlusBtn_Luk->OnClicked.AddDynamic(this, &UZMMOStatusWindowWidget::OnPlusLuk);
if (CloseBtn) CloseBtn->OnClicked.AddDynamic(this, &UZMMOStatusWindowWidget::OnCloseClicked);
UE_LOG(LogZMMO, Log, TEXT("ZMMOStatusWindow activated"));
}
void UZMMOStatusWindowWidget::NativeOnDeactivated()
{
UnbindFromComponent();
if (PlusBtn_Str) PlusBtn_Str->OnClicked.RemoveDynamic(this, &UZMMOStatusWindowWidget::OnPlusStr);
if (PlusBtn_Agi) PlusBtn_Agi->OnClicked.RemoveDynamic(this, &UZMMOStatusWindowWidget::OnPlusAgi);
if (PlusBtn_Vit) PlusBtn_Vit->OnClicked.RemoveDynamic(this, &UZMMOStatusWindowWidget::OnPlusVit);
if (PlusBtn_Int) PlusBtn_Int->OnClicked.RemoveDynamic(this, &UZMMOStatusWindowWidget::OnPlusInt);
if (PlusBtn_Dex) PlusBtn_Dex->OnClicked.RemoveDynamic(this, &UZMMOStatusWindowWidget::OnPlusDex);
if (PlusBtn_Luk) PlusBtn_Luk->OnClicked.RemoveDynamic(this, &UZMMOStatusWindowWidget::OnPlusLuk);
if (CloseBtn) CloseBtn->OnClicked.RemoveDynamic(this, &UZMMOStatusWindowWidget::OnCloseClicked);
Super::NativeOnDeactivated();
}
void UZMMOStatusWindowWidget::BindToLocalPlayer()
{
UWorld* World = GetWorld();
if (!World) { return; }
APlayerController* PC = World->GetFirstPlayerController();
if (!PC || !PC->PlayerState) { return; }
UZMMOAttributeComponent* Comp = PC->PlayerState->FindComponentByClass<UZMMOAttributeComponent>();
if (!Comp || Comp == BoundComponent.Get()) { return; }
UnbindFromComponent();
BoundComponent = Comp;
Comp->OnAttributesChanged.AddDynamic(this, &UZMMOStatusWindowWidget::HandleAttributesChanged);
Comp->OnStatAllocReply.AddDynamic(this, &UZMMOStatusWindowWidget::HandleStatAllocReply);
RefreshFromSnapshot(Comp->GetSnapshot());
}
void UZMMOStatusWindowWidget::UnbindFromComponent()
{
if (UZMMOAttributeComponent* Old = BoundComponent.Get())
{
Old->OnAttributesChanged.RemoveDynamic(this, &UZMMOStatusWindowWidget::HandleAttributesChanged);
Old->OnStatAllocReply.RemoveDynamic(this, &UZMMOStatusWindowWidget::HandleStatAllocReply);
}
BoundComponent.Reset();
}
void UZMMOStatusWindowWidget::HandleAttributesChanged(const FZMMOAttributesSnapshot& Snapshot)
{
RefreshFromSnapshot(Snapshot);
}
void UZMMOStatusWindowWidget::HandleStatAllocReply(bool bAccepted, int32 Reason)
{
UE_LOG(LogZMMO, Log, TEXT("StatusWindow: alloc reply accepted=%d reason=%d"),
bAccepted ? 1 : 0, Reason);
// Snapshot subsequente vai disparar HandleAttributesChanged e atualizar UI.
// Aqui poderia popup de erro visual quando bAccepted=false (TODO polish).
}
void UZMMOStatusWindowWidget::RefreshFromSnapshot(const FZMMOAttributesSnapshot& S)
{
if (StrText) StrText->SetText(FText::FromString(FString::Printf(TEXT("STR %d"), S.Str)));
if (AgiText) AgiText->SetText(FText::FromString(FString::Printf(TEXT("AGI %d"), S.Agi)));
if (VitText) VitText->SetText(FText::FromString(FString::Printf(TEXT("VIT %d"), S.Vit)));
if (IntText) IntText->SetText(FText::FromString(FString::Printf(TEXT("INT %d"), S.Int)));
if (DexText) DexText->SetText(FText::FromString(FString::Printf(TEXT("DEX %d"), S.Dex)));
if (LukText) LukText->SetText(FText::FromString(FString::Printf(TEXT("LUK %d"), S.Luk)));
if (StatusPointText)
{
StatusPointText->SetText(FText::FromString(
FString::Printf(TEXT("Status Points: %d"), S.StatusPoint)));
}
// Derivados — formato RO "STAT base + equip" (ex.: "ATK 51 + 0").
// Servidor manda os dois separados; cliente NUNCA recalcula.
if (AtkText) AtkText->SetText(FText::FromString(FString::Printf(TEXT("ATK %d + %d"), S.AtkBase, S.AtkEquipBonus)));
if (MatkText) MatkText->SetText(FText::FromString(FString::Printf(TEXT("MATK %d + %d"), S.MatkBase, S.MatkEquipBonus)));
if (DefText) DefText->SetText(FText::FromString(FString::Printf(TEXT("DEF %d + %d"), S.DefBase, S.DefEquipBonus)));
if (MdefText) MdefText->SetText(FText::FromString(FString::Printf(TEXT("MDEF %d + %d"), S.MdefBase, S.MdefEquipBonus)));
if (HitText) HitText->SetText(FText::FromString(FString::Printf(TEXT("HIT %d + %d"), S.HitBase, S.HitEquipBonus)));
if (FleeText) FleeText->SetText(FText::FromString(FString::Printf(TEXT("FLEE %d + %d"), S.FleeBase, S.FleeEquipBonus)));
if (CritText)
{
// CRIT vem ×10 em ambas as camadas — soma X10 antes de converter pra
// decimal (10 = 1.0). Display: "CRIT base + equip" com 1 casa cada.
const int32 BaseInt = S.CritBaseX10 / 10;
const int32 BaseFrac = S.CritBaseX10 % 10;
const int32 EquipInt = S.CritEquipBonusX10 / 10;
const int32 EquipFrac = S.CritEquipBonusX10 % 10;
CritText->SetText(FText::FromString(FString::Printf(
TEXT("CRIT %d.%d + %d.%d"), BaseInt, BaseFrac, EquipInt, EquipFrac)));
}
// ASPD nao splita (calculo nao-linear server-side via aspd_base × stats).
if (AspdText) AspdText->SetText(FText::FromString(FString::Printf(TEXT("ASPD %d"), S.Aspd)));
// Habilita botões `+` somente se ha status_point disponível.
const bool bCanAlloc = S.StatusPoint > 0;
if (PlusBtn_Str) PlusBtn_Str->SetIsEnabled(bCanAlloc);
if (PlusBtn_Agi) PlusBtn_Agi->SetIsEnabled(bCanAlloc);
if (PlusBtn_Vit) PlusBtn_Vit->SetIsEnabled(bCanAlloc);
if (PlusBtn_Int) PlusBtn_Int->SetIsEnabled(bCanAlloc);
if (PlusBtn_Dex) PlusBtn_Dex->SetIsEnabled(bCanAlloc);
if (PlusBtn_Luk) PlusBtn_Luk->SetIsEnabled(bCanAlloc);
}
void UZMMOStatusWindowWidget::RequestAlloc(int32 StatId)
{
if (UZMMOAttributeComponent* Comp = BoundComponent.Get())
{
Comp->RequestStatAlloc(StatId, /*Amount*/ 1);
}
}
void UZMMOStatusWindowWidget::OnPlusStr() { RequestAlloc(0); }
void UZMMOStatusWindowWidget::OnPlusAgi() { RequestAlloc(1); }
void UZMMOStatusWindowWidget::OnPlusVit() { RequestAlloc(2); }
void UZMMOStatusWindowWidget::OnPlusInt() { RequestAlloc(3); }
void UZMMOStatusWindowWidget::OnPlusDex() { RequestAlloc(4); }
void UZMMOStatusWindowWidget::OnPlusLuk() { RequestAlloc(5); }
void UZMMOStatusWindowWidget::OnCloseClicked()
{
if (UGameInstance* GI = GetGameInstance())
{
if (UUIInGameFlowSubsystem* Flow = GI->GetSubsystem<UUIInGameFlowSubsystem>())
{
Flow->SetState(EZMMOInGameUIState::Playing);
}
}
}

View File

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

View File

@@ -0,0 +1,69 @@
#include "UIPlayerStatus_BonusRow.h"
#include "Components/TextBlock.h"
#include "Internationalization/Text.h"
void UUIPlayerStatus_BonusRow::SetStatName(FText DisplayName)
{
if (Text_StatName) Text_StatName->SetText(DisplayName);
}
void UUIPlayerStatus_BonusRow::SetValues(int32 Base, int32 Bonus)
{
if (Text_BasePart) Text_BasePart->SetText(FText::AsNumber(Base));
const bool bShowBonus = (Bonus != 0);
SetBonusVisibility(bShowBonus);
if (bShowBonus)
{
if (Text_SplitOp) Text_SplitOp->SetText(FText::FromString(Bonus > 0 ? TEXT("+") : TEXT("-")));
if (Text_BonusPart) Text_BonusPart->SetText(FText::AsNumber(FMath::Abs(Bonus)));
}
}
void UUIPlayerStatus_BonusRow::SetRangeValues(int32 MinBase, int32 MaxBase, int32 Bonus)
{
if (Text_BasePart)
{
Text_BasePart->SetText(FText::FromString(FString::Printf(TEXT("%d~%d"), MinBase, MaxBase)));
}
const bool bShowBonus = (Bonus != 0);
SetBonusVisibility(bShowBonus);
if (bShowBonus)
{
if (Text_SplitOp) Text_SplitOp->SetText(FText::FromString(Bonus > 0 ? TEXT("+") : TEXT("-")));
if (Text_BonusPart) Text_BonusPart->SetText(FText::AsNumber(FMath::Abs(Bonus)));
}
}
void UUIPlayerStatus_BonusRow::SetValuesX10(int32 BaseX10, int32 BonusX10)
{
auto FormatX10 = [](int32 ValX10) -> FString
{
const int32 Sign = ValX10 < 0 ? -1 : 1;
const int32 Abs = FMath::Abs(ValX10);
return FString::Printf(TEXT("%d.%d"), Sign * (Abs / 10), Abs % 10);
};
if (Text_BasePart) Text_BasePart->SetText(FText::FromString(FormatX10(BaseX10)));
const bool bShowBonus = (BonusX10 != 0);
SetBonusVisibility(bShowBonus);
if (bShowBonus)
{
if (Text_SplitOp) Text_SplitOp->SetText(FText::FromString(BonusX10 > 0 ? TEXT("+") : TEXT("-")));
if (Text_BonusPart)
{
const int32 Abs = FMath::Abs(BonusX10);
Text_BonusPart->SetText(FText::FromString(FString::Printf(TEXT("%d.%d"), Abs / 10, Abs % 10)));
}
}
}
void UUIPlayerStatus_BonusRow::SetBonusVisibility(bool bShow)
{
const ESlateVisibility Vis = bShow ? ESlateVisibility::Visible : ESlateVisibility::Collapsed;
if (Text_SplitOp) Text_SplitOp->SetVisibility(Vis);
if (Text_BonusPart) Text_BonusPart->SetVisibility(Vis);
}

View File

@@ -0,0 +1,46 @@
#pragma once
#include "CoreMinimal.h"
#include "CommonUserWidget.h"
#include "UIPlayerStatus_BonusRow.generated.h"
class UTextBlock;
/**
* Linha de um stat derivado (ATK/DEF/HIT/etc.) na janela de Status.
* Layout: nome + valor base + (separador + bônus). Sem botão.
*
* Três modos de valor:
* - SetValues(base, bonus) "10 + 2"
* - SetRangeValues(min, max, bonus) "10~15 + 2" (MATK pre-renewal)
* - SetValuesX10(baseX10, bonusX10) "1.5 + 0.0" (CRIT armazenado ×10)
*
* Pareada com o WBP UI_PlayerStatus_BonusRow.
*/
UCLASS(Abstract, Blueprintable)
class ZMMO_API UUIPlayerStatus_BonusRow : public UCommonUserWidget
{
GENERATED_BODY()
public:
UFUNCTION(BlueprintCallable, Category = "Zeus|Status")
void SetStatName(FText DisplayName);
UFUNCTION(BlueprintCallable, Category = "Zeus|Status")
void SetValues(int32 Base, int32 Bonus);
UFUNCTION(BlueprintCallable, Category = "Zeus|Status")
void SetRangeValues(int32 MinBase, int32 MaxBase, int32 Bonus);
UFUNCTION(BlueprintCallable, Category = "Zeus|Status")
void SetValuesX10(int32 BaseX10, int32 BonusX10);
protected:
UPROPERTY(meta = (BindWidgetOptional)) TObjectPtr<UTextBlock> Text_StatName;
UPROPERTY(meta = (BindWidgetOptional)) TObjectPtr<UTextBlock> Text_BasePart;
UPROPERTY(meta = (BindWidgetOptional)) TObjectPtr<UTextBlock> Text_SplitOp;
UPROPERTY(meta = (BindWidgetOptional)) TObjectPtr<UTextBlock> Text_BonusPart;
private:
void SetBonusVisibility(bool bShow);
};

View File

@@ -0,0 +1,73 @@
#include "UIPlayerStatus_StatRow.h"
#include "Components/Button.h"
#include "Components/HorizontalBox.h"
#include "Components/TextBlock.h"
#include "Internationalization/Text.h"
void UUIPlayerStatus_StatRow::SetupStat(int32 InStatId, FText IconLetter, FText DisplayName)
{
StatId = InStatId;
if (Border_StatIconWrap_Label) Border_StatIconWrap_Label->SetText(IconLetter);
if (Text_StatName) Text_StatName->SetText(DisplayName);
}
void UUIPlayerStatus_StatRow::SetValues(int32 Base, int32 Bonus)
{
if (Text_StatBase) Text_StatBase->SetText(FText::AsNumber(Base));
const bool bShowBonus = (Bonus != 0);
const ESlateVisibility BonusVis = bShowBonus ? ESlateVisibility::Visible : ESlateVisibility::Collapsed;
if (Layout_BonusGroup)
{
Layout_BonusGroup->SetVisibility(BonusVis);
}
else
{
if (Text_Sign) Text_Sign->SetVisibility(BonusVis);
if (Text_StatBonus) Text_StatBonus->SetVisibility(BonusVis);
}
if (bShowBonus)
{
if (Text_Sign) Text_Sign->SetText(FText::FromString(Bonus > 0 ? TEXT("+") : TEXT("-")));
if (Text_StatBonus) Text_StatBonus->SetText(FText::AsNumber(FMath::Abs(Bonus)));
}
}
void UUIPlayerStatus_StatRow::SetPlusVisible(bool bVisible)
{
if (Button_PlusBtn)
{
Button_PlusBtn->SetVisibility(bVisible ? ESlateVisibility::Visible : ESlateVisibility::Collapsed);
}
}
void UUIPlayerStatus_StatRow::SetPlusEnabled(bool bEnabled)
{
if (Button_PlusBtn) Button_PlusBtn->SetIsEnabled(bEnabled);
}
void UUIPlayerStatus_StatRow::NativeOnInitialized()
{
Super::NativeOnInitialized();
if (Button_PlusBtn)
{
Button_PlusBtn->OnClicked.AddDynamic(this, &UUIPlayerStatus_StatRow::HandlePlusClicked);
}
}
void UUIPlayerStatus_StatRow::NativeDestruct()
{
if (Button_PlusBtn)
{
Button_PlusBtn->OnClicked.RemoveDynamic(this, &UUIPlayerStatus_StatRow::HandlePlusClicked);
}
Super::NativeDestruct();
}
void UUIPlayerStatus_StatRow::HandlePlusClicked()
{
OnPlusClicked.Broadcast(StatId);
}

View File

@@ -0,0 +1,68 @@
#pragma once
#include "CoreMinimal.h"
#include "CommonUserWidget.h"
#include "UIPlayerStatus_StatRow.generated.h"
class UButton;
class UHorizontalBox;
class UTextBlock;
DECLARE_DYNAMIC_MULTICAST_DELEGATE_OneParam(FZMMOOnStatRowPlusClicked, int32, StatId);
/**
* Linha de um stat primário (STR/AGI/VIT/INT/DEX/LUK) na janela de Status.
* Layout: ícone + nome + valor base + (sinal + bônus) + botão "+".
*
* Sem lógica de domínio — a janela (UUIPlayerStatus_Window) injeta o
* StatId e os valores via Setup/SetValues; o clique no "+" é re-emitido
* como OnPlusClicked(StatId).
*
* Pareada com o WBP UI_PlayerStatus_StatRow.
*/
UCLASS(Abstract, Blueprintable)
class ZMMO_API UUIPlayerStatus_StatRow : public UCommonUserWidget
{
GENERATED_BODY()
public:
/** Setup único chamado pela window quando inicializa a linha. */
UFUNCTION(BlueprintCallable, Category = "Zeus|Status")
void SetupStat(int32 InStatId, FText IconLetter, FText DisplayName);
/** Atualiza valores. Bonus=0 esconde a parte "+N". */
UFUNCTION(BlueprintCallable, Category = "Zeus|Status")
void SetValues(int32 Base, int32 Bonus);
/** Mostra/esconde o botão "+". */
UFUNCTION(BlueprintCallable, Category = "Zeus|Status")
void SetPlusVisible(bool bVisible);
/** Habilita/desabilita o botão "+" (independente da visibilidade). */
UFUNCTION(BlueprintCallable, Category = "Zeus|Status")
void SetPlusEnabled(bool bEnabled);
/** Disparado quando o usuário clica em "+". */
UPROPERTY(BlueprintAssignable, Category = "Zeus|Status")
FZMMOOnStatRowPlusClicked OnPlusClicked;
protected:
virtual void NativeOnInitialized() override;
virtual void NativeDestruct() override;
UPROPERTY(meta = (BindWidgetOptional)) TObjectPtr<UTextBlock> Border_StatIconWrap_Label;
UPROPERTY(meta = (BindWidgetOptional)) TObjectPtr<UTextBlock> Text_StatName;
UPROPERTY(meta = (BindWidgetOptional)) TObjectPtr<UTextBlock> Text_StatBase;
UPROPERTY(meta = (BindWidgetOptional)) TObjectPtr<UTextBlock> Text_Sign;
UPROPERTY(meta = (BindWidgetOptional)) TObjectPtr<UTextBlock> Text_StatBonus;
UPROPERTY(meta = (BindWidgetOptional)) TObjectPtr<UButton> Button_PlusBtn;
/** HBox que envolve sinal+bonus; ocultado quando bonus = 0. Bind opcional —
* se o WBP não expuser, cai pra ocultar Text_Sign/Text_StatBonus individualmente. */
UPROPERTY(meta = (BindWidgetOptional)) TObjectPtr<UHorizontalBox> Layout_BonusGroup;
private:
UFUNCTION() void HandlePlusClicked();
int32 StatId = 0;
};

View File

@@ -0,0 +1,220 @@
#include "UIPlayerStatus_Window.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 "ZMMOJobDataAsset.h"
#include "ZMMOJobsSubsystem.h"
#include "UI/InGame/UIInGameFlowSubsystem.h"
#include "UI/InGameTypes.h"
#include "UI/Widgets/UIPlayerStatus_BonusRow.h"
#include "UI/Widgets/UIPlayerStatus_StatRow.h"
UUIPlayerStatus_Window::UUIPlayerStatus_Window(const FObjectInitializer& ObjectInitializer)
: Super(ObjectInitializer)
{
bIsBackHandler = true;
bAutoActivate = true;
}
TOptional<FUIInputConfig> UUIPlayerStatus_Window::GetDesiredInputConfig() const
{
return FUIInputConfig(ECommonInputMode::Menu, EMouseCaptureMode::NoCapture);
}
void UUIPlayerStatus_Window::NativeOnActivated()
{
Super::NativeOnActivated();
BindRow(Row_Str, 0, TEXT("S"), TEXT("STR"));
BindRow(Row_Agi, 1, TEXT("A"), TEXT("AGI"));
BindRow(Row_Vit, 2, TEXT("V"), TEXT("VIT"));
BindRow(Row_Int, 3, TEXT("I"), TEXT("INT"));
BindRow(Row_Dex, 4, TEXT("D"), TEXT("DEX"));
BindRow(Row_Luk, 5, TEXT("L"), TEXT("LUK"));
if (Row_Atk) Row_Atk->SetStatName(FText::FromString(TEXT("ATK")));
if (Row_Matk) Row_Matk->SetStatName(FText::FromString(TEXT("MATK")));
if (Row_Def) Row_Def->SetStatName(FText::FromString(TEXT("DEF")));
if (Row_Mdef) Row_Mdef->SetStatName(FText::FromString(TEXT("MDEF")));
if (Row_Hit) Row_Hit->SetStatName(FText::FromString(TEXT("HIT")));
if (Row_Flee) Row_Flee->SetStatName(FText::FromString(TEXT("FLEE")));
if (Row_Crit) Row_Crit->SetStatName(FText::FromString(TEXT("CRIT")));
if (Row_Aspd) Row_Aspd->SetStatName(FText::FromString(TEXT("ASPD")));
if (TitleText) TitleText->SetText(FText::FromString(TEXT("Status")));
if (CloseBtn) CloseBtn->OnClicked.AddDynamic(this, &UUIPlayerStatus_Window::OnCloseClicked);
BindToLocalPlayer();
UE_LOG(LogZMMO, Log, TEXT("UIPlayerStatus_Window activated"));
}
void UUIPlayerStatus_Window::NativeOnDeactivated()
{
UnbindFromComponent();
auto UnbindRow = [this](UUIPlayerStatus_StatRow* Row)
{
if (Row) Row->OnPlusClicked.RemoveDynamic(this, &UUIPlayerStatus_Window::HandleRowPlusClicked);
};
UnbindRow(Row_Str); UnbindRow(Row_Agi); UnbindRow(Row_Vit);
UnbindRow(Row_Int); UnbindRow(Row_Dex); UnbindRow(Row_Luk);
if (CloseBtn) CloseBtn->OnClicked.RemoveDynamic(this, &UUIPlayerStatus_Window::OnCloseClicked);
Super::NativeOnDeactivated();
}
void UUIPlayerStatus_Window::BindRow(UUIPlayerStatus_StatRow* Row, int32 StatId, const TCHAR* IconLetter, const TCHAR* DisplayName)
{
if (!Row) return;
Row->SetupStat(StatId, FText::FromString(IconLetter), FText::FromString(DisplayName));
Row->OnPlusClicked.AddDynamic(this, &UUIPlayerStatus_Window::HandleRowPlusClicked);
}
void UUIPlayerStatus_Window::BindToLocalPlayer()
{
UWorld* World = GetWorld();
if (!World) return;
APlayerController* PC = World->GetFirstPlayerController();
if (!PC || !PC->PlayerState) return;
// CharName vem do PlayerState (estável por sessão) — popula uma vez.
if (Text_CharName)
{
Text_CharName->SetText(FText::FromString(PC->PlayerState->GetPlayerName()));
}
UZMMOAttributeComponent* Comp = PC->PlayerState->FindComponentByClass<UZMMOAttributeComponent>();
if (!Comp || Comp == BoundComponent.Get()) return;
UnbindFromComponent();
BoundComponent = Comp;
Comp->OnAttributesChanged.AddDynamic(this, &UUIPlayerStatus_Window::HandleAttributesChanged);
Comp->OnStatAllocReply.AddDynamic(this, &UUIPlayerStatus_Window::HandleStatAllocReply);
RefreshFromSnapshot(Comp->GetSnapshot());
}
void UUIPlayerStatus_Window::UnbindFromComponent()
{
if (UZMMOAttributeComponent* Old = BoundComponent.Get())
{
Old->OnAttributesChanged.RemoveDynamic(this, &UUIPlayerStatus_Window::HandleAttributesChanged);
Old->OnStatAllocReply.RemoveDynamic(this, &UUIPlayerStatus_Window::HandleStatAllocReply);
}
BoundComponent.Reset();
}
void UUIPlayerStatus_Window::HandleAttributesChanged(const FZMMOAttributesSnapshot& Snapshot)
{
bPlusButtonsLocked = false;
RefreshFromSnapshot(Snapshot);
}
void UUIPlayerStatus_Window::HandleStatAllocReply(bool bAccepted, int32 Reason)
{
UE_LOG(LogZMMO, Log, TEXT("UIPlayerStatus_Window: alloc reply accepted=%d reason=%d"),
bAccepted ? 1 : 0, Reason);
bPlusButtonsLocked = false;
if (UZMMOAttributeComponent* Comp = BoundComponent.Get())
{
const FZMMOAttributesSnapshot& S = Comp->GetSnapshot();
SetAllPrimaryRowsPlusEnabled(S.StatusPoint > 0);
}
}
void UUIPlayerStatus_Window::HandleRowPlusClicked(int32 StatId)
{
if (bPlusButtonsLocked) return;
UZMMOAttributeComponent* Comp = BoundComponent.Get();
if (!Comp) return;
bPlusButtonsLocked = true;
SetAllPrimaryRowsPlusEnabled(false);
Comp->RequestStatAlloc(StatId, /*Amount*/ 1);
}
void UUIPlayerStatus_Window::SetAllPrimaryRowsPlusEnabled(bool bEnabled)
{
auto Set = [bEnabled](UUIPlayerStatus_StatRow* Row)
{
if (Row) Row->SetPlusEnabled(bEnabled);
};
Set(Row_Str); Set(Row_Agi); Set(Row_Vit);
Set(Row_Int); Set(Row_Dex); Set(Row_Luk);
}
void UUIPlayerStatus_Window::RefreshFromSnapshot(const FZMMOAttributesSnapshot& S)
{
// Primários: snapshot atual não splita base/bonus de primários (só derivados).
if (Row_Str) Row_Str->SetValues(S.Str, 0);
if (Row_Agi) Row_Agi->SetValues(S.Agi, 0);
if (Row_Vit) Row_Vit->SetValues(S.Vit, 0);
if (Row_Int) Row_Int->SetValues(S.Int, 0);
if (Row_Dex) Row_Dex->SetValues(S.Dex, 0);
if (Row_Luk) Row_Luk->SetValues(S.Luk, 0);
// Header (level/job).
if (Text_BaseLevel_Value) Text_BaseLevel_Value->SetText(FText::AsNumber(S.BaseLevel));
if (Text_JobLevel_Value) Text_JobLevel_Value->SetText(FText::AsNumber(S.JobLevel));
if (Text_Job)
{
FText JobName;
if (UGameInstance* GI = GetGameInstance())
{
if (const UZMMOJobsSubsystem* Jobs = GI->GetSubsystem<UZMMOJobsSubsystem>())
{
if (const UZMMOJobDataAsset* Data = Jobs->GetJobData(S.ClassId))
{
JobName = Data->DisplayName;
}
}
}
if (JobName.IsEmpty())
{
JobName = FText::FromString(FString::Printf(TEXT("Job %d"), S.ClassId));
}
Text_Job->SetText(JobName);
}
if (StatusPointText)
{
StatusPointText->SetText(FText::FromString(
FString::Printf(TEXT("Status Points: %d"), S.StatusPoint)));
}
// Derivados (server-authoritative: base + equip).
if (Row_Atk) Row_Atk->SetValues(S.AtkBase, S.AtkEquipBonus);
if (Row_Matk) Row_Matk->SetRangeValues(S.MatkBaseMin, S.MatkBaseMax, S.MatkEquipBonus);
if (Row_Def) Row_Def->SetValues(S.DefBase, S.DefEquipBonus);
if (Row_Mdef) Row_Mdef->SetValues(S.MdefBase, S.MdefEquipBonus);
if (Row_Hit) Row_Hit->SetValues(S.HitBase, S.HitEquipBonus);
if (Row_Flee) Row_Flee->SetValues(S.FleeBase, S.FleeEquipBonus);
if (Row_Crit) Row_Crit->SetValuesX10(S.CritBaseX10, S.CritEquipBonusX10);
// ASPD não splita.
if (Row_Aspd) Row_Aspd->SetValues(S.Aspd, 0);
const bool bCanAlloc = !bPlusButtonsLocked && S.StatusPoint > 0;
SetAllPrimaryRowsPlusEnabled(bCanAlloc);
}
void UUIPlayerStatus_Window::OnCloseClicked()
{
if (UGameInstance* GI = GetGameInstance())
{
if (UUIInGameFlowSubsystem* Flow = GI->GetSubsystem<UUIInGameFlowSubsystem>())
{
Flow->SetState(EZMMOInGameUIState::Playing);
}
}
}

View File

@@ -0,0 +1,82 @@
#pragma once
#include "CoreMinimal.h"
#include "CommonActivatableWidget.h"
#include "ZMMOAttributeTypes.h"
#include "UIPlayerStatus_Window.generated.h"
class UButton;
class UTextBlock;
class UUIPlayerStatus_BonusRow;
class UUIPlayerStatus_StatRow;
class UZMMOAttributeComponent;
/**
* Janela de Status do player — composta por instâncias dos sub-widgets
* UUIPlayerStatus_StatRow (6 primários) e UUIPlayerStatus_BonusRow (8 derivados).
*
* Consome o UZMMOAttributeComponent do PlayerState e a pipeline de
* C_STAT_ALLOC (RequestStatAlloc → server valida → OnStatAllocReply).
*
* Pareada com o WBP UI_PlayerStatus_Window. Os 14 rows são BindWidgetOptional
* pra suportar variantes (ex.: HUD compacto sem CRIT/ASPD).
*/
UCLASS(Abstract, Blueprintable, BlueprintType)
class ZMMO_API UUIPlayerStatus_Window : public UCommonActivatableWidget
{
GENERATED_BODY()
public:
UUIPlayerStatus_Window(const FObjectInitializer& ObjectInitializer);
protected:
virtual void NativeOnActivated() override;
virtual void NativeOnDeactivated() override;
virtual TOptional<FUIInputConfig> GetDesiredInputConfig() const override;
UPROPERTY(BlueprintReadOnly, meta = (BindWidgetOptional), Category = "Zeus|Status") TObjectPtr<UTextBlock> TitleText;
UPROPERTY(BlueprintReadOnly, meta = (BindWidgetOptional), Category = "Zeus|Status") TObjectPtr<UTextBlock> StatusPointText;
// Header: identidade do char.
UPROPERTY(BlueprintReadOnly, meta = (BindWidgetOptional), Category = "Zeus|Status|Header") TObjectPtr<UTextBlock> Text_CharName;
UPROPERTY(BlueprintReadOnly, meta = (BindWidgetOptional), Category = "Zeus|Status|Header") TObjectPtr<UTextBlock> Text_Job;
UPROPERTY(BlueprintReadOnly, meta = (BindWidgetOptional), Category = "Zeus|Status|Header") TObjectPtr<UTextBlock> Text_BaseLevel_Value;
UPROPERTY(BlueprintReadOnly, meta = (BindWidgetOptional), Category = "Zeus|Status|Header") TObjectPtr<UTextBlock> Text_JobLevel_Value;
UPROPERTY(BlueprintReadOnly, meta = (BindWidgetOptional), Category = "Zeus|Status") TObjectPtr<UUIPlayerStatus_StatRow> Row_Str;
UPROPERTY(BlueprintReadOnly, meta = (BindWidgetOptional), Category = "Zeus|Status") TObjectPtr<UUIPlayerStatus_StatRow> Row_Agi;
UPROPERTY(BlueprintReadOnly, meta = (BindWidgetOptional), Category = "Zeus|Status") TObjectPtr<UUIPlayerStatus_StatRow> Row_Vit;
UPROPERTY(BlueprintReadOnly, meta = (BindWidgetOptional), Category = "Zeus|Status") TObjectPtr<UUIPlayerStatus_StatRow> Row_Int;
UPROPERTY(BlueprintReadOnly, meta = (BindWidgetOptional), Category = "Zeus|Status") TObjectPtr<UUIPlayerStatus_StatRow> Row_Dex;
UPROPERTY(BlueprintReadOnly, meta = (BindWidgetOptional), Category = "Zeus|Status") TObjectPtr<UUIPlayerStatus_StatRow> Row_Luk;
UPROPERTY(BlueprintReadOnly, meta = (BindWidgetOptional), Category = "Zeus|Status|Derived") TObjectPtr<UUIPlayerStatus_BonusRow> Row_Atk;
UPROPERTY(BlueprintReadOnly, meta = (BindWidgetOptional), Category = "Zeus|Status|Derived") TObjectPtr<UUIPlayerStatus_BonusRow> Row_Matk;
UPROPERTY(BlueprintReadOnly, meta = (BindWidgetOptional), Category = "Zeus|Status|Derived") TObjectPtr<UUIPlayerStatus_BonusRow> Row_Def;
UPROPERTY(BlueprintReadOnly, meta = (BindWidgetOptional), Category = "Zeus|Status|Derived") TObjectPtr<UUIPlayerStatus_BonusRow> Row_Mdef;
UPROPERTY(BlueprintReadOnly, meta = (BindWidgetOptional), Category = "Zeus|Status|Derived") TObjectPtr<UUIPlayerStatus_BonusRow> Row_Hit;
UPROPERTY(BlueprintReadOnly, meta = (BindWidgetOptional), Category = "Zeus|Status|Derived") TObjectPtr<UUIPlayerStatus_BonusRow> Row_Flee;
UPROPERTY(BlueprintReadOnly, meta = (BindWidgetOptional), Category = "Zeus|Status|Derived") TObjectPtr<UUIPlayerStatus_BonusRow> Row_Crit;
UPROPERTY(BlueprintReadOnly, meta = (BindWidgetOptional), Category = "Zeus|Status|Derived") TObjectPtr<UUIPlayerStatus_BonusRow> Row_Aspd;
UPROPERTY(BlueprintReadOnly, meta = (BindWidgetOptional), Category = "Zeus|Status") TObjectPtr<UButton> CloseBtn;
private:
void BindToLocalPlayer();
void UnbindFromComponent();
void RefreshFromSnapshot(const FZMMOAttributesSnapshot& Snap);
void SetAllPrimaryRowsPlusEnabled(bool bEnabled);
void BindRow(UUIPlayerStatus_StatRow* Row, int32 StatId, const TCHAR* IconLetter, const TCHAR* DisplayName);
UFUNCTION() void HandleAttributesChanged(const FZMMOAttributesSnapshot& Snapshot);
UFUNCTION() void HandleStatAllocReply(bool bAccepted, int32 Reason);
UFUNCTION() void HandleRowPlusClicked(int32 StatId);
UFUNCTION() void OnCloseClicked();
UPROPERTY(Transient)
TWeakObjectPtr<UZMMOAttributeComponent> BoundComponent;
/** Trava local pra evitar spam-click: vira true ao clicar, false ao
* receber StatAllocReply (ou snapshot novo, o que vier primeiro). */
bool bPlusButtonsLocked = false;
};

View File

@@ -37,7 +37,8 @@ namespace
S.Money = P.Money;
S.AtkBase = P.AtkBase;
S.AtkEquipBonus = P.AtkEquipBonus;
S.MatkBase = P.MatkBase;
S.MatkBaseMin = P.MatkBaseMin;
S.MatkBaseMax = P.MatkBaseMax;
S.MatkEquipBonus = P.MatkEquipBonus;
S.DefBase = P.DefBase;
S.DefEquipBonus = P.DefEquipBonus;

View File

@@ -5,7 +5,7 @@
/**
* Snapshot completo dos atributos do char vindo do server via
* S_ATTRIBUTE_SNAPSHOT_FULL (opcode 1200).
* S_ATTRIBUTE_SNAPSHOT_FULL (opcode 1500).
*
* Cliente NUNCA recalcula derivados — apenas exibe o que recebe. Os campos
* HP/MaxHp/SP/MaxSp ja sao os "efetivos" (base + equip + buff somados no
@@ -56,7 +56,10 @@ struct ZMMOATTRIBUTES_API FZMMOAttributesSnapshot
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;
/** MATK pre-renewal e' um range [min, max] (rathena status_base_matk_min/max).
* Display: "MATK min ~ max + equip". */
UPROPERTY(BlueprintReadOnly, Category = "Zeus|Attributes|Derived") int32 MatkBaseMin = 0;
UPROPERTY(BlueprintReadOnly, Category = "Zeus|Attributes|Derived") int32 MatkBaseMax = 0;
UPROPERTY(BlueprintReadOnly, Category = "Zeus|Attributes|Derived") int32 MatkEquipBonus = 0;
UPROPERTY(BlueprintReadOnly, Category = "Zeus|Attributes|Derived") int32 DefBase = 0;

View File

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

View File

@@ -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<UZMMOJobDataAsset*> UZMMOJobsLibrary::GetJobsByTier(
const UObject* WorldContextObject, EZMMOJobTier Tier)
{
if (UZMMOJobsSubsystem* Sub = ResolveSubsystem(WorldContextObject))
{
return Sub->GetJobsByTier(Tier);
}
return {};
}
TArray<UZMMOJobDataAsset*> 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<UZeusNetworkSubsystem>())
{
Net->SendJobChangeRequest(TargetClassId);
}
}

View File

@@ -65,3 +65,32 @@ UZMMOJobDataAsset* UZMMOJobsSubsystem::GetJobData(int32 ClassId) const
}
return nullptr;
}
TArray<UZMMOJobDataAsset*> UZMMOJobsSubsystem::GetJobsByTier(EZMMOJobTier Tier) const
{
TArray<UZMMOJobDataAsset*> Out;
Out.Reserve(CachedJobs.Num());
for (const TPair<int32, TObjectPtr<UZMMOJobDataAsset>>& Pair : CachedJobs)
{
if (Pair.Value && Pair.Value->Tier == Tier)
{
Out.Add(Pair.Value.Get());
}
}
return Out;
}
TArray<UZMMOJobDataAsset*> UZMMOJobsSubsystem::GetEligibleNextJobs(int32 CurrentClassId) const
{
TArray<UZMMOJobDataAsset*> Out;
Out.Reserve(4); // tipico: 2-6 destinos
for (const TPair<int32, TObjectPtr<UZMMOJobDataAsset>>& Pair : CachedJobs)
{
if (Pair.Value && Pair.Value->ParentClassId == CurrentClassId
&& Pair.Value->ClassId != CurrentClassId)
{
Out.Add(Pair.Value.Get());
}
}
return Out;
}

View File

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

View File

@@ -2,6 +2,7 @@
#include "CoreMinimal.h"
#include "Engine/DataAsset.h"
#include "ZMMOJobTypes.h"
#include "ZMMOJobDataAsset.generated.h"
class UTexture2D;
@@ -39,11 +40,28 @@ public:
UPROPERTY(EditDefaultsOnly, BlueprintReadOnly, Category = "Zeus|Job")
FString TechnicalName;
/// Nome localizado exibido na UI (pt-BR no projeto base). Ex: "Aprendiz".
/// Nome localizado exibido na UI (pt-BR no projeto base). Ex: "Novato".
/// Pode usar FText Localization tables pra multi-idioma futuro.
UPROPERTY(EditDefaultsOnly, BlueprintReadOnly, Category = "Zeus|Job")
FText DisplayName;
/// Tier hierarquico — espelha `JobDef::tier` do server. UI usa pra agrupar
/// jobs (ex: lista de Vocacoes possiveis quando promovendo Novato).
UPROPERTY(EditDefaultsOnly, BlueprintReadOnly, Category = "Zeus|Job")
EZMMOJobTier Tier = EZMMOJobTier::Beginner;
/// ClassId do parent na arvore de promocao. 0 = sem parent (Novato eh root).
/// Espelha `JobDef::parentJobId` do server. UI usa pra filtrar destinos
/// possiveis ao mostrar painel de promocao.
UPROPERTY(EditDefaultsOnly, BlueprintReadOnly, Category = "Zeus|Job")
int32 ParentClassId = 0;
/// Lista de armas que o job pode equipar. Espelha `JobDef::allowedWeaponsMask`
/// do server (mas como TArray pra editor visual em vez de bitmask cru).
/// Inventory UI usa pra hint "nao pode equipar" + filtro de catalogo.
UPROPERTY(EditDefaultsOnly, BlueprintReadOnly, Category = "Zeus|Job")
TArray<EZMMOWeaponType> AllowedWeapons;
/// 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")

View File

@@ -0,0 +1,57 @@
#pragma once
#include "CoreMinimal.h"
#include "ZMMOJobTypes.generated.h"
/**
* Tier hierarquico de um job. Espelha `EJobTier` do server em
* `Server/.../JobsSystem/Public/JobTypes.hpp`. Valores TEM que bater
* byte-a-byte com a enum do server porque o snapshot pode carregar
* tier como uint8 no futuro.
*
* Renascer (RO pre-renewal): char no Tier 2 (Specialization) que cumpre
* condicao volta pra Tier 3 (RebornBeginner) com bonus permanente,
* abrindo path paralelo RebornVocation → Mastery (coroa final).
*/
UENUM(BlueprintType)
enum class EZMMOJobTier : uint8
{
Beginner = 0 UMETA(DisplayName = "Iniciante"),
Vocation = 1 UMETA(DisplayName = "Vocacao"),
Specialization = 2 UMETA(DisplayName = "Especializacao"),
RebornBeginner = 3 UMETA(DisplayName = "Iniciante Renascido"),
RebornVocation = 4 UMETA(DisplayName = "Vocacao Renascida"),
Mastery = 5 UMETA(DisplayName = "Mestria"),
};
/**
* Tipos de arma. Espelha `EWeaponType` do server. Usado em `AllowedWeapons`
* do DA pra UI (filtrar items equipaveis) + validacao server-side futura
* quando InventorySystem entrar.
*
* Ordem TEM que bater com server porque o bitmask `allowedWeaponsMask` no
* JobDef usa bit N = EWeaponType(N).
*/
UENUM(BlueprintType)
enum class EZMMOWeaponType : uint8
{
Fist = 0 UMETA(DisplayName = "Punho"),
Dagger = 1 UMETA(DisplayName = "Adaga"),
Sword1H = 2 UMETA(DisplayName = "Espada 1M"),
Sword2H = 3 UMETA(DisplayName = "Espada 2M"),
Spear1H = 4 UMETA(DisplayName = "Lanca 1M"),
Spear2H = 5 UMETA(DisplayName = "Lanca 2M"),
Axe1H = 6 UMETA(DisplayName = "Machado 1M"),
Axe2H = 7 UMETA(DisplayName = "Machado 2M"),
Mace1H = 8 UMETA(DisplayName = "Maca 1M"),
Mace2H = 9 UMETA(DisplayName = "Maca 2M"),
Staff1H = 10 UMETA(DisplayName = "Cajado 1M"),
Staff2H = 11 UMETA(DisplayName = "Cajado 2M"),
Bow = 12 UMETA(DisplayName = "Arco"),
Knuckle = 13 UMETA(DisplayName = "Soqueira"),
Musical = 14 UMETA(DisplayName = "Instrumento Musical"),
Whip = 15 UMETA(DisplayName = "Chicote"),
Book = 16 UMETA(DisplayName = "Livro"),
Katar = 17 UMETA(DisplayName = "Katar"),
Gun = 18 UMETA(DisplayName = "Arma de Fogo"),
};

View File

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

View File

@@ -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<UZMMOJobDataAsset*> 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<UZMMOJobDataAsset*> 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

View File

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