9 Commits

Author SHA1 Message Date
87d9ca4dae chore(ui): textura do panel gradient quadrado + ignore /AutoCreated
- Adiciona as 3 texturas que o refactor de panel referencia via
  EUIPanelTexture::Rectangle_Square_Gradient_01 (background gradient,
  outline e outline effect) em /UI/Themes/Default/Panel/.
- Ignora Content/AutoCreated/ no .gitignore: pasta de saida do
  ZeusUMGForge web converter, regenerada sob demanda, nao deve ser
  versionada.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-26 00:14:38 -03:00
037aed30ce refactor(ui/base): panel data-driven 3-brush + button via FButtonStyle
UIPanel_Base: substitui PanelType/Padding por bUseTheme + 3 chaves
(Background/Outline/OutlineEffectKey) lidas de FUIStylePanel. Modo manual
(bUseTheme=false) expoe 3 FSlateBrush diretos via FUIPanelBrushSet pra
override por instancia. Adiciona BindWidgetOptional OutlineEffect (3o
Border sobre Outline pra glow/embossing). Defaults NoDrawType (regra
Hyper: brush vazio nao pinta branco).

UIButton_Base: remove auto-config (Variants enum, Number/Icon/Size/
InputAction/Timer). Vira shell minimal com BindWidgetOptional + helpers
GetActiveUIStyle/GetActiveButtonStyle pra leitura de DT_UI_Styles.

UIStyleTokens: FUIStylePanel ganha PanelOutlineEffect map. FUIPanelBrushSet
vira 3 FSlateBrush diretos. FUIStyleButton refatora pra 7 sub-maps por
variante FName (ButtonStyle=FButtonStyle do Slate, ButtonStroke, ButtonFont,
Text/Icon colors). Tudo com defaults NoDrawType.

UIStyleTypes: adiciona enum EUIPanelTexture::Rectangle_Square_Gradient_01.

Content: UI_Panel_Master e UI_Button_Master_New consomem a API nova;
DT_UI_Styles atualizado com rows/keys novas. UIServerCard_Base ajustado
pro consumer da API nova.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-26 00:12:58 -03:00
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
54 changed files with 1089 additions and 1241 deletions

3
.gitignore vendored
View File

@@ -29,3 +29,6 @@ desktop.ini
*.log *.log
*.pdb *.pdb
.vscode/ .vscode/
# Hyper / ZeusUMGForge web converter outputs (assets gerados, não versionar)
Content/AutoCreated/

View File

@@ -9,7 +9,7 @@
> **Extensões da arquitetura** (documentos normativos paralelos, escopo específico): > **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_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 > **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 > de UI** (`UI/Themes/`), com tutorial §4.7, regra de "no hard ref a textura

View File

@@ -6,7 +6,7 @@
## Context ## 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: Este doc fixa:
1. O que persiste no MySQL (CharServer) vs o que vive em memória (WorldServer). 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 | | 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) | | 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 | | 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 | | Recalcular ATK/MATK/DEF/etc. | WorldServer (memória, toda hora) | NÃO persiste |
| Casting de skill | WorldServer | NÃO persiste (efêmero) | | Casting de skill | WorldServer | NÃO persiste (efêmero) |
| Resultado de skill (HP perdido, item dropado) | WorldServer | WorldServer → writeback | | Resultado de skill (HP perdido, item dropado) | WorldServer | WorldServer → writeback |
@@ -260,7 +260,7 @@ version: 1
jobs: jobs:
- id: 0 - id: 0
name: Novice # nome técnico (inglês, estável) name: Novice # nome técnico (inglês, estável)
display_name_ptbr: Aprendiz display_name_ptbr: Novato
parent_job: null parent_job: null
max_base_level: 99 max_base_level: 99
max_job_level: 10 max_job_level: 10
@@ -322,7 +322,7 @@ jobs:
## 5. Fluxos ## 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 } Cliente → C_CHAR_CREATE { name, world_id, class_id=NOVICE, appearance }
@@ -342,7 +342,7 @@ CharServer:
4. → S_CHAR_CREATE_OK 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) ### 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). 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): 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 ## 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`). - 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. - CharServer carrega `jobs.yml` no boot.
- `C_CHAR_CREATE` valida `class_id == NOVICE` e usa defaults do yml. - `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 ### 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.** 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`). 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. 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. 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. 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. 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:** **Notas:**
- `world_id` é nullable (sem chars em prod hoje — evita downtime futuro). Validação no service: novos chars devem ter `world_id`. - `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). - 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). - **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. - `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+) ### 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.

Binary file not shown.

View File

@@ -27,6 +27,7 @@ enum class EZMMOInGameUIState : uint8
None, ///< Antes do player local spawnar; UI in-game inativa. None, ///< Antes do player local spawnar; UI in-game inativa.
Playing, ///< HUD principal visivel (HP/SP/level/etc). Playing, ///< HUD principal visivel (HP/SP/level/etc).
StatusWindow, ///< Janela de atributos + botões de alocacao (Fase 4). 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. Inventory, ///< (futuro) Bag/equipamentos.
SkillTree, ///< (futuro) Arvore de skills. SkillTree, ///< (futuro) Arvore de skills.
EscapeMenu, ///< (futuro) Pausa, settings, logout. EscapeMenu, ///< (futuro) Pausa, settings, logout.

View File

@@ -3,6 +3,7 @@
#include "CoreMinimal.h" #include "CoreMinimal.h"
#include "Fonts/SlateFontInfo.h" #include "Fonts/SlateFontInfo.h"
#include "Styling/SlateBrush.h" #include "Styling/SlateBrush.h"
#include "Styling/SlateTypes.h"
#include "Layout/Margin.h" #include "Layout/Margin.h"
#include "Templates/SubclassOf.h" #include "Templates/SubclassOf.h"
#include "Engine/Texture2D.h" #include "Engine/Texture2D.h"
@@ -195,117 +196,13 @@ struct ZMMO_API FUITextStyle
}; };
// ===================================================================== // =====================================================================
// FUIStyleButton — padrão multi-map (Hyper-style). 3 sub-mapas com a MESMA // FUIStyleButton — padrão Hyper "Buttons": 7 sub-mapas independentes
// chave (nome da variante: "Primary", "Secondary", "Danger", "Ghost", ou // indexados pela MESMA chave FName de variante (autor define livremente:
// customizada). UUIButton_Base.Variant é FName e busca em cada sub-mapa. // "Default", "Square", "Topbar", "Home_Main_Menu", ...). UUIButton_Base.Variant
// O struct FUIStyleButtonVariant (composto, FLAT) permanece com a forma // é FName e busca o pedaço necessário em cada sub-mapa. ButtonStyle traz
// antiga — assim o hook BP_ApplyUIStyle dos WBP_Master continua funcional. // FButtonStyle do Slate completo (Normal/Hovered/Pressed/Disabled como
// FSlateBrush, Foregrounds, Padding, Sounds).
// ===================================================================== // =====================================================================
USTRUCT(BlueprintType)
struct ZMMO_API FUIStyleButtonBackground
{
GENERATED_BODY()
UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "UI Style|Button")
FLinearColor BgNormal = FLinearColor::Transparent;
UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "UI Style|Button")
FLinearColor BgHover = FLinearColor::Transparent;
UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "UI Style|Button")
FLinearColor BgPressed = FLinearColor::Transparent;
UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "UI Style|Button")
FLinearColor BgDisabled = FLinearColor(0.f, 0.f, 0.f, 0.42f);
/** Textura opcional como background (Hyper-style). */
UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "UI Style|Button")
TObjectPtr<UTexture2D> BackgroundTexture = nullptr;
UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "UI Style|Button")
FMargin BackgroundMargin = FMargin(8.f);
};
USTRUCT(BlueprintType)
struct ZMMO_API FUIStyleButtonStroke
{
GENERATED_BODY()
UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "UI Style|Button")
FLinearColor BorderNormal = FLinearColor::Transparent;
UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "UI Style|Button")
FLinearColor BorderHover = FLinearColor::Transparent;
UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "UI Style|Button", meta = (ClampMin = "0"))
float BorderWidth = 1.f;
UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "UI Style|Button", meta = (ClampMin = "0"))
float CornerRadius = 8.f;
};
USTRUCT(BlueprintType)
struct ZMMO_API FUIStyleButtonFont
{
GENERATED_BODY()
UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "UI Style|Button")
FLinearColor TextColor = FLinearColor::White;
/** CommonTextStyle define fonte/tamanho/peso; TextColor sobrescreve a cor. */
UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "UI Style|Button")
TSubclassOf<UCommonTextStyle> TextStyle;
};
/**
* View FLAT composta da variante de botão (Background + Stroke + Font).
* Mantida com a forma antiga (BgNormal/BgHover/.../TextStyle no topo) para
* NÃO QUEBRAR o hook BP_ApplyUIStyle nos WBP_Master existentes. É construída
* pelo UUIButton_Base::ResolveVariant a partir dos 3 sub-mapas do tema.
*/
USTRUCT(BlueprintType)
struct ZMMO_API FUIStyleButtonVariant
{
GENERATED_BODY()
UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "UI Style|Button")
FLinearColor BgNormal = FLinearColor::Transparent;
UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "UI Style|Button")
FLinearColor BgHover = FLinearColor::Transparent;
UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "UI Style|Button")
FLinearColor BgPressed = FLinearColor::Transparent;
UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "UI Style|Button")
FLinearColor BgDisabled = FLinearColor(0.f, 0.f, 0.f, 0.42f);
UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "UI Style|Button")
FLinearColor BorderNormal = FLinearColor::Transparent;
UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "UI Style|Button")
FLinearColor BorderHover = FLinearColor::Transparent;
UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "UI Style|Button")
FLinearColor TextColor = FLinearColor::White;
UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "UI Style|Button")
TSubclassOf<UCommonTextStyle> TextStyle;
// Campos extras vindos das sub-categorias (background texture + stroke):
UPROPERTY(BlueprintReadOnly, Category = "UI Style|Button")
TObjectPtr<UTexture2D> BackgroundTexture = nullptr;
UPROPERTY(BlueprintReadOnly, Category = "UI Style|Button")
FMargin BackgroundMargin = FMargin(8.f);
UPROPERTY(BlueprintReadOnly, Category = "UI Style|Button")
float BorderWidth = 1.f;
UPROPERTY(BlueprintReadOnly, Category = "UI Style|Button")
float CornerRadius = 8.f;
};
USTRUCT(BlueprintType) USTRUCT(BlueprintType)
struct ZMMO_API FUIStyleButton struct ZMMO_API FUIStyleButton
{ {
@@ -313,163 +210,103 @@ struct ZMMO_API FUIStyleButton
FUIStyleButton() FUIStyleButton()
{ {
// ---- Backgrounds (Primary/Secondary/Danger/Ghost) ---- // Defaults "no draw" pra não pintar branco quando o brush estiver vazio
FUIStyleButtonBackground PriBg; // (regra Hyper — ver feedback_slatebrush_empty_white). Autor preenche
PriBg.BgNormal = FLinearColor(FColor(201, 167, 90)); // gold // no DT_UI_Styles.
PriBg.BgHover = FLinearColor(FColor(231, 200, 115)); // gold-hi FButtonStyle DefaultStyle;
PriBg.BgPressed = FLinearColor(FColor(201, 167, 90)); DefaultStyle.Normal.DrawAs = ESlateBrushDrawType::NoDrawType;
PriBg.BgDisabled = FLinearColor(FColor(201, 167, 90, 107)); DefaultStyle.Hovered.DrawAs = ESlateBrushDrawType::NoDrawType;
Backgrounds.Add(TEXT("Primary"), PriBg); DefaultStyle.Pressed.DrawAs = ESlateBrushDrawType::NoDrawType;
DefaultStyle.Disabled.DrawAs = ESlateBrushDrawType::NoDrawType;
ButtonStyle.Add(TEXT("Default"), DefaultStyle);
FUIStyleButtonBackground SecBg; FSlateBrush DefaultStroke;
SecBg.BgNormal = FLinearColor(FColor(17, 24, 42)); // panel-2 DefaultStroke.DrawAs = ESlateBrushDrawType::NoDrawType;
SecBg.BgHover = FLinearColor(FColor(22, 32, 58)); ButtonStroke.Add(TEXT("Default"), DefaultStroke);
SecBg.BgPressed = FLinearColor(FColor(17, 24, 42));
Backgrounds.Add(TEXT("Secondary"), SecBg);
FUIStyleButtonBackground DngBg; ButtonFont.Add(TEXT("Default"), FSlateFontInfo());
DngBg.BgNormal = FLinearColor(FColor(217, 92, 92, 36));
DngBg.BgHover = FLinearColor(FColor(217, 92, 92, 66));
DngBg.BgPressed = FLinearColor(FColor(217, 92, 92, 36));
Backgrounds.Add(TEXT("Danger"), DngBg);
FUIStyleButtonBackground GhoBg; ButtonTextColorRegular.Add(TEXT("Default"), FLinearColor::White);
GhoBg.BgNormal = FLinearColor::Transparent; ButtonTextColorHovered.Add(TEXT("Default"), FLinearColor::White);
GhoBg.BgHover = FLinearColor(FColor(22, 32, 58, 128)); ButtonTextColorSelected.Add(TEXT("Default"), FLinearColor::White);
GhoBg.BgPressed = FLinearColor::Transparent;
Backgrounds.Add(TEXT("Ghost"), GhoBg);
// ---- Strokes ---- ButtonIconColor.Add(TEXT("Default"), FLinearColor::White);
FUIStyleButtonStroke PriS;
PriS.BorderNormal = FLinearColor(FColor(201, 167, 90));
PriS.BorderHover = FLinearColor(FColor(231, 200, 115));
Strokes.Add(TEXT("Primary"), PriS);
FUIStyleButtonStroke SecS;
SecS.BorderNormal = FLinearColor(FColor(58, 75, 120));
SecS.BorderHover = FLinearColor(FColor(78, 163, 255));
Strokes.Add(TEXT("Secondary"), SecS);
FUIStyleButtonStroke DngS;
DngS.BorderNormal = FLinearColor(FColor(217, 92, 92));
DngS.BorderHover = FLinearColor(FColor(217, 92, 92));
Strokes.Add(TEXT("Danger"), DngS);
FUIStyleButtonStroke GhoS;
GhoS.BorderNormal = FLinearColor(FColor(58, 75, 120, 140));
GhoS.BorderHover = FLinearColor(FColor(78, 163, 255));
Strokes.Add(TEXT("Ghost"), GhoS);
// ---- Fonts ----
FUIStyleButtonFont PriF; PriF.TextColor = FLinearColor(FColor(10, 10, 18));
Fonts.Add(TEXT("Primary"), PriF);
FUIStyleButtonFont SecF; SecF.TextColor = FLinearColor(FColor(244, 240, 230));
Fonts.Add(TEXT("Secondary"), SecF);
FUIStyleButtonFont DngF; DngF.TextColor = FLinearColor(FColor(240, 182, 182));
Fonts.Add(TEXT("Danger"), DngF);
FUIStyleButtonFont GhoF; GhoF.TextColor = FLinearColor(FColor(174, 182, 200));
Fonts.Add(TEXT("Ghost"), GhoF);
} }
/** Cores/textura de fundo por variante (chave FName). */ /** FButtonStyle do Slate por variante: Normal/Hovered/Pressed/Disabled
* como FSlateBrush completo + Foregrounds + Padding + Sounds. */
UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "UI Style|Button") UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "UI Style|Button")
TMap<FName, FUIStyleButtonBackground> Backgrounds; TMap<FName, FButtonStyle> ButtonStyle;
/** Borda (cor, espessura, raio) por variante. */ /** Stroke/contorno como brush independente (texturizável). Hoje o
* UUIButton_Base não consome este campo automaticamente — quem precisa
* de stroke decora o FSlateBrush dentro de ButtonStyle.Normal via
* OutlineSettings. Reservado pra evolução com 2 Borders empilhados. */
UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "UI Style|Button") UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "UI Style|Button")
TMap<FName, FUIStyleButtonStroke> Strokes; TMap<FName, FSlateBrush> ButtonStroke;
/** Tipografia (cor de texto, CommonTextStyle) por variante. */ /** Fonte do texto por variante. */
UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "UI Style|Button") UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "UI Style|Button")
TMap<FName, FUIStyleButtonFont> Fonts; TMap<FName, FSlateFontInfo> ButtonFont;
// Globais comuns às variantes:
UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "UI Style|Button")
FMargin Padding = FMargin(22.f, 12.f);
UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "UI Style|Button") UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "UI Style|Button")
float MinHeight = 48.f; TMap<FName, FLinearColor> ButtonTextColorRegular;
UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "UI Style|Button") UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "UI Style|Button")
float HoverScale = 1.02f; TMap<FName, FLinearColor> ButtonTextColorHovered;
UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "UI Style|Button") UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "UI Style|Button")
float PressedScale = 0.98f; TMap<FName, FLinearColor> ButtonTextColorSelected;
/** Tamanho por silhueta (Square/Rectangle/Topbar/Header). */
UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "UI Style|Button") UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "UI Style|Button")
TMap<EUIButtonShape, FVector2D> ShapeSizes; TMap<FName, FLinearColor> ButtonIconColor;
}; };
/** /**
* Espelha 1:1 o Struct_UI_Style_Panels do Hyper: só os dois mapas de brush * Override manual por instância (modo bUseTheme = false). 3 brushes diretos
* (fundo + contorno) indexados por EUIPanelTexture. É o que o UI_Panel_Master * pros 3 Borders do painel — designer edita o brush bruto no Details.
* do Hyper expõe editável na instância (Details). EditAnywhere/ReadWrite para * Default = NoDrawType pra não pintar branco quando vazio (regra Hyper).
* o designer ajustar brush por widget; o tema (DT) só faz seed/fallback.
*/ */
USTRUCT(BlueprintType) USTRUCT(BlueprintType)
struct ZMMO_API FUIPanelBrushSet struct ZMMO_API FUIPanelBrushSet
{ {
GENERATED_BODY() GENERATED_BODY()
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Panel|Texture") FUIPanelBrushSet()
TMap<EUIPanelTexture, FSlateBrush> PanelBackground; {
PanelBackground.DrawAs = ESlateBrushDrawType::NoDrawType;
PanelOutline.DrawAs = ESlateBrushDrawType::NoDrawType;
PanelOutlineEffect.DrawAs = ESlateBrushDrawType::NoDrawType;
}
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Panel|Texture") UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Panel|Brush")
TMap<EUIPanelTexture, FSlateBrush> PanelOutline; FSlateBrush PanelBackground;
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Panel|Brush")
FSlateBrush PanelOutline;
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Panel|Brush")
FSlateBrush PanelOutlineEffect;
}; };
/**
* Espelha 1:1 o Struct_UI_Style_Panels do Hyper, agora com 3 brushes por
* EUIPanelTexture: fundo, contorno e efeito sobre o contorno. Tudo
* data-driven via brush no DT_UI_Styles.
*/
USTRUCT(BlueprintType) USTRUCT(BlueprintType)
struct ZMMO_API FUIStylePanel struct ZMMO_API FUIStylePanel
{ {
GENERATED_BODY() GENERATED_BODY()
UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "UI Style|Panel")
FLinearColor Bg = FLinearColor(FColor(12, 18, 34, 235)); // Panel
UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "UI Style|Panel")
FLinearColor BgRaised = FLinearColor(FColor(17, 24, 42)); // Panel2
UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "UI Style|Panel")
FLinearColor BgSunken = FLinearColor(FColor(14, 20, 36)); // Panel3
UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "UI Style|Panel")
FLinearColor BorderColor = FLinearColor(FColor(58, 75, 120));
UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "UI Style|Panel")
FLinearColor BorderSoftColor = FLinearColor(FColor(58, 75, 120, 140));
UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "UI Style|Panel")
float CornerRadius = 12.f;
UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "UI Style|Panel")
float CornerRadiusSmall = 10.f;
UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "UI Style|Panel")
float BorderWidth = 1.f;
UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "UI Style|Panel")
FMargin Padding = FMargin(30.f);
UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "UI Style|Panel")
FMargin PaddingSmall = FMargin(20.f);
UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "UI Style|Panel")
FLinearColor CardSelectedBorder = FLinearColor(FColor(201, 167, 90)); // Accent
UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "UI Style|Panel")
FLinearColor CardSelectedBg = FLinearColor(FColor(24, 32, 58));
/**
* Modo texturizado (espelha Struct_UI_Style_Panels do Hyper): brush por
* EUIPanelTexture. PanelBackground = fundo; PanelOutline = contorno.
* Se vazio p/ a chave, o painel usa o modo cor (RoundedBox) acima.
*/
UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "UI Style|Panel|Texture") UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "UI Style|Panel|Texture")
TMap<EUIPanelTexture, FSlateBrush> PanelBackground; TMap<EUIPanelTexture, FSlateBrush> PanelBackground;
UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "UI Style|Panel|Texture") UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "UI Style|Panel|Texture")
TMap<EUIPanelTexture, FSlateBrush> PanelOutline; TMap<EUIPanelTexture, FSlateBrush> PanelOutline;
UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "UI Style|Panel|Texture")
TMap<EUIPanelTexture, FSlateBrush> PanelOutlineEffect;
}; };
USTRUCT(BlueprintType) USTRUCT(BlueprintType)

View File

@@ -66,6 +66,7 @@ enum class EUIPanelTexture : uint8
Rectangle_Vertical_03 UMETA(DisplayName = "Rectangle Vertical 03"), Rectangle_Vertical_03 UMETA(DisplayName = "Rectangle Vertical 03"),
Rectangle_Vertical_04 UMETA(DisplayName = "Rectangle Vertical 04"), Rectangle_Vertical_04 UMETA(DisplayName = "Rectangle Vertical 04"),
Rectangle_Square_01 UMETA(DisplayName = "Rectangle Square 01"), Rectangle_Square_01 UMETA(DisplayName = "Rectangle Square 01"),
Rectangle_Square_Gradient_01 UMETA(DisplayName = "Rectangle Square Gradient 01"),
Vertical_Footer_01 UMETA(DisplayName = "Vertical Footer 01") Vertical_Footer_01 UMETA(DisplayName = "Vertical Footer 01")
}; };

View File

@@ -104,6 +104,14 @@ void AZMMOPlayerController::SetupInputComponent()
this, &AZMMOPlayerController::ToggleStatusWindow); this, &AZMMOPlayerController::ToggleStatusWindow);
Binding.bConsumeInput = true; Binding.bConsumeInput = true;
Binding.bExecuteWhenPaused = true; // dispara mesmo com StatusWindow aberto (CommonUI menu mode) 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 bool AZMMOPlayerController::ShouldUseTouchControls() const
{ {
return SVirtualJoystick::ShouldDisplayTouchInterface() || bForceTouchControls; return SVirtualJoystick::ShouldDisplayTouchInterface() || bForceTouchControls;

View File

@@ -50,4 +50,7 @@ protected:
/** Hotkey Alt+A → toggle StatusWindow via UUIInGameFlowSubsystem. */ /** Hotkey Alt+A → toggle StatusWindow via UUIInGameFlowSubsystem. */
void ToggleStatusWindow(); 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->OnConnected.AddDynamic(this, &UZMMOGameInstance::HandleZeusConnected);
Zeus->OnConnectionFailed.AddDynamic(this, &UZMMOGameInstance::HandleZeusConnectionFailed); Zeus->OnConnectionFailed.AddDynamic(this, &UZMMOGameInstance::HandleZeusConnectionFailed);
Zeus->OnDisconnected.AddDynamic(this, &UZMMOGameInstance::HandleZeusDisconnected); 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) void UZMMOGameInstance::UnbindZeusDelegates(UZeusNetworkSubsystem* Zeus)
@@ -69,7 +73,7 @@ void UZMMOGameInstance::UnbindZeusDelegates(UZeusNetworkSubsystem* Zeus)
Zeus->OnConnected.RemoveDynamic(this, &UZMMOGameInstance::HandleZeusConnected); Zeus->OnConnected.RemoveDynamic(this, &UZMMOGameInstance::HandleZeusConnected);
Zeus->OnConnectionFailed.RemoveDynamic(this, &UZMMOGameInstance::HandleZeusConnectionFailed); Zeus->OnConnectionFailed.RemoveDynamic(this, &UZMMOGameInstance::HandleZeusConnectionFailed);
Zeus->OnDisconnected.RemoveDynamic(this, &UZMMOGameInstance::HandleZeusDisconnected); Zeus->OnDisconnected.RemoveDynamic(this, &UZMMOGameInstance::HandleZeusDisconnected);
Zeus->OnNetworkLog.RemoveDynamic(this, &UZMMOGameInstance::HandleZeusNetworkLog); // OnNetworkLog: nao foi assinado (ver BindZeusDelegates) — nada a remover.
} }
void UZMMOGameInstance::HandleZeusConnected() void UZMMOGameInstance::HandleZeusConnected()

View File

@@ -9,12 +9,11 @@
void UUIServerCard_Base::NativePreConstruct() void UUIServerCard_Base::NativePreConstruct()
{ {
Super::NativePreConstruct(); Super::NativePreConstruct();
// Reaplica tema do botao antes de qualquer SetIsEnabled — garante que o // TODO(ui-system): reativar quando UUIButton_Base voltar a ter RefreshUIStyle.
// background da variante "Primary" pinta tanto em Normal quanto Disabled. // if (Btn_Enter)
if (Btn_Enter) // {
{ // Btn_Enter->RefreshUIStyle();
Btn_Enter->RefreshUIStyle(); // }
}
} }
void UUIServerCard_Base::NativeConstruct() void UUIServerCard_Base::NativeConstruct()
@@ -25,10 +24,11 @@ void UUIServerCard_Base::NativeConstruct()
Btn_Enter->OnClicked().AddUObject(this, &UUIServerCard_Base::HandleEnterClicked); Btn_Enter->OnClicked().AddUObject(this, &UUIServerCard_Base::HandleEnterClicked);
bBound = true; bBound = true;
} }
if (Btn_Enter) // TODO(ui-system): reativar quando UUIButton_Base voltar a ter RefreshUIStyle.
{ // if (Btn_Enter)
Btn_Enter->RefreshUIStyle(); // {
} // Btn_Enter->RefreshUIStyle();
// }
} }
void UUIServerCard_Base::NativeDestruct() void UUIServerCard_Base::NativeDestruct()
@@ -69,10 +69,11 @@ void UUIServerCard_Base::SetFromEntry(const FZMMOWorldEntry& InEntry)
// o usuario pode entrar no Lobby pra ver a lista de personagens, criar/ // o usuario pode entrar no Lobby pra ver a lista de personagens, criar/
// deletar. O gate de "entrar no mundo" fica no proprio Lobby (botao // deletar. O gate de "entrar no mundo" fica no proprio Lobby (botao
// "Entrar no Servidor"), que checa World.State antes do handoff. // "Entrar no Servidor"), que checa World.State antes do handoff.
if (Btn_Enter) // TODO(ui-system): reativar quando UUIButton_Base voltar a ter RefreshUIStyle.
{ // if (Btn_Enter)
Btn_Enter->RefreshUIStyle(); // {
} // Btn_Enter->RefreshUIStyle();
// }
} }
void UUIServerCard_Base::HandleEnterClicked() void UUIServerCard_Base::HandleEnterClicked()

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

@@ -1,434 +1,28 @@
#include "UIButton_Base.h" #include "UIButton_Base.h"
#include "ZMMOThemeSubsystem.h" #include "UI/UIStyleRow.h"
#include "Engine/GameInstance.h" #include "UI/UIStyleTokens.h"
#include "Engine/World.h" #include "Engine/DataTable.h"
#include "TimerManager.h"
#include "Components/Border.h"
#include "Styling/SlateBrush.h"
#include "Components/TextBlock.h"
#include "Components/Image.h"
#include "Components/SizeBox.h"
#include "Components/ScaleBox.h"
#include "Components/OverlaySlot.h"
#include "CommonTextBlock.h"
#include "CommonActionWidget.h"
namespace const FUIStyle* UUIButton_Base::GetActiveUIStyle(FName ThemeId) const
{ {
/** Resolve o FUIStyle ativo; fallback p/ defaults (preview de designer). */ const UDataTable* DT = LoadObject<UDataTable>(nullptr,
const FUIStyle& ResolveActiveStyle(const UUserWidget* Widget, const FUIStyle& Fallback) TEXT("/Game/ZMMO/Data/UI/DT_UI_Styles.DT_UI_Styles"));
if (!DT)
{ {
if (Widget) return nullptr;
{
if (const UGameInstance* GI = Widget->GetGameInstance())
{
if (const UZMMOThemeSubsystem* Theme = GI->GetSubsystem<UZMMOThemeSubsystem>())
{
return Theme->GetActiveUIStyle();
}
}
}
return Fallback; // sem GameInstance (design-time): Aurora Arcana default
} }
if (const FUIStyleRow* Row = DT->FindRow<FUIStyleRow>(
ThemeId, TEXT("UUIButton_Base::GetActiveUIStyle"), false))
{
return &Row->Style;
}
return nullptr;
} }
FUIStyleButtonVariant UUIButton_Base::ResolveVariant(const FUIStyleButton& Button) const const FUIStyleButton* UUIButton_Base::GetActiveButtonStyle(FName ThemeId) const
{ {
// Compõe FLAT a partir dos 3 sub-mapas do tema (Backgrounds/Strokes/Fonts). const FUIStyle* UIStyle = GetActiveUIStyle(ThemeId);
// Fallback inline → Defaults (constructor de FUIStyleButton popula return UIStyle ? &UIStyle->Button : nullptr;
// Primary/Secondary/Danger/Ghost com defaults Aurora Arcana — GC-safe).
static const FUIStyleButton Defaults;
FUIStyleButtonVariant V;
const FUIStyleButtonBackground* BG = Button.Backgrounds.Find(Variant);
if (!BG) { BG = Defaults.Backgrounds.Find(Variant); }
if (!BG) { BG = Defaults.Backgrounds.Find(TEXT("Primary")); }
if (BG)
{
V.BgNormal = BG->BgNormal;
V.BgHover = BG->BgHover;
V.BgPressed = BG->BgPressed;
V.BgDisabled = BG->BgDisabled;
V.BackgroundTexture = BG->BackgroundTexture;
V.BackgroundMargin = BG->BackgroundMargin;
}
const FUIStyleButtonStroke* ST = Button.Strokes.Find(Variant);
if (!ST) { ST = Defaults.Strokes.Find(Variant); }
if (!ST) { ST = Defaults.Strokes.Find(TEXT("Primary")); }
if (ST)
{
V.BorderNormal = ST->BorderNormal;
V.BorderHover = ST->BorderHover;
V.BorderWidth = ST->BorderWidth;
V.CornerRadius = ST->CornerRadius;
}
const FUIStyleButtonFont* F = Button.Fonts.Find(Variant);
if (!F) { F = Defaults.Fonts.Find(Variant); }
if (!F) { F = Defaults.Fonts.Find(TEXT("Primary")); }
if (F)
{
V.TextColor = F->TextColor;
V.TextStyle = F->TextStyle;
}
return V;
}
TArray<FString> UUIButton_Base::GetVariantOptions() const
{
TArray<FString> Options;
for (const TCHAR* N : { TEXT("Primary"), TEXT("Secondary"), TEXT("Danger"), TEXT("Ghost") })
{
Options.Add(N);
}
if (const UDataTable* DT = LoadObject<UDataTable>(nullptr,
TEXT("/Game/ZMMO/Data/UI/DT_UI_Styles.DT_UI_Styles")))
{
if (const FUIStyleRow* Row = DT->FindRow<FUIStyleRow>(
FName(TEXT("Default")), TEXT("UIButtonVariantOptions"), false))
{
for (const TPair<FName, FUIStyleButtonBackground>& P : Row->Style.Button.Backgrounds) { Options.AddUnique(P.Key.ToString()); }
for (const TPair<FName, FUIStyleButtonStroke>& P : Row->Style.Button.Strokes) { Options.AddUnique(P.Key.ToString()); }
for (const TPair<FName, FUIStyleButtonFont>& P : Row->Style.Button.Fonts) { Options.AddUnique(P.Key.ToString()); }
}
}
return Options;
}
// ---- Hyper "Set Button Text" ----
void UUIButton_Base::SetButtonText(FText InText)
{
if (!ButtonText)
{
return;
}
const FText Final = bUppercaseText
? FText::FromString(InText.ToString().ToUpper())
: InText;
ButtonText->SetText(Final);
}
// ---- Hyper "Set Number Text" ----
void UUIButton_Base::SetNumberText()
{
if (!Number_Text)
{
return;
}
if (bShowNumberText)
{
Number_Text->SetText(FText::AsNumber(NumberTextAmount));
Number_Text->SetVisibility(ESlateVisibility::Visible);
}
else
{
Number_Text->SetVisibility(ESlateVisibility::Hidden);
}
}
// ---- Hyper "Set Icon" ----
void UUIButton_Base::SetIcon()
{
if (bUseIconInsteadOfText)
{
if (Image_Icon)
{
if (IconImage)
{
Image_Icon->SetBrushResourceObject(IconImage);
}
Image_Icon->SetVisibility(ESlateVisibility::Visible);
}
if (ButtonText)
{
ButtonText->SetVisibility(ESlateVisibility::Collapsed);
}
}
else
{
if (Image_Icon)
{
Image_Icon->SetVisibility(ESlateVisibility::Collapsed);
}
if (ButtonText)
{
ButtonText->SetVisibility(ESlateVisibility::Visible);
}
}
}
// ---- Hyper "Set Sizebox Master Dimmensions" ----
void UUIButton_Base::ApplySizeBox()
{
if (bDoNotOverrideWithSizebox || !SizeBox_Master)
{
return;
}
SizeBox_Master->SetHeightOverride(InHeightOverride);
SizeBox_Master->SetWidthOverride(InWidthOverride);
}
// ---- Hyper "Set InputAction Style" ----
void UUIButton_Base::ApplyInputActionStyle()
{
// InputActionWidget é privado em UCommonButtonBase (bind interno). Pega por
// nome — o widget do WBP chama-se "InputActionWidget" (bind do CommonUI).
UCommonActionWidget* IAW = Cast<UCommonActionWidget>(
GetWidgetFromName(TEXT("InputActionWidget")));
if (!IAW)
{
return;
}
if (bShowInputActionWidget)
{
IAW->SetVisibility(ESlateVisibility::Visible);
IAW->SetIsEnabled(true);
}
else
{
IAW->SetVisibility(ESlateVisibility::Collapsed);
IAW->SetIsEnabled(false);
}
if (UOverlaySlot* OS = Cast<UOverlaySlot>(IAW->Slot))
{
OS->SetPadding(InputPadding);
OS->SetHorizontalAlignment(InputHorzAlignment);
OS->SetVerticalAlignment(InputVertAlignment);
}
}
// ---- Hyper "Initialize Button" (orquestrador) ----
void UUIButton_Base::InitializeButton()
{
SetButtonText(ButtonTextValue);
SetNumberText();
SetIcon();
ApplySizeBox();
RefreshUIStyle();
}
// ---- Hyper "Update Button Style" / "Set Brush" → nossa camada FUIStyle ----
void UUIButton_Base::RefreshUIStyle()
{
const FUIStyle Fallback; // defaults do struct = paleta Aurora Arcana
const FUIStyle& ActiveStyle = ResolveActiveStyle(this, Fallback);
const FUIStyleButtonVariant& VariantStyle = ResolveVariant(ActiveStyle.Button);
// Fundo + borda no estado Normal (hover/pressed/disabled via NativeOn*).
ApplyVisualState(EUIButtonVisual::Normal);
if (ButtonText)
{
// Tipografia: CommonTextStyle da variante (padrão Hyper/CommonUI).
if (VariantStyle.TextStyle)
{
ButtonText->SetStyle(VariantStyle.TextStyle);
}
// Cor: FUIStyle manda na cor por tema (sobrepõe o CommonTextStyle).
ButtonText->SetColorAndOpacity(FSlateColor(VariantStyle.TextColor));
// Alinhamento L/C/R dentro da largura do botão.
ButtonText->SetJustification(TextAlign);
}
// Texto adapta ao tamanho do botão (ScaleToFit) ou usa tamanho fixo.
if (TextScaleBox)
{
TextScaleBox->SetStretch(bAutoSizeText ? EStretch::ScaleToFit : EStretch::None);
// Posiciona o bloco de texto L/C/R dentro do botão (slot do Overlay).
if (UOverlaySlot* OS = Cast<UOverlaySlot>(TextScaleBox->Slot))
{
EHorizontalAlignment H = HAlign_Center;
switch (TextAlign)
{
case ETextJustify::Left: H = HAlign_Left; break;
case ETextJustify::Right: H = HAlign_Right; break;
default: H = HAlign_Center; break;
}
OS->SetHorizontalAlignment(H);
OS->SetVerticalAlignment(VAlign_Center);
}
}
BP_ApplyUIStyle(ActiveStyle.Button, VariantStyle);
}
void UUIButton_Base::ApplyVisualState(EUIButtonVisual State)
{
if (!Background)
{
return;
}
const FUIStyle Fallback;
const FUIStyle& AS = ResolveActiveStyle(this, Fallback);
const FUIStyleButtonVariant& V = ResolveVariant(AS.Button);
FLinearColor Fill, Outline;
switch (State)
{
case EUIButtonVisual::Hovered: Fill = V.BgHover; Outline = V.BorderHover; break;
case EUIButtonVisual::Pressed: Fill = V.BgPressed; Outline = V.BorderHover; break;
case EUIButtonVisual::Disabled: Fill = V.BgDisabled; Outline = V.BorderNormal; break;
default: Fill = V.BgNormal; Outline = V.BorderNormal; break;
}
FSlateBrush Brush = Background->Background;
Brush.TintColor = FSlateColor(Fill);
if (bRoundedBackground)
{
// CornerRadius/BorderWidth agora vêm da variante composta (Stroke).
const float R = V.CornerRadius;
Brush.DrawAs = ESlateBrushDrawType::RoundedBox;
Brush.OutlineSettings = FSlateBrushOutlineSettings(
FVector4(R, R, R, R), FSlateColor(Outline), V.BorderWidth);
Brush.OutlineSettings.RoundingType = ESlateBrushRoundingType::FixedRadius;
}
else
{
Brush.DrawAs = ESlateBrushDrawType::Box;
Brush.OutlineSettings = FSlateBrushOutlineSettings(
FVector4(0, 0, 0, 0), FSlateColor(Outline), V.BorderWidth);
}
Background->SetBrush(Brush);
}
void UUIButton_Base::NativeOnHovered()
{
Super::NativeOnHovered();
ApplyVisualState(EUIButtonVisual::Hovered);
}
void UUIButton_Base::NativeOnUnhovered()
{
Super::NativeOnUnhovered();
ApplyVisualState(EUIButtonVisual::Normal);
}
void UUIButton_Base::NativeOnPressed()
{
Super::NativeOnPressed();
ApplyVisualState(EUIButtonVisual::Pressed);
}
void UUIButton_Base::NativeOnReleased()
{
Super::NativeOnReleased();
ApplyVisualState(IsHovered() ? EUIButtonVisual::Hovered : EUIButtonVisual::Normal);
}
void UUIButton_Base::NativeOnEnabled()
{
Super::NativeOnEnabled();
ApplyVisualState(EUIButtonVisual::Normal);
}
void UUIButton_Base::NativeOnDisabled()
{
Super::NativeOnDisabled();
ApplyVisualState(EUIButtonVisual::Disabled);
}
void UUIButton_Base::NativePreConstruct()
{
Super::NativePreConstruct();
InitializeButton();
ApplyInputActionStyle();
}
void UUIButton_Base::NativeConstruct()
{
Super::NativeConstruct();
if (!bThemeBound)
{
if (const UGameInstance* GI = GetGameInstance())
{
if (UZMMOThemeSubsystem* Theme = GI->GetSubsystem<UZMMOThemeSubsystem>())
{
Theme->OnThemeChanged.AddDynamic(this, &UUIButton_Base::HandleThemeChanged);
bThemeBound = true;
}
}
}
InitializeButton();
ApplyInputActionStyle();
if (bDeactivatedOnConstructWithTimeout && Timeout > 0.f)
{
StartDeactivateTimeout();
}
}
void UUIButton_Base::NativeDestruct()
{
if (UWorld* World = GetWorld())
{
World->GetTimerManager().ClearTimer(InactivityTimer);
World->GetTimerManager().ClearTimer(TimerTickHandle);
}
if (bThemeBound)
{
if (const UGameInstance* GI = GetGameInstance())
{
if (UZMMOThemeSubsystem* Theme = GI->GetSubsystem<UZMMOThemeSubsystem>())
{
Theme->OnThemeChanged.RemoveDynamic(this, &UUIButton_Base::HandleThemeChanged);
}
}
bThemeBound = false;
}
Super::NativeDestruct();
}
// ---- Hyper "Deactivate Button On Timeout" ----
void UUIButton_Base::StartDeactivateTimeout()
{
UWorld* World = GetWorld();
if (!World)
{
return;
}
SetIsEnabled(false);
TimeRemaining = Timeout;
World->GetTimerManager().SetTimer(
InactivityTimer, this, &UUIButton_Base::HandleTimeoutFinished, Timeout, false);
if (TimeUpdateFrequency > 0.f)
{
World->GetTimerManager().SetTimer(
TimerTickHandle, this, &UUIButton_Base::HandleTimerTick,
TimeUpdateFrequency, true);
}
}
void UUIButton_Base::HandleTimeoutFinished()
{
if (UWorld* World = GetWorld())
{
World->GetTimerManager().ClearTimer(TimerTickHandle);
}
TimeRemaining = 0.f;
SetIsEnabled(true);
OnButtonActivatedByTimeout.Broadcast();
}
void UUIButton_Base::HandleTimerTick()
{
TimeRemaining = FMath::Max(0.f, TimeRemaining - TimeUpdateFrequency);
OnButtonTimerUpdated.Broadcast(TimeRemaining);
}
void UUIButton_Base::HandleThemeChanged(FName /*NewThemeId*/)
{
RefreshUIStyle();
} }

View File

@@ -2,220 +2,55 @@
#include "CoreMinimal.h" #include "CoreMinimal.h"
#include "CommonButtonBase.h" #include "CommonButtonBase.h"
#include "Layout/Margin.h"
#include "Types/SlateEnums.h"
#include "UI/UIStyleTypes.h"
#include "UI/UIStyleTokens.h"
#include "UIButton_Base.generated.h" #include "UIButton_Base.generated.h"
class UBorder;
class UTextBlock;
class UImage;
class USizeBox; class USizeBox;
class UScaleBox; class UImage;
class UCanvasPanel;
class UCommonTextBlock; class UCommonTextBlock;
class UCommonActionWidget; class UTextBlock;
DECLARE_DYNAMIC_MULTICAST_DELEGATE(FOnUIButtonActivatedByTimeout); struct FUIStyle;
DECLARE_DYNAMIC_MULTICAST_DELEGATE_OneParam(FOnUIButtonTimerUpdated, float, TimeRemaining); struct FUIStyleButton;
/** /**
* Botão base do ZMMO — porte 1:1 do UI_Button_Master do projeto Hyper para C++ * Botão base do ZMMO (CommonUI). Hoje expõe BindWidgetOptional dos widgets
* auto-configurável, trocando o Struct_Style_Button do Hyper pela camada de * que o WBP UI_Button_Master_New consome, e helpers pra ler o estilo ativo
* tema C++ (FUIStyle via UZMMOThemeSubsystem::GetActiveUIStyle()). * de DT_UI_Styles. A aplicação visual (pintar brushes/cores/fonte) entra
* * depois — esta camada só prepara o acesso.
* Fundação: CommonUI (UCommonButtonBase) cuida de input/foco/click.
* Camada Abstract (padrão "_Abstract" do Hyper). WBP UI_Button_Base herda
* disto; UI_Button_Master herda do WBP. Os widgets visuais são opcionais
* (BindWidgetOptional) e nomeados como no Hyper.
*
* Auto-config: NativePreConstruct chama InitializeButton (text/number/icon/
* sizebox/estilo) + ApplyInputActionStyle. Reage à troca de tema.
*/ */
UCLASS(Abstract, Blueprintable) UCLASS(Abstract, Blueprintable)
class ZMMO_API UUIButton_Base : public UCommonButtonBase class ZMMO_API UUIButton_Base : public UCommonButtonBase
{ {
GENERATED_BODY() GENERATED_BODY()
public:
// ---- Botão ----
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Button")
EUIButtonShape ButtonTypeSelection = EUIButtonShape::Rectangle;
/**
* Variante visual do botão (dropdown vem de FUIStyle.Button.Backgrounds/
* Strokes/Fonts no DT_UI_Styles + as 4 clássicas como fallback).
* Data-driven via FName, sem enum fixo (padrão Hyper-style).
*/
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Button",
meta = (GetOptions = "GetVariantOptions"))
FName Variant = TEXT("Primary");
/** Opções do dropdown de Variant (data-driven do DT_UI_Styles). */
UFUNCTION()
TArray<FString> GetVariantOptions() const;
/** Hyper "Transform Policy = To Upper": força o texto em maiúsculas. */
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Button")
bool bUppercaseText = true;
// ---- Text Style ----
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Text Style")
FText ButtonTextValue = FText::FromString(TEXT("Button Text"));
/** Alinhamento do texto dentro do botão (esquerda/centro/direita). */
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Text Style")
TEnumAsByte<ETextJustify::Type> TextAlign = ETextJustify::Center;
/** true = texto escala p/ caber no tamanho do botão (ScaleBox ScaleToFit);
* false = usa o tamanho fixo da fonte da variante. */
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Text Style")
bool bAutoSizeText = true;
/** Arredonda o Background (RoundedBox) e desenha a borda na cor da
* variante. Desligado = caixa reta. Raio/largura vêm de FUIStyleButton. */
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Button")
bool bRoundedBackground = true;
// ---- Number Text ----
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Number Text")
bool bShowNumberText = false;
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Number Text")
int32 NumberTextAmount = 0;
// ---- Icon ----
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Icon")
bool bUseIconInsteadOfText = false;
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Icon")
TObjectPtr<UObject> IconImage = nullptr;
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Icon")
EUIIconType IconTypeSelection = EUIIconType::None;
// ---- Size ----
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Size")
float InHeightOverride = 50.f;
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Size")
float InWidthOverride = 150.f;
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Size")
bool bDoNotOverrideWithSizebox = false;
// ---- Input Action Style ----
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Input Action Style")
bool bShowInputActionWidget = false;
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Input Action Style")
FMargin InputPadding = FMargin(0.f, 0.f, 0.f, -22.f);
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Input Action Style")
TEnumAsByte<EHorizontalAlignment> InputHorzAlignment = HAlign_Center;
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Input Action Style")
TEnumAsByte<EVerticalAlignment> InputVertAlignment = VAlign_Bottom;
// ---- Deactivate by Timer ----
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Deactivate By Timer")
bool bDeactivatedOnConstructWithTimeout = false;
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Deactivate By Timer")
float Timeout = 0.f;
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Timer")
float TimeUpdateFrequency = 0.05f;
UPROPERTY(BlueprintReadOnly, Category = "Timer")
float TimeRemaining = 0.f;
UPROPERTY(BlueprintAssignable, Category = "Timer")
FOnUIButtonActivatedByTimeout OnButtonActivatedByTimeout;
UPROPERTY(BlueprintAssignable, Category = "Timer")
FOnUIButtonTimerUpdated OnButtonTimerUpdated;
// ---- API (porte 1:1 das funções do Hyper) ----
UFUNCTION(BlueprintCallable, Category = "UI Button")
void SetButtonText(FText InText);
UFUNCTION(BlueprintCallable, Category = "UI Button")
void SetNumberText();
UFUNCTION(BlueprintCallable, Category = "UI Button")
void SetIcon();
UFUNCTION(BlueprintCallable, Category = "UI Button")
void ApplySizeBox();
UFUNCTION(BlueprintCallable, Category = "UI Button")
void ApplyInputActionStyle();
/** Orquestrador (Hyper "Initialize Button"): aplica tudo + estilo do tema. */
UFUNCTION(BlueprintCallable, Category = "UI Button")
void InitializeButton();
/** Re-resolve os tokens do tema ativo e reaplica (Hyper "Update Button Style"). */
UFUNCTION(BlueprintCallable, Category = "UI Style")
void RefreshUIStyle();
/** Estado visual do botão (mapeia nos tokens BgNormal/Hover/Pressed/Disabled). */
enum class EUIButtonVisual : uint8 { Normal, Hovered, Pressed, Disabled };
/** Pinta o Background (fill + borda) conforme o estado, re-resolvendo o tema. */
void ApplyVisualState(EUIButtonVisual State);
protected: protected:
virtual void NativePreConstruct() override; // ---- Widgets BindWidgetOptional (espelham a árvore do UI_Button_Master_New) ----
virtual void NativeConstruct() override; UPROPERTY(BlueprintReadOnly, Category = "UI Button", meta = (BindWidgetOptional))
virtual void NativeDestruct() override; TObjectPtr<USizeBox> SizeBox;
// Estados do UCommonButtonBase → repinta o Background pela variante. UPROPERTY(BlueprintReadOnly, Category = "UI Button", meta = (BindWidgetOptional))
virtual void NativeOnHovered() override; TObjectPtr<UImage> ImgStoke;
virtual void NativeOnUnhovered() override;
virtual void NativeOnPressed() override;
virtual void NativeOnReleased() override;
virtual void NativeOnEnabled() override;
virtual void NativeOnDisabled() override;
/** Hook opcional: o WBP pode estender/sobrescrever a aplicação visual. */ UPROPERTY(BlueprintReadOnly, Category = "UI Button", meta = (BindWidgetOptional))
UFUNCTION(BlueprintImplementableEvent, Category = "UI Style", TObjectPtr<UCanvasPanel> CanvasPanel_Text;
meta = (DisplayName = "Apply UI Style"))
void BP_ApplyUIStyle(const FUIStyleButton& Button, const FUIStyleButtonVariant& VariantStyle);
FUIStyleButtonVariant ResolveVariant(const FUIStyleButton& Button) const; UPROPERTY(BlueprintReadOnly, Category = "UI Button", meta = (BindWidgetOptional))
// ---- Widgets visuais opcionais (nomes espelham o Hyper) ----
UPROPERTY(meta = (BindWidgetOptional))
TObjectPtr<USizeBox> SizeBox_Master;
UPROPERTY(meta = (BindWidgetOptional))
TObjectPtr<UCommonTextBlock> ButtonText; TObjectPtr<UCommonTextBlock> ButtonText;
/** Envolve o ButtonText: escala o texto p/ caber no botão (bAutoSizeText). */ UPROPERTY(BlueprintReadOnly, Category = "UI Button", meta = (BindWidgetOptional))
UPROPERTY(meta = (BindWidgetOptional))
TObjectPtr<UScaleBox> TextScaleBox;
UPROPERTY(meta = (BindWidgetOptional))
TObjectPtr<UTextBlock> Number_Text; TObjectPtr<UTextBlock> Number_Text;
UPROPERTY(meta = (BindWidgetOptional)) UPROPERTY(BlueprintReadOnly, Category = "UI Button", meta = (BindWidgetOptional))
TObjectPtr<UImage> Image_Icon; TObjectPtr<UImage> ImgIcon;
/** Fundo temável (nossa adição sobre o Hyper, p/ FUIStyle). */ // ---- Acesso ao estilo ativo (DT_UI_Styles) — C++ only ----
UPROPERTY(meta = (BindWidgetOptional))
TObjectPtr<UBorder> Background;
private: /** Carrega DT_UI_Styles e devolve o FUIStyle da row pedida (default "Default").
void StartDeactivateTimeout(); * Retorna nullptr se a DT ou a row não existirem. Ponteiro válido enquanto
void HandleTimeoutFinished(); * a DT estiver carregada — uso típico é leitura imediata, sem armazenar. */
void HandleTimerTick(); const FUIStyle* GetActiveUIStyle(FName ThemeId = TEXT("Default")) const;
UFUNCTION() /** Atalho: devolve só o bloco Button do FUIStyle ativo. */
void HandleThemeChanged(FName NewThemeId); const FUIStyleButton* GetActiveButtonStyle(FName ThemeId = TEXT("Default")) const;
bool bThemeBound = false;
FTimerHandle InactivityTimer;
FTimerHandle TimerTickHandle;
}; };

View File

@@ -52,83 +52,43 @@ void UUIPanel_Base::RefreshUIStyle()
const FUIStyle AS = ResolvePanelStyle(this, Fallback); const FUIStyle AS = ResolvePanelStyle(this, Fallback);
const FUIStylePanel& P = AS.Panel; const FUIStylePanel& P = AS.Panel;
// ---- Modo TEXTURA (padrão Hyper): brush por EUIPanelTexture ---- // bUseTheme = true -> brushes vêm da DT (lookup pelas 3 chaves).
// Fiel ao UI_Panel_Master do Hyper: só faz SetBrush nos dois Borders // bUseTheme = false -> brushes vêm dos 3 FSlateBrush em Panels (direto).
// (Find no mapa por chave). Sem padding/visibility — a arte da textura
// já traz a moldura; o conteúdo é injetado pelo WBP filho.
// Seed dos mapas da instância a partir do tema quando vazios — assim o
// Details mostra Panel Background/Outline preenchidos e editáveis (igual
// ao Hyper, cujo `Panels` é alimentado pelo estilo). Edições do designer
// têm prioridade; o tema (P) é fallback por chave.
if (Panels.PanelBackground.Num() == 0) { Panels.PanelBackground = P.PanelBackground; }
if (Panels.PanelOutline.Num() == 0) { Panels.PanelOutline = P.PanelOutline; }
const FSlateBrush* BgTex = nullptr; static const FSlateBrush NoDrawBrush = []
if (PanelTexture != EUIPanelTexture::None)
{ {
BgTex = Panels.PanelBackground.Find(PanelTexture); FSlateBrush B;
if (!BgTex) { BgTex = P.PanelBackground.Find(PanelTexture); } B.DrawAs = ESlateBrushDrawType::NoDrawType;
} return B;
if (BgTex) }();
auto ResolveFromTheme = [](EUIPanelTexture Key,
const TMap<EUIPanelTexture, FSlateBrush>& Map) -> const FSlateBrush*
{ {
Background->SetBrush(*BgTex); return (Key == EUIPanelTexture::None) ? nullptr : Map.Find(Key);
if (Outline) };
{
// Hyper: Find sempre seguido de SetBrush. No Hyper a entrada de const FSlateBrush* BgTex = bUseTheme
// outline ausente é um brush TRANSPARENTE (alpha 0) — não some ? ResolveFromTheme(BackgroundKey, P.PanelBackground)
// widget, só não desenha. Um FSlateBrush default desenha um : &Panels.PanelBackground;
// retângulo BRANCO; o equivalente correto é DrawAs=NoDrawType. Background->SetBrush(BgTex ? *BgTex : NoDrawBrush);
const FSlateBrush* OlTex = Panels.PanelOutline.Find(PanelTexture);
if (!OlTex) { OlTex = P.PanelOutline.Find(PanelTexture); }
static const FSlateBrush NoDrawBrush = []
{
FSlateBrush B;
B.DrawAs = ESlateBrushDrawType::NoDrawType;
return B;
}();
Outline->SetBrush(OlTex ? *OlTex : NoDrawBrush);
}
BP_ApplyPanelStyle(P);
return;
}
// ---- Modo COR (RoundedBox) ----
if (Outline) if (Outline)
{ {
Outline->SetVisibility(ESlateVisibility::Collapsed); const FSlateBrush* OlTex = bUseTheme
? ResolveFromTheme(OutlineKey, P.PanelOutline)
: &Panels.PanelOutline;
Outline->SetBrush(OlTex ? *OlTex : NoDrawBrush);
} }
FLinearColor Fill; if (OutlineEffect)
FLinearColor OutlineColor = P.BorderColor;
switch (PanelType)
{ {
case EUIPanelType::Secondary: Fill = P.BgRaised; break; const FSlateBrush* OeTex = bUseTheme
case EUIPanelType::Tertiary: Fill = P.BgSunken; break; ? ResolveFromTheme(OutlineEffectKey, P.PanelOutlineEffect)
case EUIPanelType::Card: Fill = P.CardSelectedBg; OutlineColor = P.CardSelectedBorder; break; : &Panels.PanelOutlineEffect;
case EUIPanelType::Primary: OutlineEffect->SetBrush(OeTex ? *OeTex : NoDrawBrush);
case EUIPanelType::None:
default: Fill = P.Bg; break;
} }
FSlateBrush Brush = Background->Background;
Brush.TintColor = FSlateColor(Fill);
if (bRoundedBackground)
{
const float R = bUseSmallPadding ? P.CornerRadiusSmall : P.CornerRadius;
Brush.DrawAs = ESlateBrushDrawType::RoundedBox;
Brush.OutlineSettings = FSlateBrushOutlineSettings(
FVector4(R, R, R, R), FSlateColor(OutlineColor), P.BorderWidth);
Brush.OutlineSettings.RoundingType = ESlateBrushRoundingType::FixedRadius;
}
else
{
Brush.DrawAs = ESlateBrushDrawType::Box;
Brush.OutlineSettings = FSlateBrushOutlineSettings(
FVector4(0, 0, 0, 0), FSlateColor(OutlineColor), P.BorderWidth);
}
Background->SetBrush(Brush);
Background->SetPadding(bUseSmallPadding ? P.PaddingSmall : P.Padding);
BP_ApplyPanelStyle(P); BP_ApplyPanelStyle(P);
} }

View File

@@ -23,34 +23,31 @@ class ZMMO_API UUIPanel_Base : public UCommonUserWidget
GENERATED_BODY() GENERATED_BODY()
public: public:
/** Tipo do painel — escolhe Bg/BgRaised/BgSunken/Card de FUIStylePanel. */
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Panel")
EUIPanelType PanelType = EUIPanelType::Primary;
/** Arredonda o Background (RoundedBox) com a borda do tema. */
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Panel")
bool bRoundedBackground = true;
/** Usa o padding "pequeno" do tema (PaddingSmall) em vez de Padding. */
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Panel")
bool bUseSmallPadding = false;
/** /**
* Conjunto de textura (modo Hyper). Se != None E houver brush no mapa * true = lê brushes da DT_UI_Styles (FUIStylePanel) — tema centralizado.
* FUIStylePanel.PanelBackground p/ esta chave, usa o painel TEXTURIZADO * false = ignora a DT e usa apenas `Panels` (brushes manuais no Details).
* (brush bg + outline). Senão, cai no modo COR (RoundedBox acima).
*/ */
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Panel") UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Panel")
EUIPanelTexture PanelTexture = EUIPanelTexture::None; bool bUseTheme = true;
/** // ---- Modo TEMA: escolhe a chave do mapa FUIStylePanel pra cada Border. ----
* Mapas de brush por instância — espelha o `Panels` do UI_Panel_Master
* do Hyper (Details mostra Panel Background / Panel Outline editáveis). UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Panel",
* Tem prioridade. Se não houver brush p/ a chave aqui, cai no tema meta = (EditCondition = "bUseTheme", EditConditionHides))
* (DT_UI_Styles). Em NativePreConstruct é semeado a partir do tema EUIPanelTexture BackgroundKey = EUIPanelTexture::None;
* quando vazio, então por padrão o tema manda — como no Hyper.
*/ UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Panel",
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Panel") meta = (EditCondition = "bUseTheme", EditConditionHides))
EUIPanelTexture OutlineKey = EUIPanelTexture::None;
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Panel",
meta = (EditCondition = "bUseTheme", EditConditionHides))
EUIPanelTexture OutlineEffectKey = EUIPanelTexture::None;
// ---- Modo MANUAL: 3 brushes editáveis direto no Details. ----
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Panel",
meta = (EditCondition = "!bUseTheme", EditConditionHides))
FUIPanelBrushSet Panels; FUIPanelBrushSet Panels;
/** Re-resolve os tokens do tema ativo e reaplica no Background. */ /** Re-resolve os tokens do tema ativo e reaplica no Background. */
@@ -75,6 +72,10 @@ protected:
UPROPERTY(meta = (BindWidgetOptional)) UPROPERTY(meta = (BindWidgetOptional))
TObjectPtr<UBorder> Outline; TObjectPtr<UBorder> Outline;
/** Efeito sobre o contorno (glow/embossing/etc.) — pintado acima do Outline. */
UPROPERTY(meta = (BindWidgetOptional))
TObjectPtr<UBorder> OutlineEffect;
private: private:
UFUNCTION() UFUNCTION()
void HandleThemeChanged(FName NewThemeId); void HandleThemeChanged(FName NewThemeId);

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.Money = P.Money;
S.AtkBase = P.AtkBase; S.AtkBase = P.AtkBase;
S.AtkEquipBonus = P.AtkEquipBonus; S.AtkEquipBonus = P.AtkEquipBonus;
S.MatkBase = P.MatkBase; S.MatkBaseMin = P.MatkBaseMin;
S.MatkBaseMax = P.MatkBaseMax;
S.MatkEquipBonus = P.MatkEquipBonus; S.MatkEquipBonus = P.MatkEquipBonus;
S.DefBase = P.DefBase; S.DefBase = P.DefBase;
S.DefEquipBonus = P.DefEquipBonus; S.DefEquipBonus = P.DefEquipBonus;

View File

@@ -5,7 +5,7 @@
/** /**
* Snapshot completo dos atributos do char vindo do server via * 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 * Cliente NUNCA recalcula derivados — apenas exibe o que recebe. Os campos
* HP/MaxHp/SP/MaxSp ja sao os "efetivos" (base + equip + buff somados no * 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 AtkBase = 0;
UPROPERTY(BlueprintReadOnly, Category = "Zeus|Attributes|Derived") int32 AtkEquipBonus = 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 MatkEquipBonus = 0;
UPROPERTY(BlueprintReadOnly, Category = "Zeus|Attributes|Derived") int32 DefBase = 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 "Engine/World.h"
#include "ZMMOJobDataAsset.h" #include "ZMMOJobDataAsset.h"
#include "ZMMOJobsSubsystem.h" #include "ZMMOJobsSubsystem.h"
#include "ZeusNetworkSubsystem.h"
namespace namespace
{ {
@@ -52,3 +53,37 @@ bool UZMMOJobsLibrary::IsJobRegistered(const UObject* WorldContextObject, int32
} }
return false; 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; 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 "CoreMinimal.h"
#include "Engine/DataAsset.h" #include "Engine/DataAsset.h"
#include "ZMMOJobTypes.h"
#include "ZMMOJobDataAsset.generated.h" #include "ZMMOJobDataAsset.generated.h"
class UTexture2D; class UTexture2D;
@@ -39,11 +40,28 @@ public:
UPROPERTY(EditDefaultsOnly, BlueprintReadOnly, Category = "Zeus|Job") UPROPERTY(EditDefaultsOnly, BlueprintReadOnly, Category = "Zeus|Job")
FString TechnicalName; 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. /// Pode usar FText Localization tables pra multi-idioma futuro.
UPROPERTY(EditDefaultsOnly, BlueprintReadOnly, Category = "Zeus|Job") UPROPERTY(EditDefaultsOnly, BlueprintReadOnly, Category = "Zeus|Job")
FText DisplayName; 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. /// Icone do job — usado em StatusWindow, CharacterCard, JobChangeUI.
/// Soft ptr: nao carrega ate alguem chamar LoadSynchronous() ou async load. /// Soft ptr: nao carrega ate alguem chamar LoadSynchronous() ou async load.
UPROPERTY(EditDefaultsOnly, BlueprintReadOnly, Category = "Zeus|Job|Assets") 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 "CoreMinimal.h"
#include "Kismet/BlueprintFunctionLibrary.h" #include "Kismet/BlueprintFunctionLibrary.h"
#include "ZMMOJobTypes.h"
#include "ZMMOJobsLibrary.generated.h" #include "ZMMOJobsLibrary.generated.h"
class UZMMOJobDataAsset; class UZMMOJobDataAsset;
@@ -36,4 +37,26 @@ public:
UFUNCTION(BlueprintPure, Category = "Zeus|Jobs", UFUNCTION(BlueprintPure, Category = "Zeus|Jobs",
meta = (WorldContext = "WorldContextObject")) meta = (WorldContext = "WorldContextObject"))
static bool IsJobRegistered(const UObject* WorldContextObject, int32 ClassId); 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 "CoreMinimal.h"
#include "Subsystems/GameInstanceSubsystem.h" #include "Subsystems/GameInstanceSubsystem.h"
#include "ZMMOJobTypes.h"
#include "ZMMOJobsSubsystem.generated.h" #include "ZMMOJobsSubsystem.generated.h"
class UZMMOJobDataAsset; class UZMMOJobDataAsset;
@@ -41,6 +42,22 @@ public:
UFUNCTION(BlueprintPure, Category = "Zeus|Jobs") UFUNCTION(BlueprintPure, Category = "Zeus|Jobs")
int32 GetJobCount() const { return CachedJobs.Num(); } 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: private:
/// Scan AssetRegistry + popula CachedJobs. Sincronizo (V1) — chamado UMA /// Scan AssetRegistry + popula CachedJobs. Sincronizo (V1) — chamado UMA
/// vez no Initialize. Pra 50 jobs eh aceitavel; se crescer pra 200+ pode /// 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[] { 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). // Espelho do `Server/.../JobsSystem/module.json` (modulo nucleo MMO).