90 Commits

Author SHA1 Message Date
432741af52 feat(combat): cues de hit react + som punch + camera shake (cliente UE)
3 GameplayCueNotify_Burst BPs (taxonomia Lyra GameplayCue.Zeus.Combat.Hit.*):
  GC_Combat_Hit_React   -> PlayAnimMontage AM_HitReact_Light no alvo
  GC_Combat_Hit_Impact  -> SpawnSoundAtLocation SW_Punch_Cartoon na pos do alvo
  GC_Combat_Hit_Self    -> ClientStartCameraShake CS_HitReact no PC do alvo

Assets novos:
  AS_HitReact (+ AM_HitReact_Light/Medium/Heavy) -- montages de reacao
  SW_Punch_Cartoon -- SFX de impacto (0.3s)
  CS_HitReact -- LegacyCameraShake (osc 0.35s, pitch/roll/locZ)

DT_Abilities recriado (RowStruct ZeusAbilityDataRow): rows Dash+Kick com
AbilityClass + ActivateCueTag GameplayCue.Zeus.*. GC_Kick/GC_Dash com tags
renomeadas pro prefixo GameplayCue.* (filter do Asset Picker).

ZeusServerTags.ini: regenerado via ZeusTool tags sync (106 tags canonicas).

Source/Combat/ZeusAnimNotifyState_BodyHitWindow: AnimNotifyState que abre a
janela de hit detection (FootR capsule) durante o swing do Kick.

GC_Kick_Target removido (substituido por Combat.Hit.React reutilizavel).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-18 21:37:40 -03:00
84f552b4fa feat(gas): Kick V0 end-to-end -- IA_Kick + BP_GA_Kick + GC_Kick + rename Pawn + limpeza ThirdPerson
Cliente UE Kick V0 funcionando end-to-end (testado com 2 PIEs: ativador chuta
+ proxies dos peers veem montage via GameplayCue replicado).

ASSETS NOVOS:
- IA_Kick.uasset: Enhanced Input Action (Boolean) -- tecla F.
- IMC_Default: mapping F -> IA_Kick (trigger Pressed).
- BP_ZeusPlayerCharacter.uasset (em /Game/ZMMO/Core/Player/): renomeado a
  partir do BP_ThirdPersonCharacter; agora possui handler IA_Kick que chama
  RequestActivateAbilityByTag("Zeus.Ability.Melee.Kick") no UZeusGASComponent
  do PlayerState. ZeusGameMode aponta pra ele via FClassFinder.
- GC_Kick.uasset (em /Game/ZMMO/GAS/Cues/): GameplayCueNotify_Burst BP, parent
  AZeusGameplayCueNotify_Burst, tag Zeus.Cue.Melee.Kick. Override do hook
  OnZeusCueExecute (Lyra-style) -> Cast To Character (MyTarget) -> Play Anim
  Montage (AM_Kick_Montage). Quando server emite S_ABILITY_CUE, cliente
  spawna esse GC e roda a animacao no Mesh do proxy.

ASSETS MODIFICADOS:
- GA_Kick: conexoes PlayMontageAndWait -> EndAbility ajustadas. Antes TODAS
  as 5 saidas (OnCompleted/OnBlendedIn/OnBlendOut/OnInterrupted/OnCancelled)
  ligavam no EndAbility -- OnBlendedIn dispara ~100ms apos inicio (fim do
  blend in) -> EndAbility -> bStopWhenAbilityEnds=true -> montage para
  prematuro. Fix: so OnCompleted/OnInterrupted/OnCancelled. Hook BP novo
  OnZeusAbilityConfirmedByServer -> Print "Kick CONFIRMED by server".
- DT_Abilities: row Kick adicionada (Tag=Zeus.Ability.Melee.Kick, AbilityClass
  =GA_Kick_C, ActivationMontage=AM_Kick_Montage, ActivateCueTag=Zeus.Cue.Melee.Kick,
  Category=Melee, KeybindHint=F).
- BP_PlayerState: salvo apos GameMode apontar pra ele (mark dirty).
- ABP_Unarmed: deletado 1 Slot DefaultSlot duplicado (warning AnimBlueprintLog
  "node ja existe" + PlayMontage interno falhava em escolher slot).

LIMPEZA Content/ThirdPerson/:
- BP_ThirdPersonCharacter.uasset DELETED (renomeado pra BP_ZeusPlayerCharacter).
- BP_ThirdPersonGameMode.uasset DELETED (sem refs externas).
- BP_ThirdPersonPlayerController.uasset DELETED (so usado por BP_ThirdPersonGameMode).
- Menu.umap DELETED (sample UE template, sem refs).
- MI_ThirdPersonColWay.uasset mantido (90 refs em L_Test_UI -- mover pra
  /Game/ZMMO/Materials/ fica pra batch futuro).

Content/_DevOnly/ NAO incluido.

Tested in PIE 2 players: PIE0 aperta F -> chuta visualmente; proxy do PIE0
no PIE1 tambem chuta (montage replicado via GC). Cooldown 1.5s ativo.
Stamina cai 100 -> 95 a cada kick.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-06-18 03:56:28 -03:00
1308338953 feat(gas): GameMode aponta pra BPs (Pawn/PlayerState) + cleanup ZeusCharacter
ZeusGameMode.cpp:
- DefaultPawnClass: usa FClassFinder pra /Game/ZMMO/Core/Player/BP_ZeusPlayerCharacter
  (fallback AZeusCharacter). Sem isso, engine instancia C++ direto e perde
  toda customizacao BP (EnhancedInputAction events, AnimBP wire, componentes
  BP-added).
- PlayerStateClass: usa FClassFinder pra /Game/ZMMO/Core/Player/BP_PlayerState
  (fallback AZeusPlayerState). Mesmo motivo -- BP filho tem componentes (ex:
  UZeusGASComponent via ComponentClasses no DefaultGame.ini) e overrides do
  designer que classes C++ puras nao tem.
- TODO: renomear BP_PlayerState -> BP_ZeusPlayerState (alinhar com convencao
  BP_ZeusPlayerCharacter); path do FClassFinder ja preparado pra renomear.

ZeusCharacter.cpp/h cleanup:
- Removida gambiarra anterior (PossessedBy + OnRep_PlayerState overrides +
  RefreshGASAvatar com FTimerHandle retry). Substituida pelo pattern
  event-driven em UZeusGASComponent::HandlePawnSet (commit server anterior).
- Includes desnecessarios (AbilitySystemComponent.h + ZeusAbilitySystemComponent.h)
  removidos.

ZMMO.uproject:
- GameplayAbilities adicionado em Plugins (UE 5.7 exige que modulo do jogo
  declare explicitamente plugins que ele consome -- antes vinha transitivamente
  do plugin ZeusGAS, gerava warning de build).

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-06-18 03:56:00 -03:00
21d42d9846 chore(gas): sync GameplayTags do server (Stamina/Kick/Spell/Buff) + flags de debug GAS
ZeusServerTags.ini -- merge gerado por sync_tags.ps1 (14 tags novas vindo do
ZeusServer dump JSON via ZeusTool tags sync):
- Stamina:           Zeus.Attribute.Stamina + MaxStamina
- Kick:              Zeus.Ability.Melee + Zeus.Ability.Melee.Kick
                     Zeus.Effect.Cooldown.Kick + Zeus.Cooldown.Kick
                     Zeus.Cue.Melee + Zeus.Cue.Melee.Kick
- FireballRain rename: Zeus.Ability.Spell.* + Zeus.Effect.Cooldown.FireballRain
                       Zeus.Cooldown.FireballRain
- Buff reorg:        Zeus.Effect.Buff + Zeus.Effect.Buff.StrUp

Tag orfa removida manualmente: Zeus.Ability.Combat.Kick (Kick mudou
de Combat.* pra Melee.*; merge preservou a antiga, limpo aqui).

DefaultGame.ini: flags de debug do GAS pra Kick development:
- bUseDebugTargetFromHud=true   (showdebug abilitysystem aponta pra mira)
- AbilitySystem.LogAbilityTriggers=1
- AbilitySystem.DebugAbilityTags=1

Lembrete: GameplayTags.ini so' eh lido no boot do editor -- reiniciar UE
pra registrar as tags novas (BP_GA_Kick depende de Zeus.Ability.Melee.Kick).

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-06-18 01:37:45 -03:00
2895889e2b feat(gas): rename BP_GA_* -> GA_* + DT_Abilities seed + wire no ThirdPersonCharacter
Convencao de nome: abilities passam de "BP_GA_<Nome>" pra "GA_<Nome>"
(remove o "BP_" redundante -- ja sabemos que GA_ e' Blueprint).

- BP_GA_Dash.uasset -> GA_Dash.uasset
- BP_GA_Kick.uasset -> GA_Kick.uasset (pasta Kick/ mantida)
- DT_Abilities: linha do Kick atualizada pra apontar pra GA_Kick
- BP_ThirdPersonCharacter: ability Kick ligada (input/grant)

Doc HOWTO ja' reflete a nova convencao (ver server commit).
Content/_DevOnly/ NAO incluido.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-06-18 00:50:09 -03:00
1114ddbb11 feat(gas): seed inicial do GASCombatSystem -- ABP_Unarmed + Kick ability
- ABP_Unarmed.uasset: AnimBP unarmed (modificado) - base de animacao p/ combate sem arma
- BP_GA_Kick.uasset: GameplayAbility blueprint do Kick
- AS_Kick.uasset: animation sequence do golpe
- AM_Kick_Montage.uasset: montage que a ability dispara

Primeiro asset GAS authored cliente-side -- pre-requisito pra Fase 2 (M9 cues + M10 AbilityTask cliente).

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-06-18 00:29:10 -03:00
9a430dabc3 feat(aoi): UZeusAOIComponent ganha visibility tracking + delegates BP (1.4 do roteiro PvP)
Estende o componente (que ja existia como debug overlay) com:
- TSet<int64> VisibleEntityIds_ espelhando ENT_SPAWN/ENT_DESPAWN do V1
- Delegates BlueprintAssignable: OnEntityEnteredAOI(EntityId) + OnEntityExitedAOI(EntityId, Reason)
- BlueprintPure: GetVisibleEntityCount, GetVisibleEntityIds, IsEntityVisible, GetSelfEntityId
- Bind/unbind dos delegates DYNAMIC do UZeusNetworkingClientSubsystem (AddDynamic/RemoveDynamic)
  no BeginPlay/EndPlay
- Self-entity filtrada via OnSelfEntityAssigned -- proprio char nao trackeia

Sem histerese client-side (server ja filtra com margem ~2048cm; se aparecer
flapping visual, adicionar depois). Memoria: project_aoi_client_component.

NAO testado in-game.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-06-18 00:23:51 -03:00
a3f2fa9c06 feat(client): overlay de fronteira -- ajustes + materiais e external actor
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-06-17 22:44:11 -03:00
1f2441bde7 Merge pull request 'Consolidar GameplayAbilitySystem na main (baseline cliente tudo funcionando 2026-06-16)' (#6) from GameplayAbilitySystem into main
Reviewed-on: #6
2026-06-16 00:26:52 -03:00
216e580548 Overlay de fronteira (cliente): paredes do grid + labels Cell X,Y
- AZeusFrontierOverlayActor: paredes (ProceduralMesh, material de portal) em
  todas as fronteiras do grid; label DrawDebugString no centro de cada cell com
  notacao de quadtree ("Cell 0" / "Cell 0,2" pos-split) + nome do servidor.
- UZeusAOIComponent: liga via toggle "Fronteiras (Mesh)" / CVar zeus.debug.frontier,
  spawna/atualiza o ator, re-pede o 6163 a cada 2s (reflete split/merge).
- ZMMO.Build.cs: + ProceduralMeshComponent.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-15 00:51:42 -03:00
de294f4075 fix(yaw): proxy replica o yaw do CORPO do dono (orient-to-movement), nao a mira
[YAW-BODY-ORIENT] commit conjunto cliente + servidor.

Sintoma: ao apertar D o proxy virava ~45 graus de uma vez, em vez de virar
igual ao player local (suave, so um pouco), e parado seguia o mouse.

Raiz: a fonte de verdade do yaw estava errada. O servidor derivava
simpleYawDeg de atan2(velocidade) INSTANTANEO ao mover (snap pra direcao do
movimento) e da mira (ControlRotation) ao parar -- nenhum dos dois e o yaw
do corpo do dono.

Correcao ponta-a-ponta:
- Cliente: envia GetActorRotation().Yaw (yaw do CORPO; o CMC ja orienta pra
  direcao do movimento a 500 deg/s e bUseControllerRotationYaw=false faz o
  MOUSE nao girar o corpo) no lugar de GetControlRotation().Yaw.
- Servidor: usa o yaw recebido DIRETO em simpleYawDeg (remove atan2
  instantaneo + mira-no-idle).
- Proxy: replica Snapshot.YawDeg interpolado shortest-path (remove a
  derivacao por Atan2 da velocidade).

Resultado: o proxy vira identico ao corpo do dono e o mouse nao gira o corpo.
Futuro (aim/combate/Motion Matching): adicionar um campo aimYawDeg dedicado em
vez de reusar este.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-13 02:22:07 -03:00
4d737829f5 ZeusPlayerProxy: fix jitter por fome de deltas (mitigacao + anti-stale) [Change-Set: JITTER-STARVATION-2026-06-13]
Player remoto parado nao gera DELTA_BATCH (ComputeDelta suprime no server) -> o
proxy fica "mudo": NewestMs congela, renderLagMs cresce 1:1 com o wall-clock ->
extrapola sempre -> "anda no lugar" (moonwalk) e salta quando o player volta.
Provado em log (secsSinceSnap subindo enquanto o remoto esta moving=0). NAO e'
clock errado nem self-proxy (red-herrings descartados); o seed do relogio esta' OK.

- Tick: quando faminto (extrap alem de MaxExtrapolationSeconds+0.15s), zera a vel
  visual -> proxy vai pra Idle em vez de moonwalk.
- ApplyEntitySnapshot: anti-stale -- snapshot > 1000ms a frente do topo do buffer
  descarta o buffer + re-bootstrapa o ServerClockOffsetMs -> snap limpo (sem span
  gigante na interpolacao). Loga "buffer STALE gap=Xms -> reset+reseed".
- Logs DIAG PERMANENTES (Warning, mantidos de proposito -- ver memoria
  project_proxy_delta_starvation_jitter): SEED clock + renderLagMs/newestMs/
  secsSinceSnap/speed no Tick. Sonda anti-regressao.

Cobre tambem o interserver: shadow proxies parados sofrem a mesma fome.
Validado em jogo (user: "sem jitter nenhum"). Par server: PubV2-DIAG (mesmo Change-Set).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-13 01:26:33 -03:00
341e6895c9 Config/Tags: ZeusServerTags.ini gerado (tags do server p/ o GAS) [Change-Set: TAGS-SYNC-2026-06-12]
80 GameplayTags do ZeusServer (Zeus.Status/Ability/Effect/Cue/Attribute/Cooldown)
geradas pelo pipeline build-time (par com o commit de mesmo Change-Set no repo
ZeusServerEngine). O UE le Config/Tags/*.ini no boot e registra as tags, pro
cliente resolver os hashes de GE/Tag/Cue (FNV-1a64) -- fim do "tag nao resolvida".
Regeravel com sync_tags.ps1; nao editar a mao.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-12 23:44:05 -03:00
5f4c88637f ZN V1: overlay AOI canonico + ENT_SPAWN keyframe [Change-Set: AOI-VIS-2026-06-12]
Lado CLIENTE ZMMO (par com o commit de mesmo Change-Set no repo ZeusServerEngine -- devem
ir juntos, o wire do ENT_SPAWN mudou).

- UZeusAOIComponent: migrado do transporte legacy (SendDebugAoiRequest/OnDebugAoiInfo,
  desativado no V1) pro canonico (EmitDebugAoiRequest 6160 / OnDebugAoiInfo 6161). Esfera de
  debug 32 -> 48 segments + clamp visual de 500m (resolve "zona sem limite / poucas linhas").
- ZeusWorldSubsystem::OnNetEntitySpawned: recebe vel + grounded + serverTimeMs (delegate
  FZeusV1OnEntitySpawned 4 -> 7 params) e semeia o proxy recem-criado via HandlePlayerStateUpdate
  -> ancora o relogio de interpolacao + a vel inicial -> proxy nasce ja' animando no re-spawn
  (sai/volta do raio AOI segurando W), sem Idle deslizando.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-12 21:04:43 -03:00
77d52a703b ZMMO: CHAR_INFO via V1 (Fase A) + fix loading travado
- ZeusWorldSubsystem consome ENT_CHAR_INFO (delegate OnCharInfo): aplica
  nome/guild no PlayerState do entityId certo (self ou proxy), com cache porque
  o CHAR_INFO chega antes do ator existir (server emite antes do ENT_SPAWN).
- FIX loading travado: a etapa "Spawn" do loading dependia do legacy
  OnPlayerSpawned, que não dispara mais com V1 -> loading eterno. Agora a etapa
  é marcada pelo sinal V1 OnSelfEntityAssigned (ENT_SELF); bind legacy removido.
  Travel ainda passa por OnServerTravelRequested (ponte legacy) até a Fase D.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-12 13:06:42 -03:00
9f5ccd3a05 ZMMO: migração para ZeusNetworking V1 canônico + fix overshoot do proxy
- ZeusCharacter/ZeusWorldSubsystem passam a usar UZeusNetworkingClientSubsystem:
  ENT_SELF (entidade própria), INPUT 6077 via EmitInput, spawn/despawn/delta
  rebindados pro subsystem novo (legacy vira fallback).
- ZeusPlayerProxy consome velocidade/grounded/serverTimeMs (delta 0x02): anima,
  vira na direção do movimento e cola no chão (MOVE_Walking).
- Fix overshoot/inércia: FlushInputAxisToServer considera velocidade residual
  (braking) e envia a 30Hz até o dono parar de fato; o proxy desacelera suave,
  sem extrapolar ~1m além nem snap-back.
- Char-select -> ConnectWithTicket (handoff ticket) quando bUseZeusNetworkingV1.

Inclui logs [InputDbg] (ZeusCharacter) e DEBUG-PROXY (ZeusPlayerProxy) mantidos
para testes posteriores de input/jitter.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-12 02:51:37 -03:00
c0f6d32d95 feat(zmmo-client): logs AOI categorias + K4 cfg recv
Rodando sem Jitter visivel.

- DECLARE/DEFINE LogZeusHandoff/AOI/Proxy/HaloTransition no modulo
  ZMMO (nao no plugin) por causa do adaptive non-unity build do UBT
  que usa git status do repo do cliente -- plugin em outro repo nao
  dispara rebuild quando edit so o header dele.
- ZeusAOIComponent::HandleAoiConfig loga raios recebidos do server
  pra cruzar com Net.AOI server-side ("AOI cfg recv interestCm=6000
  (60m) despawnCm=8000 (80m)").
- Parte da milestone 2026-06-11: proxy/PIE estaveis apos fix de
  double-emit AOI no server (AOIRegistry::ForceTickNow).
2026-06-11 18:01:32 -03:00
f21d059b67 fix(zmmo-client): ZeusPlayerProxy clock offset EMA + asset cleanup L_TestWorld
ZeusPlayerProxy.cpp -- H3 fix (2026-06-06): EMA com clamp para
ServerClockOffsetMs
================================================================
Antes: offset congelado na 1a amostra de ApplyEntitySnapshot. Em
cross-server handoff, a fonte do ServerTimeMs muda (publisher antigo
emitia 12340000, novo emite 12351000) e o offset fica permanentemente
skewed. Causa tremor pos-handoff + drift cumulativo de clock cliente
vs server.

Depois: cada amostra recalcula candidato (LocalNowMs - Snapshot.ServerTimeMs)
e suaviza via EMA com alpha=0.02 (~50 amostras para convergir) + clamp
max 2ms por amostra (a 30Hz isso converge a 60ms/s, suficiente para
absorver drift normal sem causar tremor visual). 1a amostra continua
bootstrap direto.

Trecho:
  const double Alpha = 0.02;
  const int64 RawTarget = ServerClockOffsetMs*(1-Alpha) + OffsetCandidate*Alpha;
  const int64 Delta = RawTarget - ServerClockOffsetMs;
  const int64 ClampedDelta = clamp(Delta, -2, 2);
  ServerClockOffsetMs += ClampedDelta;

Asset cleanup L_TestWorld
==========================
DT_Maps.uasset modificado + 3 ExternalActors deletados (provavelmente
limpeza no UE editor ao reorganizar mapa). Empacotado junto pra branch
ficar limpa.

* Content/ZMMO/Data/World/DT_Maps.uasset (M)
* Content/__ExternalActors__/ZMMO/Maps/World/L_TestWorld/2/BR/38W34OMKW0W3L1PGS3Y98G.uasset (D)
* Content/__ExternalActors__/ZMMO/Maps/World/L_TestWorld/4/8L/X3PMRJRGAACIU1WG1P1KJ0.uasset (D)
* Content/__ExternalActors__/ZMMO/Maps/World/L_TestWorld/8/L1/C6M65Q91PG5E7ML91VQZX3.uasset (D)

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-06-11 14:52:39 -03:00
e87b2cec6c fix(zmmo-client): guard HandlePlayerSpawned ignore proprio entityId + ZeusV1 config
Acompanha commit server 54f2f9a (P10-FIX-LEGACY). Resolve bug "vejo proxy
de UM mas o outro NAO ve" + "proxy fantasma do proprio player" -- causado
por server emitir S_SPAWN_PLAYER local=0 do proprio entityId via AOI re-emit
(workflow wcyeqgrad finding).

Source/ZMMO/Game/Network/ZeusWorldSubsystem.cpp
================================================
HandlePlayerSpawned guard novo logo apos EntityId==0 check:

  if (LocalEntityId != 0 && EntityId == LocalEntityId)
  {
      UE_LOG(LogZMMO, Verbose,
          TEXT("ignoring S_SPAWN_PLAYER for own LocalEntityId=%lld (bIsLocal=%d)"),
          LocalEntityId, bIsLocal ? 1 : 0);
      return;
  }

Sem o guard, server re-emit S_SPAWN_PLAYER local=0 do proprio entityId
(via AOI SetOnEnter callback em catch-up cross-server ou recompute pos-
handoff) criava proxy fantasma do proprio player. GAS/HUD/PlayerArray
roteava via fantasma e o pawn real perdia vinculo de identidade visivel
pros outros players.

Defesa em profundidade -- complementa fix server-side
(AoiReplicationHelpers.cpp:28 SendSpawnPlayerToConn agora tem skip-self
guard antes de construir payload) + SpawnKey dedupe sem IsLocal no plugin
(ZeusNetworkSubsystem.cpp:949 removeu IsLocal do hash).

Config/DefaultZeusV1.ini
=========================
Config inicial para UZeusNetworkingClientSubsystem (P9-6/P9-7 plugin V1).
Aponta para Gateway 127.0.0.1:7777 (fluxo padrao producao). Documenta
alternativa de direct connect 9001 (debug only -- bug Windows multi-NIC
loopback). bUseZeusNetworkingV1=True ativa V1 plugin em paralelo ao
legacy (transport V1 hoje quebrado via Gateway -- channelId nao eh
routing key, Phase 12 territory; gameplay continua via legacy ate la).

Validado smoke 2 PIE 2026-06-08 20:32
======================================
* SP_01: players=2 conns=2 + AOI players=2 maxSet=1 enters=5 hyst=92 stable
* UE: [PIE0] e [PIE1] prefix funcionando (commit server 54f2f9a ZNET_PIE_PREFIX)
* "S_SPAWN_PLAYER duplicate ignored" dedupe pegando re-emit (Batch 2)
* S_CHAR_INFO trocado correto: PIE0 ve Olatudook, PIE1 ve Mateuus
* Saida limpa: UnregisterViewer + Broadcast S_DESPAWN_PLAYER targets=1

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-06-08 20:33:41 -03:00
9422522d6d Merge pull request 'feat: GameplayAbilitySystem (client) - GAS client + Admin Tools + AOI debug' (#5) from GameplayAbilitySystem into main 2026-06-04 18:40:37 -03:00
c3b32bc1a1 feat(admin-tools): UZeusAOIComponent desenha as 2 zonas reais de AOI
Recebe a config de AOI do servidor (S_DEBUG_AOI_INFO via OnDebugAoiInfo) e
desenha 2 esferas com os raios REAIS: Zona de Interesse (interna, cyan) e
Zona de Despawn (AOI) (externa/histerese, laranja). Pede a config no BeginPlay
e ao ligar o overlay; nao desenha enquanto o raio == 0 (loading).

- Overlay EZeusAOIOverlay::SpawnZone -> DespawnZone (AOI).
- Expoe os raios via interface (GetInterestRadiusCm/GetDespawnRadiusCm) p/ o
  painel admin mostrar o numero (read-only — config e' do servidor).
- CVar zeus.debug.spawn -> zeus.debug.despawn.

A interface, o opcode (1700/1701) e o painel vivem no repo Server (plugins).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-04 17:53:46 -03:00
cda5fea776 feat(admin-tools): consumidor client do painel debug (F8 + UZeusAOIComponent)
- UZeusAOIComponent (Game/Network): componente de gameplay do player local com
  plus de debug. Overlays via DrawDebug* (esfera AOI real; spawn/cell/handoff
  placeholders) dirigidos pela UI via IZeusAOIDebugTarget + console vars. Estado
  persiste no componente entre aberturas do menu (IsOverlayEnabled).
- AZeusCharacter: anexa o componente + abre/fecha o painel no F8 (UIOnly trava o
  jogo; Esc/F8 fecham); LoadClass lazy do painel no content do PLUGIN
  (/ZeusAdminTools/UI/WBP_AdminToolsAIO).
- ZMMO.Build.cs: depende de ZeusAdminToolsRuntime.
- UIProgressBar_Base: rename EaseOutCubic -> EaseOutCubicPB (fix unity-build).
- ZMMO.uproject: remove entrada NwiroIntegrationKit.

A UI e o C++ do plugin ZeusAdminTools vivem no repo Server (plugin autocontido).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-04 16:19:37 -03:00
16de963301 fix(net): S_CHAR_INFO so' aplica em si mesmo (anti name overwrite por proxy)
Bug: handler aceitava qualquer S_CHAR_INFO do server (proprio + catch-up de
proxies pre-existentes) e sobrescrevia o PlayerState local. Resultado: nome
do ultimo proxy recebido virava o nome do char local na tela.

Fix: bail se InEntityId != ZeusEntityId (proxies remotos sao roteados pelo
registry no UZeusWorldSubsystem; nameplate por EntityId fica pra futuro).
2026-06-03 21:56:49 -03:00
8d73cc9df8 feat(gas): SM6 client EntityId u64 + M8 cue assets + UI/data tweaks (sessao 1+2) 2026-06-03 18:18:02 -03:00
d5402216a2 feat(gas): AZeusPlayerState implementa IAbilitySystemInterface
UE5 GAS nodes BP ("Get Ability System Component", "Apply Gameplay Effect To
Target", etc.) precisam que o PlayerState exponha o ASC via
IAbilitySystemInterface. Pattern Lyra: PlayerState dona a interface; ASC vive
como subobject do UZeusGASComponent (Component Registry).

Tres acessos:
- GetAbilitySystemComponent (interface, retorna base UAbilitySystemComponent)
- GetZeusAbilitySystemComponent (BlueprintPure, retorna UZeusAbilitySystemComponent
  custom com 12 overrides ZeusNetwork)
- GetZeusAttributeSet (BlueprintPure, helper pro AttributeSet tipado)

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-06-03 11:39:37 -03:00
ff75ad92eb feat(gas): Batch 2 - Vertical slice Dash client (input bind + assets)
ZeusCharacter:
- DashAction UPROPERTY + load /Game/Input/Actions/IA_Dash.IA_Dash no ctor.
- SetupPlayerInputComponent: BindAction(DashAction, Triggered) -> OnDashTriggered.
- OnDashTriggered: resolve UZeusGASComponent via PlayerState -> chama
  RequestActivateAbilityByTag(Zeus.Ability.Movement.Dash). Bridge envia
  C_ABILITY_TRY_ACTIVATE pro server (handle, tagHash) e o ASC local executa
  ActivateAbility (LaunchCharacter) quando server confirma S_ABILITY_ACTIVATED.

Assets (criados via MCP):
- DT_Abilities (/Game/ZMMO/Data/Abilities/) — row "Zeus.Ability.Movement.Dash"
  apontando pro BP_GA_Dash_C. Row name = tag canonica (hash bate com server).
- BP_GA_Dash (/Game/ZMMO/GAS/Abilities/) — parent UZeusGameplayAbility_Dash.
- IA_Dash (/Game/Input/Actions/) — Boolean InputAction.
- IMC_Default — bind LeftShift -> IA_Dash com trigger Pressed (13a mapping).

Fluxo completo (apos rebuild + server na branch GameplayAbilitySystem):
  LShift -> OnDashTriggered -> RequestActivateAbilityByTag ->
  C_ABILITY_TRY_ACTIVATE -> server valida cost SP/cooldown ->
  S_ABILITY_ACTIVATED + S_ATTRIBUTE_HP_SP_UPDATE (-10 SP) ->
  ASC.TryActivateAbility(local) -> BP_GA_Dash::ActivateAbility ->
  LaunchCharacter(forward * 2400 cm/s) + HUD SP cai 10 + cooldown 3s.

Branch GameplayAbilitySystem (criada a partir do main pro escopo GAS).

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-06-03 03:02:51 -03:00
98dce2f855 feat(zeus-gas): M7 - migrate HUD/Status to UZeusGASComponent + delete legacy
- ZeusHudWidget: porta de UZeusAttributeComponent legacy pra UZeusGASComponent
  (delegate OnSnapshotAppliedRaw via AddUObject; FZeusAttributesSnapshot
  construido pelo helper estatico FromPayload do plugin ZeusGAS). Logs
  diagnosticos no bind.
- UIPlayerStatus_Window: mesmo porte (snapshot raw + StatAllocReply). Lock
  de spam-click preservado.
- ZeusHudHpSpWidget: movido do plugin ZeusAttributes (deletado) pra ZMMO
  /Game/UI/InGame/. API preservada (WBP filhos nao precisam mudar).
- ZeusCharacter: tira o dual-seed legacy; so' seedea UZeusGASComponent
  via FindComponentByClass no HandleLocalSpawnReady.
- ZeusPlayerState/ZeusCharacter: comentarios atualizados pra refletir o
  UZeusGASComponent como dono unico.
- ZMMO.Build.cs: remove dep "ZeusAttributes", adiciona "ZeusGAS" +
  "GameplayAbilities".
- ZMMO.uproject: remove module "ZeusAttributes" (delete completo).
- DefaultGame.ini: remove linha legacy comentada do Component Registry.
- DefaultEngine.ini: adiciona [CoreRedirects] pra:
  * ZMMOJobs module + ZMMOJobDataAsset class (Package+Class) — 7 DA_Job_*
  * ZMMO.ZMMOMapDef struct (DT_Maps row)
  * ZMMO.ZMMOLoadingTipRow struct (DT_LoadingTips row)
  * ZMMO.ZMMOLoadingProfile/StepDef structs + EZMMOLoadingContext/StepStatus
    enums (DA_LoadingProfiles)
- DELETE: Source/ZeusAttributes/ inteiro (5 .cpp + 5 .h + Build.cs +
  module.json).

Pegadinha aprendida: ActiveClassRedirects so' renomeia classe (sem
package); CoreRedirects (ClassRedirects/StructRedirects/EnumRedirects/
PackageRedirects) e' obrigatorio quando muda modulo OU quando o asset
referencia USTRUCT/UENUM. Tipo errado nao da erro mas tambem nao funciona.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-06-03 01:26:04 -03:00
94442feca5 chore(uproject): rename plugin ZeusAbilities -> ZeusGAS
Servidor-side plugin foi renomeado de ZeusAbilities pra ZeusGAS (vai
absorver atributos + abilities + bridge ZeusNetwork). .uproject precisa
seguir o novo nome senao plugin deixa de carregar.

Server-side commit: ver Server repo branch GameplayAbilitySystem,
commit refactor(plugin): R.7 - rename plugin ZeusAbilities -> ZeusGAS.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-06-02 23:23:04 -03:00
6a6a28a086 refactor: prefixo ZMMO -> Zeus em classes/módulos C++ do cliente
- Renomeia ~50 classes UCLASS/USTRUCT/UENUM/UINTERFACE pra Zeus*
  (AZeusGameMode, AZeusCharacter, UZeusGameInstance, IZeusEntityInterface,
  UZeusWorldSubsystem, FZeusEntitySnapshot, etc.)
- Encurta AZMMOPlayerCharacter -> AZeusCharacter
- Renomeia módulos satélite ZMMOAttributes -> ZeusAttributes e
  ZMMOJobs -> ZeusJobs (pasta + .Build.cs + .uproject)
- Mantém módulo principal ZMMO (renomeia depois quando jogo ganhar nome)
- DefaultEngine.ini: ActiveClassRedirects ZMMOX -> ZeusX (preserva BPs/assets)
- DefaultGame.ini: sections atualizadas pra ZeusThemeSubsystem/ZeusPlayerState
- Category="ZMMO|..." -> "Zeus|..." em UPROPERTYs
- Preserva LogZMMO, ZMMO_API, /Script/ZMMO., /Game/ZMMO/, classes Target

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-06-02 17:24:38 -03:00
bd1dab25ca Merge branch 'feat/ui-system' into main
PR #4 — UI vitals/HUD redesign + V3 server meshing client-side.

# Conflicts:
#	Source/ZMMO/Game/UI/InGame/ZMMOStatusWindowWidget.cpp
2026-06-02 13:21:37 -03:00
e771aec259 wip: UI Button Base + content/config in-progress
Snapshot do trabalho em andamento na branch feat/ui-system: refinamentos do
UIButton_Base (header+impl), atualizacao de styles/themes do botao, mapa de
teste L_ZeusIATest, e demais .uasset/Config tocados durante a sessao de
playtest do meshing. Commit conjunto a pedido do usuario pra deixar a branch
limpa antes do proximo batch.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-06-02 12:56:28 -03:00
5e32cb0757 feat(network): migra entityId int32→int64 nos delegates do ZeusNetwork
V3 Server Meshing (server-side) precisa entityId 64-bit (high32 = FNV-1a do
worldId, low32 = counter local) pra evitar collision cross-ZS. Os delegates do
plugin (OnPlayerSpawned, OnPlayerDespawned, OnCharInfoReceived, OnPlayerStateUpdate,
OnHpSpUpdate, OnLevelUp) e os métodos TryGetLast* migram pra int64; handlers em
ZMMOPlayerCharacter, ZMMOWorldSubsystem, UIFrontEndFlowSubsystem e
ZMMOAttributeNetworkHandler acompanham. FCachedSpawn::EntityId,
FZMMOAttributesSnapshot::EntityId e SeedEntityId também viram int64.

Wire S_SPAWN_PLAYER ainda carrega uint32 (server faz XOR high32^low32 pra
manter unicidade estatística entre ZSs); ampliação do opcode pra uint64 fica
pra PR futuro.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-06-02 12:53:43 -03:00
e3aab64c8c fix(world): destroy proxy on DESPAWN + handle stale weak ptr on SPAWN
ZMMOWorldSubsystem mantinha proxies no mapa RemoteEntities apos DESPAWN
apenas marcando SetActorHiddenInGame(true) via SetEntityRelevant(false).
Entry ficava la apontando pra actor escondido — SPAWN subsequente do
mesmo entityId caia no fast-path "reactivar" e tudo bem... ate' o
TWeakObjectPtr ficar stale (GC, World Partition stream-out, etc): a
branch Contains(EntityId) saia com return incondicional sem spawnar
nada. Resultado: SPAWN silenciosamente ignorado, pawn permanente
invisivel ate' algo limpar a key.

Fixes:
1. HandlePlayerDespawned: Actor->Destroy() + RemoteEntities.Remove
   (destroi de verdade em vez de so esconder).
2. HandlePlayerSpawned: quando Contains mas weak ptr stale, remove a
   entry orfa e cai pro caminho de spawn novo — em vez de return.

Confirmado pelos logs de cliente durante teste de oscilacao do AOI
(servidor enviando DESPAWN+SPAWN ao oscilar ZI/ZD).

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-06-01 16:26:55 -03:00
b2c1153229 fix(frontend): HandleServerTravelRequested idempotente quando ja no mapa certo
Cliente fazia OpenLevel 2x:
1. CharSelect resolve mapId via DT_Maps -> TravelToMapById -> OpenLevel
2. Server confirma com S_TRAVEL_TO_MAP -> HandleServerTravelRequested ->
   OpenLevel **de novo**

O segundo OpenLevel destruia o World, PlayerState, AttributeComponent e HUD
que ja tinham sido criados/bindados. AttributeComponent novo iniciava com
snapshot vazio (entity=0, level=1, hp=0/0) — HUD ficava nesse default.
S_ATTRIBUTE_SNAPSHOT_FULL com dados reais chegava DEPOIS do reload, mas
HUD nao tinha sido recriado/reativado no novo mundo, entao HandleAttributesChanged
nunca rodava.

A deduplicacao em ZeusNetworkSubsystem (`S_TRAVEL_TO_MAP duplicate ignored`)
so pegava do 3o TRAVEL em diante — o 2o ja fazia o estrago.

Fix: compara nome curto do World atual (strip prefixo UEDPIE_N_ do PIE) com
o target. Se ja' estao no mesmo mapa, skipa OpenLevel e so seta state pra
InWorld — preservando HUD/PlayerState/AttributeComponent.
2026-05-31 23:43:13 -03:00
4e14f1feb6 refactor(charserver): World -> Realm + parse gatewayEndpoint + fix PendingLoadingSteps race
- ZMMOWorldEntry -> ZMMORealmEntry (RealmId + GatewayEndpoint single string)
- CharServerOpcodes: S_WORLD_* -> S_REALM_*
- UIServerSelectScreen: parse S_REALM_LIST com gatewayEndpoint
- UIUserLobbyScreen: parse S_CHAR_SELECT_OK, split gatewayEndpoint via FString::Split(":", FromEnd) — IPv6 tolerante
- UICharacterCreatePage: SubmitCreate envia realmId
- ZMMOCharSummary: WorldId -> RealmId
- UIFrontEndFlowSubsystem: GetSelectedRealmId; fix de race em PendingLoadingSteps_ (TSet snapshot via MoveTemp antes de iterar; evita access violation em TConstSetBitIterator durante async lambda)

Conferido E2E: 2 standalone instances entram simultaneamente em Provinces diferentes via Gateway.
2026-05-31 20:02:47 -03:00
ede49c1777 feat(ui/hud): barras EXP/JOB em % via exp-to-next do snapshot
FZMMOAttributesSnapshot ganha BaseExpToNext/JobExpToNext espelhando o wire do
server. ApplySnapshot liga ExpBar/JobBar pelo ratio (exp / exp-to-next) e os
readouts mostram a porcentagem (ou "MAX" no cap).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-28 03:58:33 -03:00
484c8d5d4f feat(ui/hud): readouts HP/SP + level multi-cor via RichText (DT_UI_VitalsReadout)
Readouts e LvlText do PlayerVitals viram RichTextBlock com markup inline,
estilizados pelo DT_UI_VitalsReadout (fonte JetBrains Mono Bold):
- HP/SP: atual (branco) + "/" (sep, #3a3830) + max (#6c6860)
- LVL: "LVL " (#b8b3a8) + numero (num, gold-bright #ffd888, maior)

Cores espelham hud_layout_modal_New.html (text-1/2/3/4 + gold-bright).
UZMMOHudPlayerVitalsWidget: HpReadout/SpReadout/ExpReadout/JobReadout/LvlText
agora URichTextBlock; ApplyHpSp/ApplySnapshot montam o markup.

EXP/JOB readout segue exp absoluto (sem %) ate o server enviar exp-to-next.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-28 03:06:44 -03:00
65edf1081b chore(ui/fonts): importa familias Cormorant Garamond, JetBrains Mono e Manrope
Fontes usadas pelos templates do designer (loading_screen.html,
hud_layout_modal_New.html): Cormorant Garamond (titulos serif), JetBrains Mono
(labels/readouts mono) e Manrope (corpo sans). Cada uma com Font composto +
FontFaces estaticos por peso.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-28 01:08:33 -03:00
e1fca1ae8d feat(ui/hud): painel de vitais (Nome/Job/Level + HP/SP/EXP) substituindo HpSpBar
UZMMOHudPlayerVitalsWidget (novo): sub-widget do HUD com 4 UUIProgressBar_Base
(HP/SP/BASE EXP/JOB EXP) + nome/classe/level. ApplySnapshot anima HP/SP via
SetTargetPrimaryLevel e formata os readouts; ApplyHpSp pro tick rapido.

UZMMOHudWidget propaga o snapshot pro PlayerVitals e resolve identidade: nome
do AZMMOPlayerState.CharName + job via UZMMOJobsLibrary.GetJobDisplayName(ClassId).
HpSpBar legado removido do WBP_HUD.

WBP_PlayerVitalsPanel: layout compacto (520w) com colunas alinhadas (label 28 /
bar FILL / readout 116 right-align) independente do tamanho do numero. Sem portrait.

MIs por barra: UI_M_ProgressBar_SP/BaseEXP/JobEXP/Loading (cores do
hud_layout_modal_New.html).

EXP/JOB readout mostra exp absoluto por ora — barras de EXP aguardam o servidor
enviar exp-to-next no snapshot pra calcular a porcentagem.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-28 00:34:03 -03:00
c9492a3910 feat(ui/progressbar): integracao com DT_UI_Styles (FUIStyleProgressBar multi-map + Variant)
FUIStyleProgressBar vira multi-map Hyper-style: TMap<FName, FUIStyleProgressBarVariant>
indexado por variante (HP/MP/XP/Cast/...). Cada variante carrega materiais por
layer (Fill/BG/TrackGrid/Edge/Shimmer) + style (DrawStyle/CornerRadius/Border) +
toggles de efeito.

UUIProgressBar_Base ganha bUseTheme + Variant (dropdown via meta=GetOptions
resolvendo o DT) + RefreshUIStyle() que aplica a variante via ThemeSubsystem
(runtime) ou DT direto (design-time), espelhando o padrao do UUIPanel_Base.
HandleThemeChanged reaplica em troca de tema.

EUIProgressBarStyle movido de UIProgressBar_Base.h para UIStyleTypes.h (usado
pela struct do DT).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-28 00:33:43 -03:00
88eb20eefa feat(ui/progressbar): edge (linha + glow) que acompanha PrimaryLevel + auto-hide nos extremos
Novo material UI_M_ProgressBar_Edge: linha vertical sharp no lado direito
do Fill (U = PrimaryLevel) com halo lateral configuravel.

Params:
- EdgeColour: cor + alpha da linha (default branco-creme 0.85)
- EdgeWidth: largura da linha em UV
- EdgeGlowWidth: largura do halo lateral (0 = sem glow)
- EdgeGlowIntensity: forca do halo (0..1)
- PrimaryLevel: posicao (sincronizada pelo C++)

WBP UI_ProgressBar_Master ganha EdgeImage entre Fill e Shimmer (Overlay).

C++ UUIProgressBar_Base:
- BindWidgetOptional EdgeImage + EdgeMID + UPROPERTYs override
- 5 setters runtime (Visible, Colour, Width, GlowWidth, GlowIntensity)
- ApplyPrimaryToMID propaga PrimaryLevel pro EdgeMID em cada tick de animacao
- UpdateEdgeVisibility() helper auto-oculta Edge quando PrimaryLevel <= 0
  ou >= 1 (linha encostaria na borda)

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-27 21:47:54 -03:00
8843ac3d1c feat(ui/progressbar): BG + TrackGrid + Shimmer com RoundedBox nativo do Slate
Substitui o SDF de cantos arredondados no material por DrawAs=RoundedBox
nativo do Slate (OutlineSettings.CornerRadii em pixel space, qualidade
perfeita independente do aspect ratio).

Adiciona 3 materiais separados pra layers de efeito:
- UI_M_ProgressBar_BG: fundo solido (1 param BackgroundColour)
- UI_M_ProgressBar_TrackGrid: linhas verticais a cada N divisoes
  (GridColour, GridDivisions, GridLineThickness)
- UI_M_ProgressBar_Shimmer: faixa de luz animada (Time node) deslizando
  E->D em loop (ShimmerColour, ShimmerWidth, ShimmerSpeed, ShimmerSoftness)

WBP UI_ProgressBar_Master ganha 3 UImage irmaos do FillImage no Overlay
(z-order: BG -> TrackGrid -> Fill -> Shimmer), todos com BindWidgetOptional.

C++ UUIProgressBar_Base:
- Enum EUIProgressBarStyle (Box/Rounded) controla DrawAs dos 4 brushes
- CornerRadius em pixels (0..100), aplicado via OutlineSettings.CornerRadii
- bShowBorder + BorderColour + BorderWidth pra outline so no BG
- bShowTrackGrid / bShowShimmer toggles (visibility do widget)
- ShimmerImage com inset = BorderWidth (slot padding + corner reduzido)
  pra nao sobrepor o outline do BG quando passa pelas extremidades
- UPROPERTYs override pra cada cor/param de cada material com
  InlineEditConditionToggle, setters runtime que persistem o flag

Inclui MI exemplo UI_M_ProgressBar_HP atualizada.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-27 21:26:50 -03:00
5d44647170 refactor(ui/panel): TMap<EUIPanelTexture> -> TMap<FName> com PostSerialize migration
FUIStylePanel migra de TMap<EUIPanelTexture, FSlateBrush> (enum legacy) para
TMap<FName, FSlateBrush>. Permite adicionar/remover categorias diretamente
no DT_UI_Styles via dropdown (meta=GetOptions) sem mexer no enum em C++.

PanelBackground, PanelOutline e PanelOutlineEffect viraram *_DEPRECATED.
Versoes novas: PanelBackgroundByName, PanelOutlineByName,
PanelOutlineEffectByName.

PostSerialize migra os legacy -> ByName no PostLoad de cada FUIStyleRow.
Apos o primeiro re-save no editor os deprecated ficam vazios e o codigo
vira no-op. PropertyRedirects em DefaultEngine.ini preservam a leitura
do asset antigo (3 entries: PanelBackground, PanelOutline, PanelOutlineEffect).

Atualiza UIPanel_Base pra ler ByName + DT_UI_Styles, UI_Panel_Master e
UI_PlayerStatus_Window resalvos. Inclui nova textura
T_Panel_Outline_Effect_Square_02.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-27 19:30:48 -03:00
1794e5d418 feat(ui/progressbar): UProgressBar -> UImage com material custom Primary/Secondary
UUIProgressBar_Base envolve UImage + MID em vez do UProgressBar nativo.
Material UI_M_ProgressBar expoe PrimaryLevel/SecondaryLevel (modelo
assimetrico: Secondary so aparece se > Primary), 5 cores (Primary +
ColourBottom, Secondary + ColourBottom, Empty) e gradient toggle com
bilinear remap controlado por GradientSplit.

Cores e gradient ficam expostos como UPROPERTY com InlineEditConditionToggle:
quando o toggle esta off usa o valor da MI atribuida no Brush, quando on
sobrescreve via MID em PreConstruct. Setters runtime tambem marcam o flag
pra persistir o override no proximo re-construct.

Wrappers semanticos: ShowHealPreview, ShowDamageLag, ShowBuffer, ClearSecondary.
NativeTick com ease-out cubica e duracoes separadas pra Primary e Secondary.

Inclui MI exemplo UI_M_ProgressBar_HP e WBP UI_ProgressBar_Master refatorado
(UProgressBar nativo substituido por UImage FillImage com brush apontando
para o material pai).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-27 19:29:43 -03:00
7fa10b4cb9 fix(ui/loading): memoiza steps quando widget em async load + draggable status
- UIFrontEndFlowSubsystem: HandlePostLoadMap/HandlePlayerSpawned agora
  registram o StepId em PendingLoadingSteps_ quando ActiveLoadingScreen
  ainda eh null (race com RequestAsyncLoad do WBP_Loading). Lambda do
  callback async drena o set apos criar a tela, fechando o loading
  retroativamente. Servidor rapido (TryFinalizeEnterWorld) deixava o
  cliente preso em "entrando no mundo" quando o async load atrasava.

- UIPlayerStatus_Window: herda de UIDraggableWindow_Base (drag + persist
  via UZMMOUISaveGame). Rename CloseBtn -> Button_Close, adiciona
  Background_Panel/Container_Header/Container_Stats, NativeOnKeyDown
  (Alt+A fecha), NativeGetDesiredFocusTarget=self. Remove bIsBackHandler
  pra parar o "Cannot create action binding" no log (projeto nao tem
  DefaultBackAction no CommonUISettings).

- UIInGameFlowSubsystem.SetState: skip re-push do HUD quando voltando de
  StatusWindow/Inventory/Menu pro Playing (Old != None && != Playing) pra
  evitar Construct novo do HUD e o flicker dos textos HP/SP.

- Novos: UIDraggableWindow_Base (drag generico + persistencia) e
  UIWindowSaveGame (TMap<FName, FVector2D> por WindowId).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-26 14:50:49 -03:00
079d05c117 feat(ui/loading): tela de loading dinamica data-driven (etapas + dicas)
- UUILoadingScreen_Base (Game/UI/Common/) generica: Configure(Context),
  MarkStepRunning/Done/Failed, OnLoadingComplete delegate, progress bar
  reativa e rotacao automatica de dicas via timer.
- Etapas configuradas por contexto em UZMMOLoadingProfilesDataAsset
  (DA_LoadingProfiles). Perfil inicial FrontEndEnteringWorld:
  Travel -> MapLoaded -> Spawn.
- Dicas em DT_LoadingTips (FText localizavel + filtro por contexto +
  GameplayTag de categoria). 4 dicas seed.
- UIFrontEndFlowSubsystem agora bloqueia a transicao InWorld ate o
  HandlePlayerSpawned (bIsLocal=true) — usuario nao ve mundo vazio.
- WBP_Loading em /UI/Shared/Loading/ (neutro, reusavel pelo InGame futuro);
  DA_FrontEndScreenSet[EnteringWorld] aponta pra ele.
- DefaultGame.ini wire LoadingProfilesAsset + LoadingTipsAsset.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-26 02:24:48 -03:00
90484876f0 feat(player): CharName do DB no PlayerState + header do Status Window
- PlayerState ganha CharName (Replicated) + SetCharInfo(CharName, GuildName)
- ZMMOPlayerCharacter assina FZeusOnCharInfoReceived; TryApplyCachedCharInfo
  resolve race se S_CHAR_INFO chega antes do pawn possuir
- UIPlayerStatus_Window header le PlayerState->CharName (fallback p/ GetPlayerName)
  em vez do "Mateuus-61E9B19A4FB2" generico

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-26 02:24:29 -03:00
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
477574f842 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:53:02 -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
bacf987e0b feat(jobs): novo módulo ZMMOJobs (peer de ZMMOAttributes) + DA_Job_Novice
Sub-módulo cliente do JobsSystem do server. Pavimenta dados visuais/UI por
job (DisplayName localizado, ícone, anim override, som de level up) sem
acoplar ao ZMMOAttributes.

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

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

NOVOS ARQUIVOS

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

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

MODIFICADOS

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

BLUEPRINT ACCESS

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

LOOKUP

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

VALIDAÇÃO

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

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

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-23 19:19:41 -03:00
dbc2898a4b feat(attributes/ui): toast "LEVEL UP!" no HUD via S_LEVEL_UP delegate
- UZMMOHudWidget ganha LevelUpToastContainer (BindWidgetOptional UWidget)
  e LevelUpToastText (BindWidgetOptional UTextBlock) — both Collapsed por
  default; HandleLevelUp os mostra com text "LEVEL UP!  Lv N" e dispara
  timer FTimerHandle pra esconder após LevelUpToastDurationSec (2.5s).
- WBP_HUD: Border escuro semi-transparente (R=0.05,G=0.04,B=0.10,A=0.85)
  com TextBlock dourado (R=1.0,G=0.85,B=0.30) fonte 32, slot top-center
  com padding top=120, visibility default Collapsed.
- Snapshot pós-level-up (vem logo após S_LEVEL_UP) atualiza HP/SP/level via
  HpSpBar automaticamente — toast é só feedback visual instantâneo.

Polish futuro (não desta frente): SFX cue + particle aura dourada +
animação de fade in/out via UMG.
2026-05-23 12:55:57 -03:00
5418da185e feat(attributes/ui): display "ATK base + bonus" igual RO + ASPD único
- FZMMOAttributesSnapshot ganha 7 pares (AtkBase/AtkEquipBonus, ...) +
  Aspd único, espelhando split do server.
- ZMMOAttributeNetworkHandler::ToSnapshot mapeia campos novos do
  FZeusAttributesPayload.
- WBP_StatusWindow / RefreshFromSnapshot agora formata derivados como
  "ATK  51 + 0", "CRIT 1.3 + 0.0", "ASPD 150" (este sem split — cálculo
  não-linear no server via aspd_base × stats).

Quando InventorySystem entrar, equipar arma muda `currentWeapon` no server,
Recalculate roda, e UI mostra ATK/MATK/DEF/etc com bonus > 0 e ASPD
ajustado pela aspd_base da arma equipada. Sem código de migração — schema
do snapshot já está estabilizado.
2026-05-23 11:47:10 -03:00
f585b0d2b9 feat(attributes/ui): Status Window com alocação STR/AGI/VIT/INT/DEX/LUK + derivados
- ZMMOStatusWindowWidget (UCommonActivatableWidget, modo Menu): grid 2 colunas
  estilo RO. Esquerda: stats primários com botões + por stat (RequestStatAlloc).
  Direita: derivados ATK/MATK/DEF/MDEF/HIT/FLEE/CRIT/ASPD vindos do snapshot
  do server — cliente NUNCA recalcula (anti-cheat foundation).
- Botão + envia C_STAT_ALLOC → server valida cost RO (floor((stat-1)/10)+2),
  aplica + recalcula derivados + SaveCharFull async, manda
  S_ATTRIBUTE_SNAPSHOT_FULL + reply. UI atualiza em tempo real via
  OnAttributesChanged.
- ZMMOAttributeComponent ganha RequestStatAlloc + NotifyStatAllocReply +
  delegate OnStatAllocReply para feedback de rejeição.
- ZMMOAttributeNetworkHandler: HandleStatAllocReply roteia pro componente do
  player local (reply não traz EntityId — sempre quem fez o request).
- PlayerController: hotkey Alt+A (FInputChord legacy + BindKey, sem precisar
  de IA asset). ToggleStatusWindow via UUIInGameFlowSubsystem.
- WBP_StatusWindow: layout 2 colunas via MCP set_widget_tree (43 widgets) +
  BgBorder escuro semi-transparente + centralizado no overlay.
- DA_InGameScreenSet: StatusWindow → WBP_StatusWindow.
2026-05-23 11:22:58 -03:00
396223e2a8 feat(ui-ingame): UI in-game system + PlayerState + Component Registry + HUD composite
Espelho simétrico do front-end pattern (UUIFrontEndFlowSubsystem +
DA_FrontEndScreenSet + UUIPrimaryGameLayout_Base) para a UI in-game.
Pavimenta caminho pra Inventory, Skills, StatusWindow, EscapeMenu, etc.

## UI in-game system (Source/ZMMO/Game/UI/InGame/)

- EZMMOInGameUIState enum (None, Playing, StatusWindow, Inventory,
  SkillTree, EscapeMenu, Loading) em Data/UI/InGameTypes.h
- UUIInGameScreenSet DataAsset: TMap<EZMMOInGameUIState, TSoftClassPtr<...>>
- UUIInGameFlowSubsystem (GameInstanceSubsystem) com SetState/ToggleScreen
- DA_InGameScreenSet em Content/ZMMO/UI/InGame/ mapeia Playing->WBP_HUD
- Layer routing: Playing -> UI.Layer.Game | menus -> GameMenu | loading -> Modal

## AHUD pattern (idiomatic UE5)

- AZMMOHUD : AHUD em Source/ZMMO/Game/Modes/
- AZMMOGameMode::HUDClass = AZMMOHUD::StaticClass()
- AZMMOHUD::BeginPlay chama UIInGameFlowSubsystem::StartInGame
- Pawn não cria mais HUD (separação de responsabilidades)

## PlayerState + Component Registry (Open-Closed)

- AZMMOPlayerState : APlayerState em Source/ZMMO/Game/Modes/
- AZMMOGameMode::PlayerStateClass = AZMMOPlayerState::StaticClass()
- UPROPERTY Config TArray<TSubclassOf<UActorComponent>> ComponentClasses
- DefaultGame.ini: +ComponentClasses=/Script/ZMMOAttributes.ZMMOAttributeComponent
- Ctor lê via GConfig (mais robusto que UPROPERTY Config + parsing) e
  instancia cada componente como CreateDefaultSubobject

Por que componentes no PlayerState e não no Pawn:
- Sobrevive ao despawn (morte/respawn)
- Spectator-friendly
- 1:1 com player (Pawn pode trocar: vehicle, mount, possession)
- Padrão MMO clássico (rathena, TrinityCore)

Adicionar novo módulo (Inventory, Skills, Guild) = 1 linha no
DefaultGame.ini, sem editar AZMMOPlayerState.cpp.

## HUD composite

- UZMMOHudWidget : UCommonActivatableWidget em ZMMO core (UI/InGame/)
- WBP_HUD em Content/ZMMO/UI/HUD/ herda UZMMOHudWidget
- HpSpBar (UZMMOHudHpSpWidget — sub-modulo ZMMOAttributes) embedded via
  BindWidgetOptional. Sub-widgets futuros: PlayerInfo, Minimap, QuickBar,
  Buffs, Chat, TargetInfo
- UZMMOHudHpSpWidget volta a ser UUserWidget puro (sub-widget, não root)
- UZMMOHudWidget::NativeOnActivated:
  - SetVisibility(HitTestInvisible) — mouse passa direto pro Pawn
  - BindToLocalPlayer: busca AttributeComponent no PC->PlayerState
  - Subscreve OnAttributesChanged/OnHpSpChanged/OnLevelUp
  - Propaga snapshots pra sub-widgets via HpSpBar->ApplySnapshot
- GetDesiredInputConfig override: ECommonInputMode::Game (sem cursor,
  movimento livre). Menus override pra GameAndMenu/Menu.

## NetworkHandler refatorado (AttributeComponent agora no PlayerState)

- ZMMOAttributeNetworkHandler::FindComponentForEntity: busca via
  GameState->PlayerArray (O(N_players)), não TActorIterator (O(N_actors))
- ZMMOAttributes.Build.cs += CommonUI, CommonInput

## EnsureRootLayout (fix crítico)

- OpenLevel invalida widgets no viewport ("InvalidateAllWidgets")
- RootLayout (criado pelo FrontEndFlow no boot) sobrevive como UObject
  (LocalPlayerSubsystem) mas fica órfão fora do viewport
- Push do WBP_HUD no Stack_Game funcionava mas widget pai (RootLayout)
  estava fora do viewport → invisível
- UUIFrontEndFlowSubsystem::EnsureRootLayout (público) recria via
  UIManagerSubsystem (idempotente)
- UUIInGameFlowSubsystem::StartInGame chama EnsureRootLayout antes do push

## PlayerCharacter simplificado

- Removido AttributeComponent (vai pro PlayerState via Registry)
- Removido HudHpSpWidgetClass + spawn manual de HUD
- HandleLocalSpawnReady só faz seed do EntityId via PlayerState
- UI lifecycle agora é responsabilidade do AZMMOHUD::BeginPlay

## Verificação (smoke test)

PIE: Login -> CharSelect -> Lobby -> EnterWorld
  ↓
LogZMMO: AZMMOPlayerState ctor: 1 componente(s) no registry
LogZMMO: ZMMOPlayerState: componente registrado [0] ZMMOAttributeComponent
LogZMMO: FrontEndFlow::EnsureRootLayout: RootLayout recriado
LogZMMO: InGameFlow: state None -> Playing
LogZMMO: InGameFlow: tela Playing pushed em UI.Layer.Game (widget=WBP_HUD_C_0)
LogZMMO: ZMMOHudWidget activated (HpSpBar=WBP_HUD_HpSpBar_C_0)
  ↓
HUD aparece com Lv 5 / HP 10/200 / SP 5/20 (valores do DB)
Regen funciona (HP/SP enchem a cada 6s/8s)
Movimento normal (input flui pro Pawn)

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-23 05:27:41 -03:00
7d83dd800f feat(attributes): cliente Fase 1 — sub-modulo ZMMOAttributes + HUD HP/SP
Espelho do AttributeSystem do servidor (Game/MMO/Modules/AttributeSystem).
Sub-modulo C++ Source/ZMMOAttributes/ (Runtime, LoadingPhase=PreDefault)
isolando o cliente do AttributeSystem em pasta propria — pavimentando o
caminho para futuros Inventory/Skills/Combat virem como sub-modulos peer.

Componentes:
  - FZMMOAttributesSnapshot (USTRUCT Blueprint, espelha
    FZeusAttributesPayload do plugin ZeusNetwork)
  - UZMMOAttributeComponent (UActorComponent ligado ao player):
    ApplySnapshot / ApplyHpSpUpdate / NotifyLevelUp + 3 delegates
    dinamicos (OnAttributesChanged / OnHpSpChanged / OnLevelUp) +
    helpers GetHpRatio / GetSpRatio
  - UZMMOAttributeNetworkHandler (UWorldSubsystem): ponte
    UZeusNetworkSubsystem -> componente via lookup por EntityId
  - UZMMOHudHpSpWidget (UUserWidget base): BindWidget (HpBar, SpBar) +
    BindWidgetOptional (HpText, SpText, LevelText). NativeConstruct
    binda nos delegates do componente; NativeDestruct desliga.
    OnSnapshotApplied / OnHpSpDelta (BlueprintNativeEvent) com impl
    C++ atualizando progress bars e textos.

Integracao no PlayerCharacter:
  - CreateDefaultSubobject<UZMMOAttributeComponent> no ctor
  - HudHpSpWidgetClass (TSubclassOf) resolvido via
    ConstructorHelpers::FClassFinder em /Game/ZMMO/UI/HUD/WBP_HUD_HpSpBar
  - HandleLocalSpawnReady seed do EntityId no componente + spawn do HUD
    (CreateWidget + AddToViewport + BindToAttributeComponent)
  - EndPlay remove widget do parent

WBP_HUD_HpSpBar (UMG, criado via MCP):
  - Overlay root + SizeBox (320x120) + VerticalBox
  - 5 widgets BindWidget: LevelText, HpBar (vermelho), HpText,
    SpBar (azul), SpText
  - Herda de UZMMOHudHpSpWidget

Verificacao (smoke test):
  - PIE login -> server envia S_ATTRIBUTE_SNAPSHOT_FULL
  - Cliente recebe via OnAttributeSnapshotFull -> handler roteia pra
    componente do entity -> ApplySnapshot dispara OnAttributesChanged ->
    widget atualiza ProgressBars + TextBlocks
  - HUD aparece no canto inferior esquerdo da tela com valores
    correspondentes ao DB

Refs:
  - Plano: C:/Users/mateu/.claude/plans/temos-uma-lab-input-nested-diffie.md
  - Server companion commit: feat(attributes): AttributeSystem modular

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-23 04:00:32 -03:00
0e96956a17 feat(char-spawn): Fase 4 client + UI WBPs paralelo (#2)
Co-authored-by: Mateus Rodrigues <mateuus27@outlook.com>
Co-committed-by: Mateus Rodrigues <mateuus27@outlook.com>
2026-05-23 02:16:27 -03:00
2cd8b3b2bb Merge pull request 'feat/frontend-mainmenu-skeleton' (#1) from feat/frontend-mainmenu-skeleton into main
Reviewed-on: #1
2026-05-19 02:06:46 -03:00
3889680469 feat(ui): painel como o Hyper — texturas reais + Panels editável + fixes
- Realoca as 13 texturas de painel do Hyper p/ /Game/ZMMO/UI/CommonUI/Textures/Panels/ (remove as procedurais)
- DT_UI_Styles.PanelBackground com as 9 entradas fiéis ao Hyper Survival_Gold (DrawAs=Image, Margin 0, tint por chave); PanelOutline vazio como no Hyper
- FUIPanelBrushSet (espelha Struct_UI_Style_Panels): só os 2 mapas
- UUIPanel_Base expõe Panels EditAnywhere (Details como no Hyper), semeado do tema; modo-textura só SetBrush (sem padding/visibility inventados)
- Design-time carrega DT_UI_Styles direto (Designer pinta igual ao runtime)
- Fix: outline ausente usa NoDrawType (antes pintava centro branco com FSlateBrush default)
- Remove membro morto Content/UNamedSlot (Hyper não usa NamedSlot)

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-19 02:03:09 -03:00
bc07c47557 feat(frontend): esqueleto CommonUI de MainMenu (boot→login→lobby)
Estrutura principal de front-end estilo Lyra portada para C++ proprio do
ZMMO (sem CommonGame), seguindo o padrao C++ Abstract -> WBP concreto:

- EZMMOFrontEndState / EZMMOLobbyPage e GameplayTags UI.Layer.* nativos
- UUIActivatableScreen_Base: base de tela (input config, hook de tema,
  voltar) espelhando o padrao de UUIButton_Base
- UUIPrimaryGameLayout_Base: switch principal (4 stacks CommonUI/camada)
- UUIManagerSubsystem (LocalPlayer): dono do root layout, idempotente
- UUIFrontEndFlowSubsystem (GameInstance): maquina de estados + driver
  de rede (Boot->Connecting->Login->ServerSelect->Lobby->EnteringWorld
  ->InWorld); Lobby e o hub logado, paginas internas = EZMMOLobbyPage
- UUIFrontEndScreenSet (DataAsset): mapa estado->tela, forge-ready
- AZMMOFrontEndGameMode + AZMMOFrontEndPlayerController
- GameInstance: bAutoConnectOnStart=false (conexao dirigida pelo fluxo)
- Build.cs: +GameplayTags; ARQUITETURA.md PR-first (s2.1/2.2/3.3/4.8/5)
- Assets: L_FrontEnd (GameMode via World Settings), WBP_PrimaryGameLayout,
  DA_FrontEndScreenSet; Config wiring

WBPs de pagina (Boot/Login/...) virao do Zeus UMG Forge depois (s4.8).
Smoke test PIE OK: root layout no viewport, fluxo Boot->Connecting,
no-op+log para telas nao configuradas, sem crash.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-19 01:56:16 -03:00
ca781eff83 feat(ui): texturas grunge Aurora Arcana + painel texturizado wired (DT+WBP+teste)
4 UTexture2D procedurais (grunge: base escura + value-noise 2 oitavas + vinheta + manchas + moldura arcana) em Content/ZMMO/UI/CommonUI/Textures/Panels (Square/Horizontal/Vertical + Outline_Gold; sRGB, UI, no-mip). DT_UI_Styles linha Default: Style.Panel.PanelBackground/PanelOutline com FSlateBrush (DrawAs Box/9-slice, Margin 0.22) -> ResourceObject resolvido p/ as texturas (verificado via read_data_table). UI_Panel_Master tree = Background(Border)->Outline(Border) (identico ao Hyper, sem NamedSlot). UI_Test_Panel: PanelA/B/Wide com PanelTexture (Square/Horizontal/Vertical_01). Play L_Test_UI mostra paineis texturizados (modo textura usa FUIStyle do subsystem em runtime).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-19 01:04:30 -03:00
ca117a559e feat(ui): painel texturizado (padrao Hyper) coexistindo com modo cor
EUIPanelTexture (None, Rectangle_Horizontal_01/02/03, Vertical_01..04, Square_01, Vertical_Footer_01) - espelha o enum Panel do Hyper. FUIStylePanel ganha TMap<EUIPanelTexture,FSlateBrush> PanelBackground + PanelOutline (modelo Struct_UI_Style_Panels), mantendo os tokens de cor. UUIPanel_Base: prop PanelTexture + border Outline; RefreshUIStyle tenta modo TEXTURA (Find brush nos mapas -> SetBrush em Background/Outline) e cai no modo COR (RoundedBox) se nao houver brush. Compila OK. Texturas Aurora Arcana proprias serao geradas (nao importadas do Hyper).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-19 00:52:10 -03:00
22fc0fbcc6 test(ui): UI_Test_Panel - SizeBox->UI_Panel_Master direto (paineis preenchem a celula)
Border do painel encolhia (NamedSlot vazio + slot Overlay nao-Fill). Agora cada SizeBox tem o UI_Panel_Master como filho unico -> SizeBox forca 360x200 / 740x160 -> Background preenche a celula e mostra o tema. Botoes (Confirmar/Cancelar Danger/Fechar Ghost) em BtnRow separada. Limite MCP: slot Fill / NamedSlot exigem o Designer.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-19 00:42:24 -03:00
78f2dd4144 test(ui): salva BP_TestUIHUD religado (BeginPlay->Create UI_Test_Panel->AddToViewport)
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-19 00:39:02 -03:00
1d363caa96 test(ui): UI_Test_Panel mock de conteudo (SizeBox [ ][ ]/[ ]) + HUD repontado
Layout: Row1 = CellA/CellB (SizeBox 380x240, Overlay = UI_Panel_Master + botoes por cima); CellWide (SizeBox 780x180, painel Secondary + 3 botoes). PanelB=Card, PanelWide=Secondary. BP_TestUIHUD religado (BeginPlay->Create(UI_Test_Panel)->AddToViewport via GetOwningPlayerController). Layout e mock grosseiro (limite do MCP p/ slots) - ajuste fino no Designer.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-19 00:38:44 -03:00
c6b0be92cc feat(ui): WBP UI_Panel_Master (parent UUIPanel_Base) Background->Content(NamedSlot)
Padrao replicado p/ 2a familia: C++ Abstract (UUIPanel_Base) -> WBP concreto. Arvore: Background(Border) -> Content(NamedSlot) via set_widget_tree; binds casam (Background/Content). Compila 0 erros, parent UUIPanel_Base. Designers injetam conteudo no NamedSlot Content; visual data-driven por FUIStyle.Panel/PanelType.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-19 00:29:17 -03:00
01c74c7683 feat(ui): UUIPanel_Base (UCommonUserWidget Abstract) data-driven por FUIStyle.Panel
Mesmo padrao do botao: C++ Abstract -> WBP concreto. PanelType (Primary/Secondary/Tertiary/Card) escolhe Bg/BgRaised/BgSunken/CardSelected; bRoundedBackground (RoundedBox + borda do tema); bUseSmallPadding. Background(UBorder) + Content(UNamedSlot) BindWidgetOptional. RefreshUIStyle resolve FUIStyle.Panel via UZMMOThemeSubsystem, reage a OnThemeChanged; BP_ApplyPanelStyle como hook. Compila OK.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-19 00:26:41 -03:00
f77223cfdb test(ui): setup PIE autocontido p/ UI_Test_Buttons (HUD+PC+GameMode+mapa)
BP_TestUIHUD: BeginPlay -> CreateWidget(UI_Test_Buttons, ctx/owner=GetOwningPlayerController) -> AddToViewport. BP_TestPC: bShowMouseCursor + click/mouseover events (p/ Hover/Pressed). GM_BP_TestGM: HUDClass=BP_TestUIHUD, PlayerControllerClass=BP_TestPC. L_Test_UI (Content/ZMMO/Debug/Maps): basic level com GameMode override = GM_BP_TestGM. Abrir L_Test_UI e dar Play exibe os botoes e permite testar estados. PIE confirmou HUD spawnado.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-19 00:22:27 -03:00
1ca546c9e9 test(ui): UI_Test_Buttons (variantes/estados/alinhamento) para smoke test
WBP em /Game/ZMMO/UI/Debug com 6 UI_Button_Master: Primary, Secondary, Danger, Ghost (so borda), Disabled (bIsEnabled=false), AlignLeft+Square (TextAlign=Left, bRoundedBackground=false). Compila 0 erros. Use o botao Play do WBP / PIE p/ exercitar Hover/Pressed/Disabled.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-19 00:12:32 -03:00
69fe754e34 feat(ui): estados Hover/Pressed/Disabled no botao (ApplyVisualState + NativeOn*)
ApplyVisualState(estado) re-resolve o tema e pinta o Background (RoundedBox/Box) com fill+borda do estado: Normal=BgNormal/BorderNormal, Hovered=BgHover/BorderHover, Pressed=BgPressed/BorderHover, Disabled=BgDisabled/BorderNormal. Overrides do UCommonButtonBase: NativeOnHovered/Unhovered/Pressed/Released(volta p/ Hovered se IsHovered senao Normal)/Enabled/Disabled. RefreshUIStyle agora chama ApplyVisualState(Normal). Compila OK.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-19 00:01:18 -03:00
2da2bc7101 feat(ui): UI_Button_Master tree com widget Background (rename de Bg) + CDO
set_widget_tree renomeia o Border Bg->Background (casa o BindWidgetOptional novo); CDO reaplicado (Style=BSB_Button_Transparent_C, ButtonTextValue, bRoundedBackground=true). Compila 0 erros.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-18 23:55:07 -03:00
c974efdbc8 feat(ui): Background (rename Bg) com RoundedBox + borda na cor da variante
- Bg -> Background (nome descritivo). - RefreshUIStyle: monta o brush do Background como RoundedBox quando bRoundedBackground (default true): TintColor=BgNormal, OutlineSettings(CornerRadii=Button.CornerRadius, Color=BorderNormal, Width=Button.BorderWidth, RoundingType=FixedRadius). 'So borda com a cor' = variante com BgNormal transparente + BorderNormal na cor (data-driven, como o Hyper). bRoundedBackground=false -> caixa reta. Compila OK.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-18 23:48:35 -03:00
98aaac57aa feat(ui): text style CommonTextStyle por variante + reorg assets CommonUI/Style
- BST_Button (UCommonTextStyle, Rajdhani Bold 15) + BSB_Button_Transparent (UCommonButtonStyle) movidos p/ Content/ZMMO/UI/CommonUI/Style/{Text,Button}/ (espelha Hyper). - UI_Button_Master: arvore CommonTextBlock dentro de TextScaleBox (auto-size) via set_widget_tree; Style->BSB_Button_Transparent_C. - DT_UI_Styles linha Default: Style.Button.<variante>.TextStyle -> BST_Button_C (por variante). - ARQUITETURA.md: prefixos BSB_/BST_ (§3.2) + pasta UI/CommonUI/Style + UI/Shared + UI/Fonts (§2.1). Pipeline conferido via read_data_table (4 variantes apontam BST_Button no path novo, cores Aurora Arcana intactas).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-18 23:39:24 -03:00
2064c37bc3 feat(ui): text style via CommonTextStyle por variante (padrao Hyper) + align + auto-size
FUIStyleButtonVariant.Font (FSlateFontInfo) -> TSubclassOf<UCommonTextStyle> TextStyle (idiomatico CommonUI, como BP_Style_Text_* do Hyper; theme-driven via DT_UI_Styles por variante). ButtonText volta a UCommonTextBlock; RefreshUIStyle: SetStyle(variante.TextStyle) + cor do FUIStyle por cima (FUIStyle manda na cor por tema). Mantidos TextAlign (ETextJustify L/C/R via SetJustification + slot do Overlay) e bAutoSizeText (TextScaleBox ScaleToFit/None) — ortogonais ao CommonTextStyle. ButtonTextValue default 'Button Text' agora no header.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-18 23:12:14 -03:00
abeec244d5 feat(ui): UI_Button_Master estilizado (FUIStyle gold) + style transparente; remove WBP base redundante
- UMG nao permite WBP-com-arvore herdar de WBP-com-arvore: a base abstrata e SO a classe C++ UUIButton_Base. UI_Button_Master reparentado direto p/ /Script/ZMMO.UIButton_Base; WBP UI_Button_Base (redundante) deletado. - UI_Button_Master: arvore propria (SizeBox_Master/Bg/ContentOverlay/ButtonText/Number_Text/Image_Icon/InputActionWidget) via set_widget_tree; ButtonTextValue='Button Text'. - BS_Button_Transparent (novo CommonButtonStyle: brushes NoDraw + padding 0) atribuido ao Style -> remove o fundo branco padrao do UCommonButtonBase; so o Bg (FUIStyle) aparece. - DT_UI_Styles linha Default: Button agora com cores Aurora Arcana (Primary dourado etc.). - ARQUITETURA.md §3.3 corrigido (regra UMG: C++ abstrato -> WBP concreto, variantes sao WBPs irmaos). Compila 0 erros; visual confirmado (dourado + BUTTON TEXT, sem moldura).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-18 22:58:10 -03:00
5a8dd796de fix(ui): defaults reais de cor das variantes do botao (Aurora Arcana)
FUIStyleButton ganha construtor que preenche Primary/Secondary/Danger/Ghost com as cores do styles.css (.btn-*). Antes as variantes vinham Transparent/White (placeholder) -> botao saia branco no designer (sem GameInstance usa defaults do struct). Agora ja sai estilizado (Primary=dourado, texto ~#0A0A12) mesmo sem DataTable.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-18 22:42:22 -03:00
f8f00f9b9d feat(ui): UI_Button_Base tree reconstruida via set_widget_tree (casa BindWidget)
Arvore: SizeBox_Master(SizeBox) -> Bg(Border) -> ContentOverlay(Overlay) -> [ButtonText, Number_Text, Image_Icon (TextBlock/TextBlock/Image), InputActionWidget (CommonActionWidget)]. Todos como variaveis -> ligam nos BindWidgetOptional do UUIButton_Base + InputActionWidget do UCommonButtonBase. UI_Button_Base Abstract + parent UUIButton_Base preservados; UI_Button_Master (concreto) herda; ambos compilam UpToDate 0 erros. Montado pela nova tool set_widget_tree do NwiroIntegrationKit.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-18 22:32:30 -03:00
4992a56eff fix(ui): ButtonText usa UTextBlock (CommonTextBlock desnecessario; estilo via FUIStyle)
Como o estilo do botao e dirigido pelo FUIStyle (nao pelo CommonTextStyle), trocar UCommonTextBlock->UTextBlock no BindWidgetOptional simplifica e casa com as tools de WBP. Sem perda de comportamento.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-18 22:11:31 -03:00
91b080aa68 feat(ui): porte 1:1 do UI_Button_Master (Hyper) para C++ auto-configuravel
UUIButton_Base enriquecido com a logica do UI_Button_Master do Hyper, lida via MCP hyperpro e convertida para C++ (Struct_Style_Button do Hyper substituido pela camada FUIStyle por tema): SetButtonText/SetNumberText/SetIcon/ApplySizeBox/ApplyInputActionStyle/InitializeButton + timeout (delegates OnButtonActivatedByTimeout/OnButtonTimerUpdated). UPROPERTYs editaveis (Button/Text/Number/Icon/Size/InputActionStyle/Timer) auto-aplicados em NativePreConstruct/NativeConstruct. BindWidgetOptional: SizeBox_Master/ButtonText(CommonTextBlock)/Number_Text/Image_Icon/Bg. InputActionWidget acessado por GetWidgetFromName (privado no UCommonButtonBase). Compila+linka OK.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-18 21:58:10 -03:00
4ea5131475 feat(ui): WBP UI_Button_Base (Abstract) + UI_Button_Master
UI_Button_Base: WidgetBlueprint abstrato, parent = UUIButton_Base (C++); arvore RootPanel(Overlay) -> Bg(Border) -> Label(TextBlock), Bg/Label como variaveis (ligam no BindWidgetOptional do C++). UI_Button_Master: child concreto de UI_Button_Base (botao reutilizavel do jogo). Ambos compilam UpToDate, 0 erros. Em /Game/ZMMO/UI/Shared, prefixo UI_ conforme convencao.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-18 21:45:40 -03:00
ab74ef8b17 feat(ui): fundacao CommonUI + UUIButton_Base (Abstract)
Habilita CommonUI como base da UI do ZMMO (decisao de arquitetura): modulos CommonUI/CommonInput no Build.cs + include path Game/UI/Widgets; CommonGameViewportClient no DefaultEngine.ini. Adiciona EUIButtonVariant (Primary/Secondary/Danger/Ghost). Cria UUIButton_Base (UCLASS Abstract : UCommonButtonBase): CommonUI cuida de input/foco/click; o C++ resolve os tokens via UZMMOThemeSubsystem::GetActiveUIStyle() e reage a OnThemeChanged; entrega ao WBP por BP_ApplyUIStyle (split contrato C++ / visual Blueprint). CommonInput data/settings deferido p/ a fiacao de input.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-18 21:28:45 -03:00
1c06d69174 feat(ui): sistema de UI Style (tokens C++ por tema) + fontes Aurora Arcana
Camada de estilo data-driven do cliente ZMMO, portando o nucleo do projeto de referencia (Hyper) para C++ conforme ARQUITETURA.md (BP Struct proibido):

- Source/ZMMO/Data/UI: UIStyleTypes.h (enums EUI*), UIStyleTokens.h (tokens primitive/semantic/component), UIStyleRow.h (FUIStyle + FUIStyleRow). - ZMMOThemeSubsystem: GetActiveUIStyle() resolve DT_UI_Styles por ThemeId com fallback Default. - Fontes: 8 UFontFace Inline (Cinzel/Rajdhani) em Content/ZMMO/UI/Fonts + DT_UI_Styles preenchido. - ARQUITETURA.md: prefixo de widget UI_, classes base UUI*_Base (UCLASS Abstract) sem prefixo ZMMO. - Config/DefaultGame.ini: UIStyleTable apontando DT_UI_Styles. - Scripts/import_fonts.py (importador idempotente). Inclui tambem o scaffolding do modulo ZMMO ainda nao versionado (Source/ZMMO/Data, Game/UI, Content/ZMMO, uproject, Build.cs).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-18 21:21:32 -03:00
50471831a2 chore: remove conteudo template da Epic (Variant_*)
Remove os pacotes de exemplo Variant_Combat, Variant_Platforming e Variant_SideScrolling (mais os External Actors/Objects associados) que vieram do template Third Person e nao fazem parte do cliente ZMMO. Ajusta Config/DefaultEditor.ini de acordo.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-18 21:21:01 -03:00
1067 changed files with 20351 additions and 1151 deletions

4
.gitignore vendored
View File

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

11
.ignore Normal file
View File

@@ -0,0 +1,11 @@
/.git
/.vs
/.vscode
/Content
/DerivedDataCache
/Intermediate
/Saved
/Plugins/VisualStudioTools/azure-pipelines
/Plugins/VisualStudioTools/Docs
/Plugins/VisualStudioTools/Intermediate
/Plugins/VisualStudioTools/Scripts

25
.mcp.json Normal file
View File

@@ -0,0 +1,25 @@
{
"mcpServers":
{
"nwiro":
{
"type": "http",
"url": "http://localhost:5353/mcp"
},
"hyperpro":
{
"type": "http",
"url": "http://localhost:5354/mcp"
},
"ZeusIntelligence":
{
"type": "http",
"url": "http://127.0.0.1:5058/mcp"
},
"zeus":
{
"type": "http",
"url": "http://127.0.0.1:5058/mcp"
}
}
}

744
ARQUITETURA.md Normal file
View File

@@ -0,0 +1,744 @@
# Arquitectura do Cliente ZMMO
> Regras de organização do projecto Unreal `ZMMO` (Content + Source) e tutorial
> de como adicionar gameplay novo seguindo o padrão. **Este documento é
> normativo.** Qualquer estrutura nova no projecto tem de respeitar estas
> regras, ou justificar a excepção por escrito.
Última actualização: 2026-05-11.
> **Extensões da arquitetura** (documentos normativos paralelos, escopo específico):
> - [`ARQUITETURA_SERVER_SELECT.md`](ARQUITETURA_SERVER_SELECT.md) — Server Select, lista de mundos, handoff CharServer↔WorldServer, multi-region, queue, Valkey. Roadmap em 7 fases.
> - [`ARQUITETURA_CHARACTER_MODEL.md`](ARQUITETURA_CHARACTER_MODEL.md) — Stats primários (STR/AGI/VIT/INT/DEX/LUK), classes (Novato → especializações), fórmulas de stats derivados (ATK/MATK/DEF/...), `jobs.yml` data-driven, stat allocation server-side. Espelha rathena.
> **Sumário de mudanças (2026-05-11):** adicionada secção de **temas sazonais
> de UI** (`UI/Themes/`), com tutorial §4.7, regra de "no hard ref a textura
> em widget" em §5 e política de activação em §1.10.
---
## 1. Princípios fundamentais
Estas são as regras que **não negociamos**:
1. **Namespace do projecto.** Todo o conteúdo do jogo vive sob
`Content/ZMMO/`. Nada de assets soltos na raiz `Content/`.
2. **Organizar por feature, não por tipo de asset.** Não criamos pastas
`Meshes/`, `Textures/`, `Materials/` no nível alto. O Content Browser já
filtra por prefixo (`SM_`, `T_`, `M_`...). Pastas existem para refletir
**domínio de gameplay**.
3. **Self-contained por entidade.** Cada Mob, NPC ou Item vive numa pasta
própria que contém **todos os seus assets** (malha, animações, materiais,
texturas, áudio, VFX). Apagar a pasta = remover a entidade do jogo, sem
referências órfãs espalhadas.
4. **Plano (máximo 3 níveis úteis).** Se está a aninhar mais que isso,
provavelmente está a organizar errado. Excepção: dentro de uma entidade
self-contained, sub-pasta `Animations/` é permitida quando o número de
anims ultrapassa 56.
5. **DataTable manda em catálogo, Blueprint manda em comportamento.** Dados
tabulares (stats, drops, preços, descrições) vão em
`Content/ZMMO/Data/**/DT_*.uasset`. Comportamento vai em Blueprints
herdados das bases em `Core/`. Nunca misturar.
6. **Structs e Enums vivem em C++.** Toda `USTRUCT` (row de DataTable
inclusive) e toda `UENUM` é declarada em `Source/ZMMO/Data/`. Blueprint
Structs estão **proibidos** — eles corrompem em refactor e não dão tipos
fortes.
7. **Hierarquia de Entity.** Tudo que vive no mundo herda da mesma raiz:
`BP_Entity_Base` → (`BP_Player_Base` | `BP_NPC_Base` | `BP_Mob_Base`) →
instâncias concretas. Espelha a classe C++ `AZMMOEntity` em
`Source/ZMMO/Game/Entity/`.
8. **Lazy loading via `TSoftObjectPtr`.** Linhas de DataTable e referências
entre features usam soft pointers. Nada de hard ref para BPs de mob/item a
partir da DT mestre — caso contrário carregamos o mundo inteiro ao abrir o
inventário.
9. **`Developers/` e `ExternalContent/` fora do namespace `ZMMO/`.** Não
entram no cook (`Developers/`) e não poluem o nosso namespace
(`ExternalContent/` para Fab/Megascans/Marketplace).
10. **Temas de UI são data-driven, não hard-coded.** Widgets não têm hard
ref para textura/som sazonal — pedem ao `UZMMOThemeSubsystem` via uma
chave declarada em `EZMMOThemeKey` (C++). Veja §1.10 abaixo para a
política de activação.
### 1.10. Política de activação de temas
A UI suporta **temas** (`Default`, `Christmas`, `Halloween`, etc.). A escolha
do tema activo segue esta **ordem de precedência** em runtime:
1. **Override de desenvolvimento** (`DefaultGame.ini`, chave `ActiveTheme=`).
Só lido fora de builds Shipping. Para QA testar Natal em Maio.
2. **Resposta autoritativa do servidor.** Quando o servidor Zeus envia o
opcode `ServerHello` com `ThemeId`, ele vence. É o único caminho válido
em produção.
3. **Calendário local do cliente** (`DT_ThemeCalendar`). Fallback antes do
login (tela de login/character-select aparece antes do servidor falar) e
se o servidor não enviar `ThemeId`.
4. **`DA_Theme_Default`.** Último recurso. Sempre presente, sempre carregado.
**Por que servidor vence:** MMO precisa ligar Halloween fora da data
canónica, fazer evento-surpresa, A/B test de tema sem patch do cliente.
**Por que data local existe:** sem ela, jogador que abre o jogo em 24 de
Dezembro vê tema Default até autenticar — quebra a magia sazonal.
**Por que .ini só fora de Shipping:** evita que um jogador edite o ficheiro
e force um tema que não devia ter acesso (cosméticos pagos, por exemplo).
---
## 2. Estrutura de pastas
### 2.1. `Clients/ZMMO/Content/`
```
Content/
├── ZMMO/ ← namespace do projecto
│ │
│ ├── Core/ ← bases que outros assets herdam
│ │ ├── Entity/
│ │ │ ├── BP_Entity_Base.uasset ← herda AZMMOEntity (C++)
│ │ │ └── BPI_Entity.uasset ← interface comum
│ │ ├── Player/
│ │ │ └── BP_Player_Base.uasset
│ │ ├── NPC/
│ │ │ └── BP_NPC_Base.uasset
│ │ ├── Mob/
│ │ │ ├── BP_Mob_Base.uasset
│ │ │ ├── BT_Mob_Default.uasset ← Behavior Tree base
│ │ │ └── BTT_Mob_*.uasset ← Tasks compartilhadas
│ │ ├── Item/
│ │ │ ├── BP_Item_Base.uasset
│ │ │ └── BPI_Item.uasset
│ │ └── Network/ ← debug do UZMMOWorldSubsystem
│ │
│ ├── Entities/ ← TODAS as instâncias concretas
│ │ ├── Players/
│ │ │ └── Classes/
│ │ │ ├── Warrior/
│ │ │ │ ├── BP_Player_Warrior.uasset
│ │ │ │ ├── SK_Warrior.uasset
│ │ │ │ ├── ABP_Warrior.uasset
│ │ │ │ ├── M_Warrior.uasset
│ │ │ │ ├── T_Warrior_*.uasset
│ │ │ │ └── Animations/
│ │ │ ├── Mage/
│ │ │ └── Archer/
│ │ │
│ │ ├── NPCs/
│ │ │ ├── Vendor_Blacksmith/
│ │ │ │ ├── BP_NPC_Blacksmith.uasset
│ │ │ │ ├── SK_Blacksmith.uasset
│ │ │ │ ├── ABP_Blacksmith.uasset
│ │ │ │ └── ...
│ │ │ └── Questgiver_Elder/
│ │ │
│ │ └── Mobs/
│ │ ├── Beasts/ ← família (organiza, não herda)
│ │ │ ├── Wolf_Gray/ ← pacote completo do mob
│ │ │ │ ├── BP_Mob_Wolf_Gray.uasset
│ │ │ │ ├── SK_Wolf.uasset
│ │ │ │ ├── PHY_Wolf.uasset
│ │ │ │ ├── M_Wolf.uasset
│ │ │ │ ├── MI_Wolf_Gray.uasset
│ │ │ │ ├── T_Wolf_*.uasset
│ │ │ │ ├── Animations/
│ │ │ │ │ ├── ABP_Wolf.uasset
│ │ │ │ │ ├── AS_Wolf_Skel.uasset
│ │ │ │ │ ├── AM_Wolf_AttackBite.uasset
│ │ │ │ │ └── A_Wolf_{Idle,Run,Hit,Death}.uasset
│ │ │ │ ├── Audio/
│ │ │ │ └── VFX/
│ │ │ ├── Bear_Brown/
│ │ │ └── Boar_Wild/
│ │ ├── Undead/
│ │ ├── Humanoids/
│ │ ├── Dragons/
│ │ └── Bosses/
│ │
│ ├── Data/ ← apenas DataTables (.uasset)
│ │ ├── Items/
│ │ │ ├── DT_Items_Master.uasset
│ │ │ ├── DT_Items_Weapons.uasset
│ │ │ ├── DT_Items_Armor.uasset
│ │ │ ├── DT_Items_Consumables.uasset
│ │ │ └── DT_ItemDrops.uasset
│ │ ├── Mobs/
│ │ │ ├── DT_Mobs_Master.uasset
│ │ │ └── DT_MobSpawns.uasset
│ │ ├── Players/
│ │ │ ├── DT_Classes.uasset
│ │ │ └── DT_ExpCurve.uasset
│ │ ├── NPCs/
│ │ │ ├── DT_NPCs.uasset
│ │ │ └── DT_Dialogs.uasset
│ │ ├── Abilities/
│ │ │ ├── DT_Abilities.uasset
│ │ │ └── DT_StatusEffects.uasset
│ │ ├── Quests/
│ │ │ ├── DT_Quests.uasset
│ │ │ └── DT_QuestRewards.uasset
│ │ └── World/
│ │ └── DT_Zones.uasset
│ │
│ ├── Items/ ← assets visuais (modelo/icon dos itens)
│ │ ├── Equipment/
│ │ │ ├── Weapons/
│ │ │ │ ├── Sword_Iron/
│ │ │ │ │ ├── SM_Sword_Iron.uasset
│ │ │ │ │ ├── M_Sword_Iron.uasset
│ │ │ │ │ ├── T_Sword_Iron_*.uasset
│ │ │ │ │ └── T_Icon_Sword_Iron.uasset
│ │ │ │ └── Bow_Hunter/
│ │ │ ├── Armor/
│ │ │ └── Accessories/
│ │ ├── Consumables/
│ │ │ ├── Potion_Health/
│ │ │ └── Potion_Mana/
│ │ ├── Materials/
│ │ └── Quest/
│ │
│ ├── Abilities/ ← VFX/Cues/Montagens (data fica em Data/)
│ │ ├── Combat/ Buffs/ Heals/ Movement/
│ │ └── Shared/
│ │
│ ├── World/
│ │ ├── Maps/ ← L_World_Open, L_City_*, L_Dungeon_*
│ │ ├── Instances/
│ │ ├── Environment/ ← Architecture/ Nature/ Props/
│ │ └── Interactables/ ← baús, portas, warps, recursos
│ │
│ ├── Maps/ ← mapas que NÃO são de mundo
│ │ └── FrontEnd/ ← L_FrontEnd (boot/menu; GameMode via
│ │ World Settings = AZMMOFrontEndGameMode)
│ │
│ ├── UI/ ← HUD, Login, CharacterSelect, Inventory,
│ │ │ Chat, Party, Guild, Trade, Quest, Map,
│ │ │ Shared, Icons
│ │ ├── Shared/ ← WBPs reutilizáveis (UI_Button_Master,
│ │ │ UI_Panel_Master, UI_Spinner_Master,
│ │ │ UI_CheckBox_Master, UI_Input_Master)
│ │ ├── FrontEnd/ ← WBP_PrimaryGameLayout, DA_FrontEndScreenSet,
│ │ │ telas refinadas (Boot/Login/… vindas do Forge)
│ │ ├── Fonts/ ← Font_*/FF_* (Cinzel, Rajdhani)
│ │ ├── CommonUI/Style/ ← estilos CommonUI (espelha o Hyper)
│ │ │ ├── Button/ ← BSB_* (UCommonButtonStyle)
│ │ │ └── Text/ ← BST_* (UCommonTextStyle)
│ │ └── Themes/ ← temas sazonais (data-driven, ver §1.10)
│ │ ├── DA_Theme_Default.uasset ← sempre carregado, fallback
│ │ ├── DA_Theme_Christmas.uasset
│ │ ├── DA_Theme_Halloween.uasset
│ │ ├── DT_ThemeCalendar.uasset ← datas → ThemeId (fallback local)
│ │ ├── _Shared/ ← assets reutilizados entre temas
│ │ ├── Default/ ← assets do tema base
│ │ │ ├── HUD/ Login/ Inventory/
│ │ │ ├── Audio/ MS_MainMenu.uasset
│ │ │ └── VFX/
│ │ ├── Christmas/ ← sazonal
│ │ │ ├── HUD/ Login/ Inventory/
│ │ │ ├── Audio/ MS_MainMenu_Christmas.uasset
│ │ │ └── VFX/ NS_Snow.uasset
│ │ └── Halloween/ ... ← mesma estrutura
│ ├── Effects/ ← VFX globais (Niagara)
│ ├── Audio/ ← Music, Ambient, SFX, Voice
│ ├── Input/ ← Enhanced Input
│ ├── Cinematics/ ← Sequencer
│ ├── Movement/ ← Mounts, Transport
│ ├── Localization/ ← StringTables
│ └── Debug/ ← Maps de smoke test
├── Developers/ ← rascunho por dev (não cooka)
│ └── <nick-do-dev>/
├── AutoCreated/ ← saída CRUA do Zeus UMG Forge (HTML→WBP).
│ └── <batch>/ Quarentena: refina-se e MOVE p/ ZMMO/UI/
│ FrontEnd/ antes de produção (não cooka cru)
├── ExternalContent/ ← Fab / Megascans / Marketplace
│ ├── Fab/ Megascans/ Marketplace/
├── Collections/
└── __ExternalActors__/ __ExternalObjects__/ ← World Partition (não mexer)
```
### 2.2. `Clients/ZMMO/Source/ZMMO/`
```
Source/ZMMO/
├── ZMMO.Build.cs
├── ZMMO.h
├── ZMMO.cpp
├── Game/ ← gameplay runtime (já existe)
│ ├── Entity/ ← AZMMOEntity + derivados
│ ├── Controller/ ← AZMMOPlayerController,
│ │ AZMMOFrontEndPlayerController
│ ├── Modes/ ← GameMode + GameInstance,
│ │ AZMMOFrontEndGameMode
│ ├── Network/ ← UZMMOWorldSubsystem
│ └── UI/ ← runtime de UI (não é contrato de dados)
│ ├── ZMMOThemeSubsystem.* ← resolução de tema
│ ├── Widgets/ ← UUIButton_Base, UUIPanel_Base,
│ │ UUISpinner_Base, UUICheckBox_Base,
│ │ UUIInput_Base (variantes Box/Outline/
│ │ Underline/Search), UUICommonText_Base
│ │ (EUITextRole → FUIStyle.Text)
│ └── FrontEnd/ ← infra de navegação CommonUI:
│ UUIActivatableScreen_Base,
│ UUIBootScreen_Base (1ª tela),
│ UUILoginScreen_Base (2ª tela),
│ CharServerOpcodes.h (faixa 2000-2099),
│ UUIPrimaryGameLayout_Base,
│ UUIManagerSubsystem (LocalPlayer),
│ UUIFrontEndFlowSubsystem (GameInstance),
│ UUIFrontEndScreenSet (UDataAsset)
└── Data/ ← contrato de dados (USTRUCT + UENUM)
├── ZMMOData.h ← header agregador (opcional)
├── Items/
│ ├── ItemRow.h ← FItemRow : public FTableRowBase
│ └── ItemTypes.h ← E_ItemType, E_ItemRarity, E_EquipSlot
├── Mobs/
│ ├── MobRow.h ← FMobRow
│ └── MobTypes.h ← E_MobFaction, E_MobBehavior
├── Players/
│ ├── ClassRow.h ← FClassRow
│ └── StatBlock.h ← FStatBlock (HP/MP/Sta/Atk/Def…)
├── NPCs/
│ ├── NPCRow.h
│ └── DialogRow.h
├── Abilities/
│ ├── AbilityRow.h
│ └── StatusEffectRow.h
├── Quests/
│ └── QuestRow.h
├── UI/
│ ├── ThemeKeys.h ← EZMMOThemeKey (HUD_Frame, Login_BG…)
│ ├── ThemeRow.h ← FThemeEntry (Key → SoftAsset)
│ ├── ThemeCalendarRow.h ← FThemeCalendarRow (data → ThemeId)
│ ├── FrontEndTypes.h ← EZMMOFrontEndState, EZMMOLobbyPage
│ └── UILayerTags.h/.cpp ← GameplayTags nativos UI.Layer.*
└── World/
└── ZoneRow.h
```
Regra: **toda DT em `Content/ZMMO/Data/X/`** tem a `Row Structure` definida no
`.h` correspondente em **`Source/ZMMO/Data/X/`**. Espelhamento 1-para-1 nos
nomes das sub-pastas.
O subsystem do tema (`UZMMOThemeSubsystem`) vive em
`Source/ZMMO/Game/UI/` — é gameplay/runtime, não contrato de dados, por isso
não fica em `Data/`.
---
## 3. Convenções de nomenclatura
### 3.1. Pastas
- `PascalCase`, sem espaços, sem acentos.
- Nome no singular para categoria conceitual (`Mob`, `Item`, `Quest`).
- Nome no plural para colecção de instâncias (`Mobs/`, `Items/`, `Quests/`).
- Família de mob/item usa nome temático: `Beasts/`, `Undead/`, `Weapons/`.
- Instância individual usa `Tipo_Variante`: `Wolf_Gray`, `Sword_Iron`,
`Vendor_Blacksmith`.
### 3.2. Assets (prefixo obrigatório)
| Prefixo | Tipo | Exemplo |
|---------|-----------------------------------|-------------------------------|
| `BP_` | Blueprint Class | `BP_Mob_Wolf_Gray` |
| `BPI_` | Blueprint Interface | `BPI_Entity` |
| `UI_` | Widget Blueprint (estilo Hyper) | `UI_Button_Base`, `UI_Inventory_Slot` |
| `ABP_` | Animation Blueprint | `ABP_Wolf` |
| `BT_` | Behavior Tree | `BT_Mob_Default` |
| `BTT_` | Behavior Tree Task | `BTT_Mob_ChasePlayer` |
| `BTS_` | Behavior Tree Service | `BTS_Mob_FindTarget` |
| `BTD_` | Behavior Tree Decorator | `BTD_Mob_CanSeeTarget` |
| `BB_` | Blackboard | `BB_Mob_Default` |
| `SK_` | Skeletal Mesh | `SK_Wolf` |
| `SKEL_` | Skeleton | `SKEL_Wolf` |
| `SM_` | Static Mesh | `SM_Sword_Iron` |
| `PHY_` | Physics Asset | `PHY_Wolf` |
| `M_` | Material | `M_Wolf` |
| `MI_` | Material Instance | `MI_Wolf_Gray` |
| `MF_` | Material Function | `MF_NormalBlend` |
| `T_` | Texture | `T_Wolf_Diffuse` |
| `RT_` | Render Target | `RT_MiniMap` |
| `Font_` | Font (UFont, Composite) | `Font_Cinzel` |
| `FF_` | Font Face (UFontFace, Inline) | `FF_Cinzel_Bold` |
| `BSB_` | Common Button Style (UCommonButtonStyle) | `BSB_Button_Transparent` |
| `BST_` | Common Text Style (UCommonTextStyle) | `BST_Button` |
| `NS_` | Niagara System | `NS_Wolf_Death` |
| `NE_` | Niagara Emitter | `NE_Sparks_Small` |
| `SC_` | Sound Cue | `SC_Wolf_Growl` |
| `SW_` | Sound Wave | `SW_Wolf_Growl_01` |
| `MS_` | MetaSound Source | `MS_AmbientForest` |
| `MSA_` | Mix MetaSound Asset | `MSA_CombatMix` |
| `A_` | Animation Sequence | `A_Wolf_Idle` |
| `AM_` | Animation Montage | `AM_Wolf_AttackBite` |
| `AS_` | Anim State / Skeleton ref | `AS_Wolf_Skel` |
| `IA_` | Input Action | `IA_Move` |
| `IMC_` | Input Mapping Context | `IMC_Default` |
| `DA_` | Data Asset (PrimaryDataAsset) | `DA_Quest_FirstSteps` |
| `DT_` | Data Table | `DT_Items_Master` |
| `CT_` | Curve Table | `CT_DamageFalloff` |
| `C_` | Curve | `C_AttackPower` |
| `L_` | Level / Map | `L_World_Open` |
| `LS_` | Level Sequence | `LS_OpeningCinematic` |
| `GE_` | Gameplay Effect *(se GAS)* | `GE_StatusBleed` |
| `GA_` | Gameplay Ability *(se GAS)* | `GA_Fireball` |
| `GC_` | Gameplay Cue *(se GAS)* | `GC_Hit_Blood` |
### 3.3. C++ (`Source/ZMMO/Data/`)
- Struct: `F<Nome>Row` quando é linha de DataTable. Ex: `FItemRow`, `FMobRow`.
- Struct genérica (não-DT): `F<Nome>`. Ex: `FStatBlock`.
- Enum: `E<Nome>`. Sempre `UENUM(BlueprintType)` e tipado: `enum class EItemType : uint8`.
- Header path espelha a pasta: `Source/ZMMO/Data/Items/ItemRow.h`
`#include "Data/Items/ItemRow.h"`.
**Exceção — classes base de UI (widgets):** a família de UI **não** leva o
prefixo `ZMMO` (ao contrário de `AZMMOEntity`/`UZMMOGameInstance`). Classe C++
base de widget = `UUI<Componente>_Base`, sempre `UCLASS(Abstract)` (não
instanciável direto; só os filhos derivam — padrão `_Abstract` do Hyper).
Ex.: `UUIButton_Base`, `UUIPanel_Base`, `UUIProgressBar_Base`.
**Sub-regra — infra de navegação de front-end:** as classes que orquestram a
navegação CommonUI seguem o mesmo prefixo `UUI` (sem `ZMMO`) por coerência com
a família de UI, **mesmo não sendo widgets**. Assim: o layout raiz é
`UUIPrimaryGameLayout_Base` (Abstract, widget), a base de tela é
`UUIActivatableScreen_Base` (Abstract, widget), e os subsistemas de UI são
`UUIManagerSubsystem` (`ULocalPlayerSubsystem`) e `UUIFrontEndFlowSubsystem`
(`UGameInstanceSubsystem`). Atores de gameplay do front-end (GameMode,
PlayerController) **mantêm** o prefixo `AZMMO` — não são UI, espelham
`AZMMOGameMode`: `AZMMOFrontEndGameMode`, `AZMMOFrontEndPlayerController`.
**Regra do UMG (importante):** um Widget Blueprint **não pode** herdar de outro
Widget Blueprint que tenha árvore de widgets (UMG aborta a compilação: "apenas
um deles deve ter árvore"). Portanto a camada abstrata é **só a classe C++**
`UUI<X>_Base`. O WBP concreto (prefixo `UI_`, ver §3.2) herda **direto da
classe C++**: `UUIButton_Base` (C++ Abstract) → `UI_Button_Master` (WBP, árvore
própria). Variantes por feature são **WBPs irmãos** sobre a mesma classe C++
(`UI_Button_<Feature>` : `UUIButton_Base`), cada um com a própria árvore
(montada via `set_widget_tree`) — nunca encadeando WBPs.
---
## 4. Tutoriais
### 4.1. Adicionar um Mob novo (ex: `Wolf_Gray`)
**Pré-requisito:** já existe `FMobRow` em
`Source/ZMMO/Data/Mobs/MobRow.h` e `DT_Mobs_Master` em
`Content/ZMMO/Data/Mobs/`.
1. Criar a pasta `Content/ZMMO/Entities/Mobs/Beasts/Wolf_Gray/`.
2. Importar **todos** os assets do lobo dentro dela:
- `SK_Wolf`, `SKEL_Wolf`, `PHY_Wolf`.
- `T_Wolf_Diffuse`, `T_Wolf_Normal`, `T_Wolf_ORM`.
- `M_Wolf` e a variante `MI_Wolf_Gray`.
- `Animations/`: `ABP_Wolf`, `AS_Wolf_Skel`, `A_Wolf_*`, `AM_Wolf_*`.
- `Audio/`: `SC_Wolf_Growl`, `SC_Wolf_Footstep`.
- `VFX/`: `NS_Wolf_Death`.
3. Criar `BP_Mob_Wolf_Gray` **herdando** de `Core/Mob/BP_Mob_Base`.
4. Atribuir no BP: `SK_Wolf` no Mesh, `ABP_Wolf` no AnimClass,
`BT_Mob_Default` (ou um BT próprio se necessário) no AIController.
5. Abrir `Content/ZMMO/Data/Mobs/DT_Mobs_Master.uasset`. Adicionar linha
`Wolf_Gray`:
- `BlueprintRef` = `BP_Mob_Wolf_Gray` (via `TSoftObjectPtr` — soft).
- `Stats` = `FStatBlock` com HP/Atk/Def adequados.
- `Faction` = `EMobFaction::Hostile`.
- `DropTableRow` = `WolfDrops` (linha em `DT_ItemDrops`).
6. Se for dropar loot novo, abrir `DT_ItemDrops` e adicionar a linha
`WolfDrops` com array de `FItemRow` ids + chances.
**Regra:** nunca colocar um asset de `Wolf_Gray` fora de
`Entities/Mobs/Beasts/Wolf_Gray/`. Apagar a pasta tem de remover o mob por
inteiro.
### 4.2. Adicionar um Item novo (ex: `Sword_Iron`)
**Pré-requisito:** `FItemRow` em `Source/ZMMO/Data/Items/ItemRow.h`,
`DT_Items_Master` em `Content/ZMMO/Data/Items/`.
1. Criar `Content/ZMMO/Items/Equipment/Weapons/Sword_Iron/`.
2. Importar `SM_Sword_Iron`, texturas `T_Sword_Iron_*`, material
`M_Sword_Iron`, e o ícone `T_Icon_Sword_Iron`.
3. Abrir `DT_Items_Master`. Adicionar linha `Sword_Iron`:
- `Type` = `EItemType::Weapon`.
- `Rarity` = `EItemRarity::Common`.
- `EquipSlot` = `EEquipSlot::MainHand`.
- `WorldMesh` = `SM_Sword_Iron` (soft).
- `Icon` = `T_Icon_Sword_Iron` (soft).
- `DisplayName`, `Description` via `StringTable` em `Localization/`.
**Sem Blueprint por item.** O item é "data only" — quando precisar de
comportamento especial (ex: poção que cura), criar `BP_Item_Potion_Health`
herdando de `Core/Item/BP_Item_Base` e apontar a coluna `BehaviorClass` da
linha para esse BP. A maioria dos itens **não** tem BP próprio.
### 4.3. Adicionar uma Classe de Player (ex: `Warrior`)
1. Criar `Content/ZMMO/Entities/Players/Classes/Warrior/`.
2. Importar `SK_Warrior`, anims, materiais, etc. (mesmo padrão self-contained
do mob).
3. Criar `BP_Player_Warrior` herdando de `Core/Player/BP_Player_Base`.
4. Adicionar linha `Warrior` em `DT_Classes`:
- `BlueprintRef` = `BP_Player_Warrior` (soft).
- `BaseStats` = `FStatBlock`.
- `LevelUpStats` = `FStatBlock` (delta por nível).
- `StartingAbilities` = `TArray<FName>` de ids em `DT_Abilities`.
### 4.4. Adicionar um NPC (ex: `Vendor_Blacksmith`)
1. Criar `Content/ZMMO/Entities/NPCs/Vendor_Blacksmith/` (self-contained).
2. Criar `BP_NPC_Blacksmith` herdando de `Core/NPC/BP_NPC_Base`.
3. Adicionar linha `Vendor_Blacksmith` em `DT_NPCs`:
- `BlueprintRef` = `BP_NPC_Blacksmith` (soft).
- `Role` = `ENPCRole::Vendor`.
- `ShopInventory` = `TArray<FName>` de ids em `DT_Items_Master`.
- `DialogRoot` = id em `DT_Dialogs`.
### 4.5. Adicionar uma Row Struct em C++
Exemplo: nova struct `FCraftRecipeRow` para receitas de crafting.
1. Criar `Source/ZMMO/Data/Crafting/CraftRecipeRow.h`:
```cpp
#pragma once
#include "CoreMinimal.h"
#include "Engine/DataTable.h"
#include "CraftRecipeRow.generated.h"
USTRUCT(BlueprintType)
struct ZMMO_API FCraftRecipeRow : public FTableRowBase
{
GENERATED_BODY()
UPROPERTY(EditAnywhere, BlueprintReadOnly)
FName OutputItem;
UPROPERTY(EditAnywhere, BlueprintReadOnly)
int32 OutputQuantity = 1;
UPROPERTY(EditAnywhere, BlueprintReadOnly)
TArray<FName> Ingredients;
};
```
2. Recompilar o módulo `ZMMO`.
3. No editor: `Content/ZMMO/Data/Crafting/``Add → Miscellaneous → Data Table`
→ escolher `CraftRecipeRow` como Row Structure → salvar como
`DT_Crafting_Recipes`.
### 4.6. Conectar uma DataTable a um Blueprint em runtime
```cpp
// Em BP_Mob_Base (ou na sua classe C++ AZMMOMobBase):
//
// UPROPERTY(EditDefaultsOnly, Category="Data")
// TSoftObjectPtr<UDataTable> MobsTable;
//
// UPROPERTY(EditAnywhere, Category="Data")
// FName MobId;
//
// void AZMMOMobBase::BeginPlay()
// {
// Super::BeginPlay();
// if (UDataTable* Table = MobsTable.LoadSynchronous())
// {
// if (FMobRow* Row = Table->FindRow<FMobRow>(MobId, TEXT("Mob init")))
// {
// ApplyStats(Row->Stats);
// Faction = Row->Faction;
// DropTableRowId = Row->DropTableRow;
// }
// }
// }
```
Regra: o `MobId` é setado pelo `UZMMOWorldSubsystem` quando o servidor envia
o opcode de spawn. O cliente nunca instancia mob "por nome" — sempre via id.
### 4.7. Adicionar um tema sazonal (ex: `Christmas`)
**Pré-requisito:** já existem `EZMMOThemeKey` em
`Source/ZMMO/Data/UI/ThemeKeys.h`, `FThemeEntry` em `ThemeRow.h`, e o
`UZMMOThemeSubsystem` em `Source/ZMMO/Game/UI/`.
1. Criar a pasta `Content/ZMMO/UI/Themes/Christmas/` com sub-pastas que
espelham as features de UI a substituir: `HUD/`, `Login/`, `Inventory/`,
`Audio/`, `VFX/`. Não precisa criar sub-pastas para features que o tema
**não** altera.
2. Importar os assets variantes lá dentro. Manter os **mesmos nomes** que os
equivalentes em `Themes/Default/` ajuda a comparar:
- `Themes/Default/HUD/T_HUD_Frame.uasset`
`Themes/Christmas/HUD/T_HUD_Frame.uasset`.
3. Criar `DA_Theme_Christmas.uasset` em `Content/ZMMO/UI/Themes/` como
`PrimaryDataAsset` herdando da classe `UZMMOThemeDataAsset` (a classe
vive em `Source/ZMMO/Game/UI/`).
4. Preencher o `TMap<EZMMOThemeKey, FThemeEntry>` do data asset **apenas com
as chaves que o tema sobrescreve**. As que ficam vazias caem
automaticamente para `DA_Theme_Default`.
5. Adicionar linha em `DT_ThemeCalendar`:
- `ThemeId` = `Christmas`.
- `StartDate` = `12-15`.
- `EndDate` = `01-05`.
- `Priority` = `10` (caso duas datas se sobreponham).
6. No servidor Zeus, quando quiser activar o tema (autoritativo), enviar o
opcode `ServerHello` com `ThemeId = "Christmas"`. Em ausência disso, o
cliente usa o calendário do passo 5.
**Adicionar uma chave nova de tema** (ex: tela de loading sazonal):
1. Abrir `Source/ZMMO/Data/UI/ThemeKeys.h` e adicionar o valor ao enum:
```cpp
UENUM(BlueprintType)
enum class EZMMOThemeKey : uint8
{
HUD_Frame,
Login_Background,
Login_Logo,
Inventory_SlotEmpty,
LoadingScreen, // <-- nova chave
MainMenu_Music,
};
```
2. Recompilar o módulo `ZMMO`.
3. Abrir `DA_Theme_Default` no editor e preencher a nova chave **obrigatoriamente**
(Default tem de ter todas).
4. Preencher nos temas sazonais apenas se quiser substituir.
**Regra de ouro do tema:** widget consome via subsystem, nunca por hard ref.
```cpp
// Em vez de:
// UPROPERTY(EditAnywhere) TObjectPtr<UTexture2D> HudFrameTex;
//
// Faz:
// const UZMMOThemeSubsystem* Theme = GetGameInstance()->GetSubsystem<UZMMOThemeSubsystem>();
// SetBrushFromSoftTexture(Theme->GetTexture(EZMMOThemeKey::HUD_Frame));
```
O subsystem aplica fallback internamente: pergunta ao tema activo; se a
chave não estiver preenchida, devolve a do `Default`.
### 4.8. Adicionar uma tela de front-end (ex: `Login`)
**Pré-requisito:** já existem `UUIActivatableScreen_Base` e
`UUIFrontEndFlowSubsystem` em `Source/ZMMO/Game/UI/FrontEnd/`, o
`WBP_PrimaryGameLayout` e o `DA_FrontEndScreenSet` em
`Content/ZMMO/UI/FrontEnd/`.
Fluxo geral: **Boot → Connecting → Login → ServerSelect → Lobby → EnteringWorld
→ InWorld**. O **Lobby** é o hub principal logado; suas páginas internas
(CharacterSelect, CharacterCreate, Shop, Settings…) são `EZMMOLobbyPage`,
navegadas por um stack CommonUI **dentro** da tela do Lobby — não são estados
de topo do fluxo.
**Rede do pré-login:** o `UUIFrontEndFlowSubsystem` dirige a conexão ao
**`UZeusCharServerSubsystem`** (WebSocket, `CharServerUrl` de Project Settings,
default `ws://127.0.0.1:7100/zeus`) ao entrar em **Boot** — NÃO o
`UZeusNetworkSubsystem` (UDP, world server, usado só no travel
EnteringWorld→InWorld). A tela Boot (`UUIBootScreen_Base`) observa o
CharServer: libera o botão "Iniciar" no `OnConnected`; o clique chama
`UUIFrontEndFlowSubsystem::RequestEnterLogin()` → estado Login. Tela nunca
segura hard-ref de asset de tema (cores via `FUIStyle`; §5).
**Login:** a tela `UUILoginScreen_Base` autentica pelo MESMO WebSocket do
CharServer enviando `C_CHAR_AUTH_REQUEST` (opcode 2000, ver
`CharServerOpcodes.h`) com um token. Escopo atual = **dev**: token `dev:<n>`
aceito pelo `StubTokenValidator` (senha ignorada no stub). `S_CHAR_AUTH_OK`
→ `RequestEnterServerSelect()` → estado ServerSelect; `S_CHAR_AUTH_REJECT`
→ mensagem de erro na própria tela. Auth real (HTTP `/auth/login` + JWT)
fica para tarefa dedicada. "Voltar" → `RequestBack()` (Login→Boot, CharServer
permanece conectado).
1. Prototipar a tela em HTML/CSS em `Tools/Templates/MMO_Widget/` (ou usar os
templates já existentes).
2. Rodar o Zeus UMG Forge (HTML→UMG). Saída crua: `Content/AutoCreated/<batch>/`.
3. Abrir o `WBP_*` gerado e **reparent** a classe pai para
`UUIActivatableScreen_Base` (a base é `UCommonActivatableWidget`; o WBP tem
árvore própria — respeita §3.3, nunca encadeia WBP→WBP).
4. **Reconectar tema:** o Forge gera hard ref de textura (proibido §5).
Substituir por consumo via `UZMMOThemeSubsystem` por `EZMMOThemeKey` no
override de `RefreshUIStyle`; remontar botões/painéis com os widgets
compartilhados (`UI_Button_Master`, `UI_Panel_Master`).
5. **Mover** o asset refinado de `Content/AutoCreated/…` para
`Content/ZMMO/UI/FrontEnd/` (o cru não cooka).
6. Registrar no `DA_FrontEndScreenSet`: `StateScreens[EZMMOFrontEndState::Login]`
= soft class do WBP refinado (páginas do Lobby vão no map de
`EZMMOLobbyPage` da tela de Lobby). Sem registro, o flow faz no-op + log.
**Regra:** uma tela nunca segura hard ref a asset de tema (textura/som/VFX) —
sempre via `UZMMOThemeSubsystem` por `EZMMOThemeKey` (igual §4.7 e §5).
---
## 5. O que **NÃO** fazer
- **Não** criar pastas `Meshes/`, `Textures/`, `Materials/` no nível alto.
- **Não** criar `Blueprint Struct` no editor. Toda struct é C++.
- **Não** referenciar BPs de mob/item com `TObjectPtr<>` (hard ref) a partir
de DataTables ou Subsystems globais — usar `TSoftObjectPtr<>`.
- **Não** misturar assets de uma entidade com assets de outra. Cada
Wolf_Gray, Sword_Iron, etc. é uma pasta isolada.
- **Não** colocar nada na raiz `Content/`. Tudo é `Content/ZMMO/...` ou
`Content/{Developers,AutoCreated,ExternalContent}/...`. `AutoCreated/` é
quarentena do Zeus UMG Forge: refina-se e **move** para `ZMMO/UI/FrontEnd/`
antes de produção; o cru não cooka.
- **Não** importar conteúdo do Fab/Megascans directo para dentro de `ZMMO/`.
Vai sempre para `ExternalContent/` e, se quiser usar, **referencia** dali.
- **Não** colocar dados tabulares em Blueprint (lista hardcoded de itens,
array de stats no ABP). Move para DataTable.
- **Não** duplicar `FStatBlock` em C++ se já existe — importar de
`Source/ZMMO/Data/Players/StatBlock.h`.
- **Não** colocar hard ref de textura/som de tema dentro de um Widget
(`TObjectPtr<UTexture2D> ChristmasFrame` é proibido). Widgets pedem ao
`UZMMOThemeSubsystem` via `EZMMOThemeKey`. Hard ref a asset de tema
bloqueia a troca em runtime e quebra a política de fallback. Vale também
para **telas de front-end** (Boot/Login/ServerSelect/Lobby/…): a saída crua
do Forge traz hard ref — tem de ser reconectada ao subsystem antes do merge
(ver §4.8).
- **Não** criar chave de tema "ad-hoc" como `FName("HUD.Frame")`. Toda chave
é entrada do enum C++ `EZMMOThemeKey`. String literal é proibida — o
compilador tem de quebrar quando alguém esquecer de adicionar a chave.
- **Não** preencher um tema sazonal com **todas** as chaves só porque o
Default preenche. Tema sazonal só lista o que **substitui**; o resto cai
no Default automaticamente.
---
## 6. Excepções permitidas
- Dentro de uma entidade self-contained, sub-pasta `Animations/` é OK quando
passa de 56 anims. O mesmo vale para `Audio/` e `VFX/` próprios da
entidade.
- `Items/.../Equipment/Weapons/Sword_Iron/` mantém o ícone `T_Icon_*` junto
do mesh — é a única excepção em que textura e mesh ficam na mesma pasta
raiz da entidade (porque o ícone é parte do item, não um asset solto).
- Skeleton compartilhado entre variantes (ex: `Wolf_Gray`, `Wolf_Black`)
pode viver em `Entities/Mobs/Beasts/_Shared/Wolf/` se as variantes
reutilizarem o mesmo `SK_` e `Animations/`. Pasta começa com `_` para
ordenar primeiro no Content Browser.
---
## 7. Referências externas
- [Allar UE5 Style Guide](https://github.com/Allar/ue5-style-guide) — base
das nossas convenções de nomenclatura e estrutura.
- [Epic Unreal Engine Directory Structure](https://dev.epicgames.com/documentation/en-us/unreal-engine/unreal-engine-directory-structure).
- [Lyra Project Structure](https://github.com/iliags/LyraGameVault/blob/main/Lyra%20Project%20Structure.md)
— modelo Epic para projecto multiplayer escalável.
- [Hyperdense UE5 folder structure best practices](https://medium.com/@sarah.hyperdense/ue5-project-folder-structure-and-organization-best-practices-b9e487c330a3).
- [Epic Forum Using DataTables, Structs and DataAssets for Inventory](https://forums.unrealengine.com/t/using-data-tables-structs-and-data-assets-for-an-inventory/2381408).
- [Epic Forum Item system: how to structure/store data](https://forums.unrealengine.com/t/item-system-how-to-structure-store-data/2170364).
---
## 8. Como mudar este documento
Este `.md` é a fonte de verdade da arquitectura do cliente. Mudanças
estruturais (nova pasta no nível alto, mudança de convenção de nome, novo
tipo de asset) **precisam de PR alterando este ficheiro primeiro**. Sem PR
neste documento, não se mexe na árvore real.

View File

@@ -0,0 +1,891 @@
# Arquitetura: Character Model — Stats, Jobs, Fórmulas
> **Status:** documento normativo paralelo ao `ARQUITETURA.md`. Define como o modelo de personagem do ZMMO funciona — atributos, classes, derivações, autoridade. Espelha as decisões do `.bases/rathena/rathena-master` mas adaptado ao stack TS + UE5.7 do Zeus.
> **Escopo:** persistência + autoridade + onde tudo é calculado. **NÃO cobre** balanceamento de gameplay (decisão de game design — vai num doc de design separado).
## 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 **Novato** (Novice) e especializa em outras classes ao chegar a critérios (job level + quest de mudança).
Este doc fixa:
1. O que persiste no MySQL (CharServer) vs o que vive em memória (WorldServer).
2. Como classes são definidas (data-driven, não tabela DB).
3. Como atributos primários geram derivados (fórmulas espelhando rathena).
4. Onde acontece o cálculo (autoridade).
5. Como fluxos de criação/alocação/level up funcionam end-to-end.
---
## 1. Princípios
### 1.1 Persistência flat — TUDO na tabela `characters`
Padrão da indústria emuladora (rathena, TrinityCore, AzerothCore, MaNGOS). Ver `.bases/rathena/rathena-master/sql-files/main.sql:209-296`.
- ✅ 1 query lê o char completo (spawn, list, transfer).
- ✅ Writeback é 1 UPDATE única.
- ❌ Sem entidade `character_stats` separada (1:1 FK seria JOIN inútil — stats não são reutilizáveis entre chars, é relacionamento estritamente 1:1).
### 1.2 Stats primários persistem; derivados são sempre recalculados
| Categoria | Persiste no MySQL | Recalcula em runtime (WorldServer) |
|---|---|---|
| **Primários** (STR/AGI/VIT/INT/DEX/LUK) | ✅ | — |
| **Progressão** (base_level, base_exp, job_level, job_exp) | ✅ | — |
| **Pool atual** (hp, sp) | ✅ | — |
| **Pool máximo** (max_hp, max_sp) | ✅ (cache) | Recalcula em level up / equip change |
| **Pontos não-gastos** (status_point, skill_point) | ✅ | — |
| **Class id** | ✅ | — |
| **Moeda** (zeny) | ✅ | — |
| **Derivados** (ATK, MATK, DEF, MDEF, hit, flee, crit, aspd) | ❌ NUNCA | ✅ Toda hora via `StatusCalc` |
| **Regen rates** (hp_regen, sp_regen) | ❌ | ✅ |
| **Buffs/debuffs ativos** | ❌ | ✅ (efêmero) |
**Por que NUNCA persistir derivados:** mudou um equip → derivado obsoleto. Aplicou buff → obsoleto. Cresceu de level → obsoleto. Salvar deriva inválida toda hora — bug fest silenciosa. Padrão rathena: `status_calc_pc_` em `.bases/rathena/rathena-master/src/map/status.cpp:4996` recalcula a `struct status_data` inteira a cada evento relevante.
### 1.3 Classes (jobs) são data-driven — arquivo YAML, não tabela DB
Padrão rathena: `db/re/job_stats.yml` carregado no boot. Hot-reloadable.
- ✅ Versionado no git (mudança de balanceamento = commit auditável)
- ✅ Sem SELECT repetido em todo spawn
- ✅ Game designer edita arquivo, não roda SQL
**No Zeus:** `Server/ZeusCharServer/data/jobs.yml` (carregado no boot do WorldServer e do CharServer). `characters.class_id` referencia lógica, não FK física.
### 1.4 Autoridade
| Componente | Quem decide | Quem persiste |
|---|---|---|
| Criar char (stats iniciais, class=Novato) | CharServer (valida) | CharServer → MySQL |
| Stat allocation (gastar `status_point` em STR/etc.) | CharServer **ou** WorldServer | CharServer → MySQL (via writeback) |
| Subir de level / ganhar EXP | WorldServer (kill mob, quest) | WorldServer → writeback → CharServer → MySQL |
| Mudar classe (Novato → Espadachim) | WorldServer (cumpre quest) | WorldServer → writeback → CharServer → MySQL |
| Recalcular ATK/MATK/DEF/etc. | WorldServer (memória, toda hora) | NÃO persiste |
| Casting de skill | WorldServer | NÃO persiste (efêmero) |
| Resultado de skill (HP perdido, item dropado) | WorldServer | WorldServer → writeback |
**Regra:** o estado "definitivo" do char vive no MySQL do CharServer. O WorldServer só **simula** com cópia em memória + writeback periódico (60s) e em eventos críticos (level up, item raro, logout).
---
## 2. Stats primários
### 2.1 Os 6 stats clássicos
| Stat | Nome PT-BR | Influencia diretamente |
|---|---|---|
| **STR** | Força | ATK (físico melee), peso carregável |
| **AGI** | Agilidade | ASPD (velocidade de ataque), Flee (esquiva) |
| **VIT** | Vitalidade | MaxHP, DEF (defesa física), resist status |
| **INT** | Inteligência | MaxSP, MATK (mágico), MDEF |
| **DEX** | Destreza | Hit (precisão), ATK (à distância — arco/arma de DEX), cast time |
| **LUK** | Sorte | CRIT (crítico), Perfect Dodge, drop rate raros |
Persistidos como `SMALLINT UNSIGNED` (0-65535), default 1, cap configurável (rathena default 99 pré-renewal, 130 renewal — Zeus pode escolher).
### 2.2 Stats primários adicionais (opcional, futuro)
Rathena Renewal adiciona 6 "trait stats" para classes 4ª: POW, STA, WIS, SPL, CON, CRT. **Não escopo da Fase 1** — adicionar quando classes 4ª entrarem (Fase de gameplay avançado).
### 2.3 Cost de alocar +1 stat
Padrão rathena: `cost = (stat_atual - 1) / 10 + 2`.
```
str=1 → custo de +1 stat = (0/10)+2 = 2 pontos
str=10 → custo = (9/10)+2 = 2
str=11 → custo = (10/10)+2 = 3
str=20 → custo = (19/10)+2 = 3
str=21 → custo = 4
...
str=99 → custo = (98/10)+2 = 11
```
Validação **SEMPRE server-side**:
```ts
const cost = Math.floor((current - 1) / 10) + 2;
if (char.status_point < cost) reject('NotEnoughPoints');
UPDATE characters SET str = str+1, status_point = status_point - cost;
```
---
## 3. Stats derivados (calculados em runtime no WorldServer)
Lista mínima a implementar. Fórmulas espelham rathena (`status_calc_misc`, `status_calc_pc_sub`, `status_base_atk` em `src/map/status.cpp`).
### 3.1 ATK físico (base, sem equip)
Renewal (Zeus alvo):
```
base_atk = floor(str + (str/10)^2 + dex/5 + luk/3 + base_level/4)
```
Para armas de DEX (arco, instrumento, chicote, armas de fogo): troca STR ↔ DEX no cálculo principal.
Total = `base_atk + weapon_atk + refine_bonus + cards_atk + status_atk_buffs`.
Ref: `.bases/rathena/rathena-master/src/map/status.cpp:2424` (`status_base_atk`).
### 3.2 MATK (mágico)
```
matk_min = floor(int + int/7^2 + dex/5 + luk/3 + base_level/4)
matk_max = floor(int + int/5^2 + dex/5 + luk/3 + base_level/4)
```
### 3.3 DEF (defesa física)
```
def2 = floor(vit + (vit/2)^2/30)
def_total = def2 + def_from_equip + def_from_refine
```
Renewal usa "soft DEF + hard DEF" — definir convenção no roadmap.
### 3.4 MDEF (defesa mágica)
```
mdef2 = floor(int + vit/5 + dex/5)
mdef_total = mdef2 + mdef_from_equip
```
### 3.5 HIT (precisão)
```
hit = base_level + dex + luk/3 + skill_bonus + equip_bonus
```
### 3.6 FLEE (esquiva)
```
flee = base_level + agi + luk/5 + skill_bonus + equip_bonus
```
### 3.7 CRIT (crítico)
```
crit = 1 + luk/3 + skill_bonus + equip_bonus
```
(Em rathena multiplica por 10 internamente para resolução; converte na display.)
### 3.8 ASPD (velocidade de ataque)
```
aspd = base_aspd_da_arma - (agi + dex/4) * fator_de_classe
```
`base_aspd_da_arma` vem do `jobs.yml` (cada job tem array `BaseASPD` por tipo de arma).
### 3.9 MaxHP / MaxSP — três camadas (cuidado para não persistir bônus)
Esta é a parte mais delicada. **Há três camadas** de MaxHP/MaxSP, cada uma com regra diferente:
| Camada | Onde vive | Quando recalcula | Persiste no DB? |
|---|---|---|---|
| **1. Base** | `characters.max_hp` / `max_sp` (cache) | Level up, job change, stat allocation (VIT/INT permanentes) | ✅ Sim |
| **2. Bônus de equipamento** (anel VIT+5, armor MaxHP+200) | WorldServer em memória | Equip / desequip de item | ❌ Nunca — recalcula do `character_inventory` no spawn |
| **3. Bônus de buff** (skill "Endure" +100 MaxHP por 60s) | WorldServer em memória (`status_change`) | Buff start / expire | ❌ Nunca — efêmero, expira sozinho |
**MaxHP efetivo (mostrado pro player) = camada 1 + camada 2 + camada 3.** Sempre calculado em runtime, nunca persistido.
**Fórmula da camada 1 (base):**
```
max_hp_base = floor(jobs.yml[class].HpFactor * base_level + jobs.yml[class].HpIncrease) * (1 + vit_base/100)
max_sp_base = floor(jobs.yml[class].SpFactor * base_level + jobs.yml[class].SpIncrease) * (1 + int_base/100)
```
Note: usa `vit_base` (`characters.vit`), **não** `vit_efetivo` (com bônus de equip). Bônus de equip não recalcula o cache base — entra como camada 2 separada.
**Triggers que escrevem `characters.max_hp` (camada 1):**
- ✅ Level up (`base_level += 1`)
- ✅ Job change (`HpFactor` novo)
- ✅ Stat allocation permanente (`vit_base += 1`)
**Triggers que NÃO escrevem `characters.max_hp` (só atualizam camadas 2/3 em memória):**
- ❌ Equip de item com VIT+5 ou MaxHP+200
- ❌ Desequip de item
- ❌ Buff aplicado (skill, food, gravação)
- ❌ Buff expirou
**Por que essa separação importa (cenário de bug se misturar):**
Suponha que você persistisse `max_hp` total (base + equip + buff):
1. Player equipa anel VIT+5 → bônus +50 MaxHP → `max_hp=1500` salvo no DB (era 1450 base).
2. WorldServer crasha.
3. Spawn: lê `max_hp=1500` do DB.
4. Recarrega equip do inventory mas o WorldServer "esqueceu" qual era o bônus do anel — só vê 1500.
5. Player desequipa o anel → bug: continua com 1500.
Pior com buff: writeback periódico salva `max_hp` durante buff ativo → buff expira mas DB tem o valor inflado → spawn dá HP zombie.
**Padrão rathena** ([src/map/status.cpp:status_calc_pc_](.bases/rathena/rathena-master/src/map/status.cpp)): salva só camada 1. Camadas 2 e 3 são sempre recalculadas do equip + buffs ativos no spawn. Buffs não persistem (morrem no logout).
**Estrutura em memória no WorldServer (template):**
```cpp
struct CharRuntimeStatus {
// --- Camada 1 (do DB; persistido) ---
uint32 max_hp_base;
uint32 max_sp_base;
uint16 str_base, agi_base, vit_base, int_base, dex_base, luk_base;
// --- Camada 2 (do inventory; não persiste) ---
int32 equip_max_hp_bonus, equip_max_sp_bonus;
int16 equip_str_bonus, equip_agi_bonus, /*...*/ equip_luk_bonus;
// --- Camada 3 (de status_change ativos; não persiste) ---
int32 buff_max_hp_bonus, buff_max_sp_bonus;
int16 buff_str_bonus, /*...*/;
// --- Computados on-demand (camada 1 + 2 + 3) ---
uint32 EffectiveMaxHp() const { return max_hp_base + equip_max_hp_bonus + buff_max_hp_bonus; }
uint16 EffectiveVit() const { return vit_base + equip_vit_bonus + buff_vit_bonus; }
// ... análogo pros outros stats
};
```
Cliente sempre vê o efetivo (`S_CHAR_HP_UPDATE { effective_hp, effective_max_hp }`). Cliente nunca vê o base nem os bônus separados — só a soma final.
---
## 4. Schema `jobs.yml`
Arquivo: `Server/ZeusCharServer/data/jobs.yml`.
```yaml
version: 1
jobs:
- id: 0
name: Novice # nome técnico (inglês, estável)
display_name_ptbr: Novato
parent_job: null
max_base_level: 99
max_job_level: 10
hp_factor: 35 # rathena Novice: ~35
hp_increase: 0
sp_factor: 10
sp_increase: 0
max_weight: 20000
base_aspd:
bare: 2000
dagger: 1900
sword: 2000
starting_hp: 40
starting_sp: 11
starting_status_points: 0
starting_skill_points: 0
allowed_weapons: [bare, dagger]
skills:
- id: NV_BASIC
max_level: 10
- id: NV_FIRST_AID
max_level: 1
- id: 1
name: Swordman
display_name_ptbr: Espadachim
parent_job: Novice # precisa ter sido Novice antes
requirements:
base_level: 1 # 1 (na verdade qualquer; Ragnarok exige job_level 10 Novice — definir)
job_level: 10 # do Novice
quest: "swordman_test" # quest id no questdb
max_base_level: 99
max_job_level: 50
hp_factor: 70
hp_increase: 200
sp_factor: 20
sp_increase: 200
bonus_stats: # auto-stats ao subir job level
- job_level: 2
str: 1
- job_level: 6
vit: 1
# ...
allowed_weapons: [bare, dagger, sword, two_hand_sword, axe]
skills:
- id: SM_BASH
max_level: 10
# ...
# Mago, Arqueiro, Mercador, Ladrão, Acólito...
```
**Notas:**
- IDs estáveis (não mude) — `characters.class_id` referencia direto.
- Mudar campo `hp_factor` ou `bonus_stats` rebalanceia o jogo retroativamente. **OK** — RuneScape/PoE/MMOs fazem isso o tempo todo.
- Hot-reload: endpoint admin `POST /admin/jobs/reload` recarrega `jobs.yml` em runtime sem restart (Fase 6+; Fase 1 só load no boot).
---
## 5. Fluxos
### 5.1 Criação de personagem (Novato)
```
Cliente → C_CHAR_CREATE { name, world_id, class_id=NOVICE, appearance }
CharServer:
1. Validar (name único globalmente, world_id existe, slot disponível, class_id=NOVICE — não pode criar direto em outra classe)
2. Lookup jobs.yml[NOVICE] → { starting_hp, starting_sp, hp_factor, sp_factor, ... }
3. INSERT characters (
account_id, world_id, slot, name, class_id=NOVICE,
base_level=1, base_exp=0, job_level=1, job_exp=0,
str=1, agi=1, vit=1, int=1, dex=1, luk=1,
hp=starting_hp, max_hp=starting_hp, sp=starting_sp, max_sp=starting_sp,
status_point=0, skill_point=0,
zeny=0, appearance,
map_name="zmmo_starting_village", pos_x=..., pos_y=..., pos_z=..., yaw_deg=0
)
4. → S_CHAR_CREATE_OK
```
**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)
```
Cliente → C_CHAR_STAT_ALLOC { stat: "str", amount: 1 }
CharServer (ou WorldServer; ver §6 — autoridade):
1. Lookup char no cache/DB
2. cost = floor((char.str - 1) / 10) + 2
3. amount * cost ≤ char.status_point? else REJECT('NotEnoughPoints')
4. char.str + amount > MAX_STAT(jobs.yml[class].max_stat)? else REJECT('StatCapped')
5. UPDATE characters SET str = str + 1, status_point = status_point - cost, version = version + 1
6. WorldServer: status_calc_pc(char) → recalcula derivados → manda S_CHAR_STAT_UPDATE pro cliente
```
### 5.3 Level up
```
WorldServer (em kill mob / complete quest):
1. char.base_exp += mob.exp_reward
2. enquanto char.base_exp >= exp_table[char.base_level + 1]:
char.base_exp -= exp_table[char.base_level + 1]
char.base_level += 1
char.status_point += status_per_level(char.base_level) // ex.: 3 base + level/4
max_hp = recalc(char, jobs.yml)
max_sp = recalc(char, jobs.yml)
char.hp = max_hp // heal full no level up (padrão Ragnarok)
char.sp = max_sp
3. POST /interserver/characters/{id}/checkpoint (writeback imediato — level up é crítico)
4. Enviar S_CHAR_LEVEL_UP pro cliente
```
Mesmo padrão pra job_exp/job_level (com skill_point em vez de status_point).
### 5.4 Job change (Novato → Espadachim, etc.)
```
WorldServer (player cumpriu quest):
1. Validar requirements: jobs.yml[Swordman].requirements.{base_level, job_level, quest}
2. char.class_id = SWORDMAN
3. char.job_level = 1
4. char.job_exp = 0
5. (Opcional Ragnarok-style: status_point NÃO zera; player mantém stats alocados)
6. recalc max_hp/max_sp (novo HpFactor/SpFactor)
7. POST /interserver/characters/{id}/checkpoint
8. S_CHAR_JOB_CHANGE
```
### 5.5 Logout — writeback final
```
WorldServer:
POST /interserver/characters/{id}/checkpoint {
str, agi, vit, int, dex, luk,
base_level, base_exp, job_level, job_exp,
hp, max_hp, sp, max_sp,
status_point, skill_point, zeny,
map_name, pos_x, pos_y, pos_z, yaw_deg,
appearance,
version: current_version
}
```
Conflict (409) → recarrega + merge + retry.
---
## 5.6 Loadouts (build switching) — Modelo A: equip + skills, stats permanentes
### Premissa
**Stats primários (str/agi/vit/int/dex/luk) são permanentes** uma vez alocados — padrão Ragnarok clássico. Permitir trocar stats via loadout livre destruiria o significado de cada ponto investido.
Loadouts no ZMMO guardam **apenas equip e skills** — exatamente o padrão FFXIV Gearset / WoW Equipment Manager. Cobre o caso de uso real (alternar entre setup PvE/PvP/farm sem reequipar manualmente todos os slots), sem virar free respec.
### Stat reset (separado de loadouts)
Reset de stats é uma operação **rara e cara**, fora do sistema de loadouts. Caminhos previstos (a decidir no game design):
- Item raro "Reset Stone" (drop de boss / cash shop).
- NPC específico que cobra moeda (zeny alto ou moeda especial).
- Privilégio VIP (limite mensal).
Mecânica: zera `str/agi/vit/int/dex/luk` aos valores base do job, e converte tudo investido em `status_point` (player realoca do zero). WorldServer endpoint dedicado, audit log obrigatório.
### Schema `character_loadouts`
```sql
CREATE TABLE character_loadouts (
id BIGINT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
character_id BIGINT UNSIGNED NOT NULL,
slot_index TINYINT UNSIGNED NOT NULL, -- 0..N-1 (N = MAX_LOADOUTS, ex.: 10)
name VARCHAR(32) NOT NULL, -- "Tank", "DPS PvE", "PvP Build"
-- Snapshot de equip (referência a instâncias específicas no inventory,
-- FK pra character_inventory.id que entra em frente futura).
equip_head_id BIGINT UNSIGNED NULL,
equip_body_id BIGINT UNSIGNED NULL,
equip_weapon_id BIGINT UNSIGNED NULL,
equip_shield_id BIGINT UNSIGNED NULL,
equip_garment_id BIGINT UNSIGNED NULL,
equip_footgear_id BIGINT UNSIGNED NULL,
equip_acc_left_id BIGINT UNSIGNED NULL,
equip_acc_right_id BIGINT UNSIGNED NULL,
equip_head_top_id BIGINT UNSIGNED NULL,
equip_head_mid_id BIGINT UNSIGNED NULL,
equip_head_bottom_id BIGINT UNSIGNED NULL,
-- (Slots espelham o padrão rathena: weapon, shield, garment, footgear,
-- acc esquerda, acc direita, head top/mid/bottom)
-- Snapshot de hotbar de skills (JSON: { slot_index: skill_id })
skill_hotbar JSON NULL,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
FOREIGN KEY (character_id) REFERENCES characters(id) ON DELETE CASCADE,
UNIQUE KEY uniq_char_slot (character_id, slot_index),
INDEX idx_loadouts_character (character_id)
);
```
**Notas:**
- **`characters` continua flat e inalterado** — não há `active_loadout_id`. Stats + equip atuais vivem em `characters`. Loadouts são *snapshots salvos*, não "estado ativo".
- Não inclui stats — modelo A. Loadout não é respec.
- Não inclui skills aprendidas (essas ficam em `character_skills` quando a feature entrar) — só a `skill_hotbar` (quais skills estão nas teclas de atalho).
- Referências de equip apontam pra **instâncias específicas** do item (`character_inventory.id`), não pra item template id genérico. Se o player vendeu o item depois de salvar a build, o slot vira NULL na hora de aplicar + warning UI.
### Fluxos de loadout
**Salvar build atual como preset:**
```
Cliente → C_LOADOUT_SAVE { slot_index, name }
WorldServer:
Pega equip atual + skill_hotbar do char em memória
INSERT/UPDATE character_loadouts SET ...
POST /interserver/characters/{id}/loadouts/save (writeback imediato)
S_LOADOUT_SAVED { slot_index }
```
**Aplicar loadout (trocar equip + hotbar):**
```
Cliente → C_LOADOUT_APPLY { slot_index }
WorldServer:
SELECT * FROM character_loadouts WHERE character_id=$id AND slot_index=$slot
Para cada equip_*_id no loadout:
Verifica se item ainda existe em character_inventory + pertence ao char
Se sim: equipa
Se não: slot vira NULL + retorna warning lista de slots "perdidos"
Aplica skill_hotbar
Recalcula stats derivados (StatusCalc)
S_LOADOUT_APPLIED { applied_slot, missing_slots? }
```
**Listar loadouts (CharSelect ou in-world):**
```
Cliente → C_LOADOUT_LIST
WorldServer:
SELECT slot_index, name FROM character_loadouts WHERE character_id=$id ORDER BY slot_index
S_LOADOUT_LIST [{ slot_index, name, summary }]
```
**Deletar loadout:**
```
Cliente → C_LOADOUT_DELETE { slot_index }
DELETE FROM character_loadouts WHERE character_id=$id AND slot_index=$slot
```
### Comportamento na seleção de char (CharSelect)
Aplicar loadout só funciona **in-world**, não na CharSelect — porque depende do inventory carregado, contexto de combate, etc. CharSelect só lista qual foi a última usada (estado UX no cliente). WorldServer aplica o loadout ativo automaticamente no spawn se cliente indicou via parâmetro do C_CHAR_SELECT (opcional — pode também simplesmente carregar o último equip salvo em `characters` direto).
### Autoridade
Mesmo princípio dos stats: **WorldServer é dono**. Cliente envia comando, WorldServer valida + aplica + persiste via canal C.
### Eager loading no spawn (resposta à sua dúvida específica)
**Pergunta original:** "ao pegar Character retorna também Stats — fica mais fácil".
Como decidimos modelo A (stats permanentes em `characters`, sem entidade separada), **não há eager loading de stats** — já vêm na mesma row.
Para loadouts:
- **CharServer (TypeORM):** anota relacionamento com `{ eager: true }` ou faz `.relations(['loadouts'])` em queries específicas. Suporte nativo.
- **WorldServer (`ZeusPersistence` C++):** hoje o `Repository<T>` é single-table. **Recomendação imediata:** 2 queries separadas no service de spawn (`Repo<Character>.Find(id)` + `Repo<CharacterLoadout>.FindWhere(character_id=id)`). Performance idêntica a JOIN para single char. Simples.
- **Roadmap (`ZeusPersistence` v2):** estender com `Repository<T>::Include<R>()` (estilo EF Core). Vale o trabalho quando aparecer a 3ª relação eager (loadouts + inventory + skills + buffs). Adiar até lá.
### Roadmap
Loadouts entram em **frente própria de gameplay**, depois das Fases A-C do Character Model e da feature de inventory. Não bloqueia Fase 1 do ServerSelect — schema é puro additive (não muda `characters`).
---
## 6. Autoridade — onde stat allocation acontece?
Duas escolhas defensáveis:
| Onde | Vantagens | Desvantagens |
|---|---|---|
| **CharServer** (recebe `C_CHAR_STAT_ALLOC` direto do cliente) | Simples; muda DB direto sem RPC | Cliente in-world tem que avisar WorldServer "stats mudaram" → recalcular |
| **WorldServer** (cliente sempre fala com mundo) | WorldServer já tem char na memória, recalcula imediato | Stat allocation só funciona logged in (sem alocação fora do mundo) |
**Recomendação:** **WorldServer.** Razões:
1. Stat allocation só faz sentido in-world (precisa de UI de char, contexto de combate).
2. WorldServer tem stats derivados em memória — UPDATE de STR força recálculo imediato.
3. CharServer não precisa entender game logic de cost (`(s-1)/10+2`) — fica isolado.
4. Padrão rathena: map-server (= WorldServer) faz `pc_statusup` ([rathena/src/map/pc.cpp](.bases/rathena/rathena-master/src/map/pc.cpp)).
Fluxo: cliente envia opcode UDP pro WorldServer → valida + UPDATE em memória + writeback diferido (não imediato — junto com o próximo checkpoint periódico de 60s). Level up é exceção (writeback imediato).
---
## 7. Anti-cheat e validação server-side
**Toda** mudança de stat passa por validação no servidor:
- Cliente envia `C_CHAR_STAT_ALLOC { stat, amount }` → servidor valida + faz, nunca cliente diz "novo valor".
- HP/SP nunca vêm do cliente — só do WorldServer (autoridade).
- EXP é WorldServer who-says.
- Zeny: transações sempre server-side (trade, NPC shop, drop).
Cliente apenas **renderiza** o estado que o servidor empurra (`S_CHAR_STAT_UPDATE`, `S_CHAR_HP_UPDATE`, etc.).
Audit log entries (Fase 3 do ServerSelect doc): toda mudança de zeny>1000, level up, mudança de classe, item raro recebido → `POST /interserver/audit`.
---
## 8. Roadmap de implementação
### Fase A — Schema + criação de char Novato (parte da Fase 1 do ServerSelect)
- Schema `characters` com todos os campos (já especificado no `ARQUITETURA_SERVER_SELECT.md`).
- `jobs.yml` apenas com `Novice` (Novato) — outras classes ficam pra B.
- CharServer carrega `jobs.yml` no boot.
- `C_CHAR_CREATE` valida `class_id == NOVICE` e usa defaults do yml.
- Cliente UI: tela de criação com nome + appearance (sem stat allocation aqui — Novato nasce com todos os stats=1).
### Fase B — Classes adicionais + job change
- `jobs.yml` expande para Espadachim, Mago, Arqueiro, Mercador, Ladrão, Acólito (6 first jobs clássicos do Ragnarok).
- Quest de mudança de classe (mínimo: NPC simples; quest system real fica pra C).
- WorldServer endpoint pra mudar class.
### Fase C — Stat allocation + status_point/skill_point granting
- WorldServer recebe `C_CHAR_STAT_ALLOC` UDP opcode.
- Validação cost = `(s-1)/10+2`.
- `S_CHAR_STAT_UPDATE` push pro cliente.
- Skill point granting tied to job_level up.
### Fase D — Stats derivados completos + StatusCalc framework
- Implementar `StatusCalc::ComputeAll(char, equip, buffs)` no WorldServer C++.
- ATK/MATK/DEF/MDEF/hit/flee/crit/aspd.
- Push de update no equip change, buff aplicado, level up.
### Fase E — Classes 2ª (advanced) e além
- Especialização (Knight, Wizard, Hunter, etc. — segunda promoção do Ragnarok).
- Eventualmente classes 3ª (Renewal) e trait stats (POW/STA/WIS/SPL/CON/CRT).
### Fase F — Loadouts (build switching, Modelo A)
**Pré-requisito:** inventory implementado (FK de `character_loadouts.equip_*_id` aponta pra `character_inventory.id`).
- Schema `character_loadouts` (já especificado em §5.6).
- WorldServer endpoints: `C_LOADOUT_SAVE/APPLY/LIST/DELETE`.
- Cliente UI: tela "Builds" com até N slots, botões salvar/aplicar/renomear/deletar, preview de equip.
- Audit log em apply (Fase 3 do ServerSelect doc).
### Fase G — Stat reset (item / NPC / VIP)
- Endpoint `C_CHAR_STAT_RESET` no WorldServer.
- Validação: tem o item de reset? Tem o zeny do NPC? Tem privilégio VIP + cooldown?
- Lógica: zera str/agi/vit/int/dex/luk pros base do job; converte total investido em `status_point`.
- Audit log obrigatório (write-heavy, sensível a abuso).
**A se conecta com a Fase 1 do ServerSelect.** B-G são frentes próprias de gameplay, fora do escopo de network/handoff.
---
## 9. Decisões consolidadas
1. **Stats primários flat** na tabela `characters` (str/agi/vit/int/dex/luk + level/exp + hp/sp/max_hp/max_sp + status_point/skill_point + zeny + class_id). **Sem entidade separada.**
2. **Stats derivados NUNCA persistidos** — sempre recalculados em runtime no WorldServer (`StatusCalc::ComputeAll`).
3. **Jobs data-driven** via `Server/ZeusCharServer/data/jobs.yml` (versionado no git, hot-reloadable). `characters.class_id` é lookup lógico.
4. **Player nasce Novato** (`class_id=NOVICE`); especialização via quest in-game (Fase B).
5. **Stat allocation acontece no WorldServer** (não CharServer). Validação `cost = (s-1)/10 + 2` server-side.
6. **Anti-cheat:** toda mudança de estado é server-authoritative. Cliente apenas renderiza.
7. **Fórmulas espelham rathena** (`status_calc_pc_` em `src/map/status.cpp:4996`) — adapta valores pra balanceamento Zeus depois.
8. **MaxHP/MaxSP são persistidos como cache** (recalcula em level up / equip change, salva pra evitar recalcular no spawn).
9. **Writeback granular:** stat allocation = writeback diferido (junto do checkpoint 60s); level up + job change = writeback imediato.
10. **Loadouts/build switching (Modelo A — Ragnarok clássico):** tabela `character_loadouts` separada (1:N com `characters`). Guarda **apenas equip + skill_hotbar**, NÃO stats. Stats permanecem permanentes; reset só via item raro / NPC / VIP (operação dedicada, fora do sistema de loadouts). Aplicar loadout = trocar equip e hotbar, server-side, in-world only.
11. **Sem `active_loadout_id` em `characters`:** estado vivo permanece em `characters` (flat). Loadouts são snapshots salvos pra reaplicar sob demanda.
12. **Eager loading:** TypeORM já suporta. No `ZeusPersistence` C++ usa 2 queries separadas por enquanto; `Include<R>()` fluent fica como roadmap quando houver 3+ relações eager.
---
## 10. Lições do rathena — o que adotar e o que evitar
Análise direta do código do rathena (versão master atual em `.bases/rathena/rathena-master/`). Lista o que vale levar pro modelo moderno do Zeus e o que é legado a evitar.
### 10.1 Três estruturas de status, não duas
Rathena tem **3 structs separadas** convivendo em `map_session_data` ([src/map/pc.hpp:382-388](.bases/rathena/rathena-master/src/map/pc.hpp)):
```cpp
class map_session_data : public block_list {
...
struct mmo_charstatus status; // (1) PERSISTED — SQL <-> RAM
struct status_data base_status; // (2) BASE — RAM only
struct status_data battle_status; // (3) BATTLE — RAM only
status_change sc; // (Buffs/debuffs ativos)
...
};
```
| Camada | Conteúdo | Persiste no DB? | Recalcula quando |
|---|---|---|---|
| **(1) `mmo_charstatus status`** (PERSISTED) | Stats primários permanentes, level/exp, HP/SP atuais, equip slots, skill levels aprendidos, zeny, pos | ✅ Sim — espelha SQL `char` table | Em writeback periódico (a partir do battle_status) |
| **(2) `status_data base_status`** (BASE) | Stats primários + bônus de equip + bônus de skills passivas. ATK/MATK/DEF/etc. SEM buffs ativos. | ❌ Não | Equip change, level up, mudança de skill, mudança de job, stat allocation. Via `status_calc_pc_` ([src/map/status.cpp:4996](.bases/rathena/rathena-master/src/map/status.cpp)) |
| **(3) `status_data battle_status`** (BATTLE) | base_status **+** todos os buffs/debuffs ativos (`status_change`). É o valor "final" usado em combat e mostrado pro cliente. | ❌ Não | Cada vez que um buff aplica/expira, cada hit (debuffs proc). Via `status_calc_bl_main` ([src/map/status.cpp:5836](.bases/rathena/rathena-master/src/map/status.cpp)) |
**Por que três e não duas:** separar equip+passivas (camada 2, muda raro) de buffs (camada 3, muda toda hora) permite recalcular APENAS camada 3 quando um buff entra/sai. Sem isso, todo buff força recálculo completo de equipamento — caro em PvP/raids com muitos buffs simultâneos.
**Adaptação Zeus C++ (WorldServer):**
```cpp
struct CharRuntimeState {
PersistedStatus persisted; // copia do MySQL via ZeusPersistence (camada 1)
BaseStatusData base_status; // calculado de persisted + equip + passivas (camada 2)
BaseStatusData battle_status; // base_status + status_change (camada 3)
StatusChangeContainer sc; // lista de buffs ativos com expire_tick
};
```
`battle_status` é o que vai pro `S_CHAR_STAT_UPDATE` pro cliente. Cliente nunca vê `base_status` nem `persisted` direto.
### 10.2 Recompute total > delta incremental
Rathena **zera `base_status` inteiro** (`memset` em status.cpp:3816) e recompila tudo do zero a cada `status_calc_pc_`. Não tenta "subtrair o equip antigo, somar o novo" — pull-not-push.
```cpp
// rathena status.cpp:3816 (simplificado)
memset(&base_status->max_hp, 0, sizeof(struct status_data) - sizeof(hp/sp/ap));
// reaplica defaults do job
// reaplica equipped items
// reaplica skills passivas
// recalcula derivados
```
**Por que:** delta incremental cria drift sutil. Um bug de "esqueci subtrair na hora de desequipar" e o stat fica inflado pra sempre. Recompute total é O(N) por evento mas N é pequeno (~10 slots de equip + ~30 skills passivas) e elimina bug class inteira.
**Adotar:** mesma estratégia no `WorldServer::StatusCalc::RecomputeBase(char)`. Em C++ moderno usar `base_status = BaseStatusData{};` (move-assign do default) em vez de `memset` (porque structs com membros não-trivially-copyable quebram).
### 10.3 HP/SP atual nunca é zerado no recálculo
Truque clássico do rathena (status.cpp:3816): o `memset` começa em `&base_status->max_hp`, **pulando** os primeiros membros (`hp`, `sp`, `ap`). HP atual sobrevive ao recálculo de base_status.
**Razão:** equipar um anel não pode resetar o HP do char pra zero. Mas pode mudar `max_hp` — se `max_hp` aumentou, HP continua o atual; se diminuiu, capa em `min(hp, max_hp)`.
**Adotar:** ordenar membros do struct em C++ pra ter HP/SP/AP no topo, e adotar regra `RecomputeBase()` sem tocar nesses 3. Ou explicitamente preservar:
```cpp
const auto saved_hp = base_status.hp, saved_sp = base_status.sp;
base_status = BaseStatusData{};
base_status.hp = saved_hp; base_status.sp = saved_sp;
ApplyJobDefaults(); ApplyEquip(); ApplyPassives(); ComputeDerived();
base_status.hp = std::min(base_status.hp, base_status.max_hp); // cap
```
### 10.4 Recursion guard obrigatório no recálculo
Rathena ([status.cpp:3756-3764](.bases/rathena/rathena-master/src/map/status.cpp)):
```cpp
static int32 calculating = 0;
if (++calculating > 10) return -1;
// ... recálculo
--calculating;
```
**Razão:** recálculo de stat pode trigger skill que aplica buff que dispara recálculo. Sem guard = stack overflow em corner cases (combos de auto-cast, item triggers cascading).
**Adotar:** flag thread-local `bool recalculating = false` ou contador. Se já está em recálculo, retornar erro/skip em vez de reentrar. Importante mesmo em arquiteturas modernas — buffs reativos a hits criam loops circulares fácil.
### 10.5 Recalc parcial via bitflags (otimização avançada)
Rathena tem ([status.hpp](.bases/rathena/rathena-master/src/map/status.hpp)):
```cpp
enum scb_flag {
SCB_NONE = 0, SCB_BASE, SCB_STR, SCB_AGI, /*...*/, SCB_WATK, SCB_BATK,
SCB_MAX
};
status_calc_bl_main(bl, std::bitset<SCB_MAX>{ SCB_STR | SCB_WATK });
```
Quando só STR muda (e nada mais), recalcula apenas WATK derivado (que depende de STR). Performance no PvP de alto nível.
**Adotar:** **NÃO na Fase 1**. Implementar como otimização depois quando profile mostrar `RecomputeBase` aparecendo no top do flame chart. Pra MMOs com <1000 players concurrent, recompute total é OK.
### 10.6 Buffs em estrutura separada com expiração por timer
Rathena `status_change` ([src/map/status.hpp](.bases/rathena/rathena-master/src/map/status.hpp)):
```cpp
struct status_change_entry {
int32 timer; // handle do scheduler
int32 val1-4; // payload (varia por tipo)
t_tick tick; // duração total
};
struct status_change {
status_change_entry *data[SC_MAX]; // indexado por SCType
};
```
Cada SC (status change) tem timer próprio que dispara `status_change_end` quando expira. Engine de timer global gerencia. **Buffs NUNCA persistem em SQL** — morrem em logout (regra mais comum) ou DC (alguns servers preservam alguns SC em rejoin rápido).
**Adotar:**
```cpp
struct StatusChangeEntry {
StatusChangeType type;
int32 stack_value; // SC ID + payload
TickT expire_at; // absolute deadline
};
class StatusChangeContainer {
std::unordered_map<StatusChangeType, StatusChangeEntry> active;
void Tick(TickT now) { /* expira os terminados, dispara recalc se necessário */ }
};
```
Buffs morrem em logout — padrão da indústria. Exceções (food buffs, gravações permanentes, equip-while-stationary) tratadas via cases especiais no game design depois.
### 10.7 Item bonus data-driven, não hardcoded
Rathena tem `pc_bonus` ([src/map/pc.cpp](.bases/rathena/rathena-master/src/map/pc.cpp)) com switch case gigante:
```cpp
void pc_bonus(map_session_data *sd, int32 type, int32 val) {
switch (type) {
case SP_STR: sd->base_status.str += val; break;
case SP_AGI: sd->base_status.agi += val; break;
case SP_MAXHP: sd->base_status.max_hp += val; break;
// ... ~200 cases
}
}
```
E `item_db.yml` declara em YAML quais bonuses cada item dá:
```yaml
- Id: 1109
Name: Sword
Type: Weapon
Script: |
bonus bAtk, 25;
bonus bStr, 1;
```
**O bom:** items são data-driven (designer edita YAML, não código).
**O ruim:** switch case com 200 cases é horror de manter. Bug em um case afeta um stat só, difícil de testar.
**Adotar (versão moderna):**
```cpp
using BonusHandler = void(*)(BaseStatusData&, int32 val);
inline const std::unordered_map<BonusType, BonusHandler> kBonusHandlers = {
{ BonusType::Str, [](auto& s, int32 v){ s.str += v; } },
{ BonusType::Agi, [](auto& s, int32 v){ s.agi += v; } },
{ BonusType::MaxHp, [](auto& s, int32 v){ s.max_hp += v; } },
// ...
};
void ApplyBonus(BaseStatusData& s, BonusType type, int32 val) {
if (auto it = kBonusHandlers.find(type); it != kBonusHandlers.end()) {
it->second(s, val);
}
}
```
Mesma capacidade data-driven, sem switch gigante. Testes unitários ficam triviais (testa cada handler isolado).
### 10.8 Save state separado do battle state
Rathena periodicamente faz `pc_makesavestatus(sd)` que **copia** `sd->battle_status.hp/sp/ap``sd->status.hp/sp/ap`. Só depois isso vai pro CharServer/SQL.
**Razão:** `battle_status` tem cap inflado por buff (max_hp +500 de buff). `status.hp` deve refletir o valor real que faz sentido persistir (HP atual, capa de `status.max_hp` que é o base puro sem buff).
**Adotar:** writeback NUNCA salva valores afetados por buff ativo. Save HP é o `min(battle.hp, base.max_hp)`. Save MaxHP é `base.max_hp`. Garante que log out + log in com buff expirado não dá HP zombie.
### 10.9 O que NÃO levar do rathena
Práticas que são legado e dão mais trabalho do que valem em projeto novo:
| Anti-pattern | Por que evitar | Alternativa moderna |
|---|---|---|
| **`battle_config` global com 500+ flags** | Estado mutável global, difícil de testar, conflito em multi-tenant | Struct `BalanceConfig` injetada por DI; YAML de balance versionado |
| **Arrays C fixos (`MAX_SKILL`, `MAX_INVENTORY`)** | Crash se exceder; memória desperdiçada se vazio | `std::vector` ou `std::array` com bounds-check em debug |
| **`map_session_data : block_list`** (herda) | Mistura "dados do char" com "ator com posição no mundo" | Separar `CharacterData` (POD) de `WorldActor` (componente de posição/movimento) |
| **Job class em bitfield** (`JOBL_BABY`, `MAPID_SUMMONER`) | Decisão de packet do RO original. Manutenção horrível | `enum class JobId` + `struct JobTraits { bool is_baby; bool can_summon; }` data-driven em `jobs.yml` |
| **Recursive call preemption por contador** | Workaround pra event model mal-formado | ECS / actor model não-reentrante; quando reentrância é necessária, fila de eventos diferida |
| **`pc_calc_skilltree` muta skill tree do char** | Mistura "skills aprendidas" (persistente) com "efeitos passivos ativos" (derivado) | Separar `LearnedSkills` (no DB) de `ActivePassives` (calculado em memória) |
| **`status_change->data[SC_MAX]` array linkedlist** | Lookup linear, memória pré-alocada | `std::unordered_map<SCType, SCEntry>` ou `boost::flat_map` para hot path |
| **Hot-reload via `@reloadbattleconf`** in-game commands | Não tem versionamento, vira PvP-zoeira "ajustei config no live" | Hot-reload via endpoint admin autenticado + audit log obrigatório |
| **SQL queries em string puro** (sem ORM) no map server | SQL injection era real; manutenção horrível | ZeusPersistence (já fizemos) ou TypeORM no CharServer |
| **Globals com função inicializadora (`do_init_status`)** | Side effects no static init | Singleton com lifetime explícito, `StatusEngine engine; engine.Init(config);` |
### 10.10 Resumo do que vai pro Zeus
**Adotar:**
1. **3 structs**: `PersistedStatus` (SQL), `BaseStatusData` (RAM, equip+passivas), `BattleStatusData` (RAM, +buffs).
2. **Recompute total** em camadas 2 e 3 — não delta incremental.
3. **HP/SP atual preservado** em recálculo de base.
4. **Recursion guard** no `RecomputeBase`/`RecomputeBattle`.
5. **`StatusChange` container** com expiração por timer; buffs nunca persistem.
6. **Bonus handler table** (não switch case).
7. **Save state separado** do battle state no writeback.
**Adiar (otimização):**
- Recalc parcial via bitflags (vale com >1000 concurrent).
**Evitar:**
- battle_config global; arrays C fixos; herança block_list; job em bits; SQL crú no map server; hot-reload sem audit.
---
## 11. Não cobertos aqui
Por design, estes temas têm docs próprios (futuros):
- **Inventory e items** (slots, refine, cards, sockets) — `ARQUITETURA_INVENTORY.md` (a criar).
- **Skill system** (skill tree, cooldowns, animations) — `ARQUITETURA_SKILLS.md` (a criar).
- **Quests** — `ARQUITETURA_QUESTS.md` (a criar).
- **Combat resolution** (damage formula, element table, race modifier) — `ARQUITETURA_COMBAT.md` (a criar).
- **Game balance** (números concretos: quanto STR dá quanto ATK em level N) — `DESIGN_BALANCE.md` (decisão de game design, não arquitetura).
- **Party/guild** — `ARQUITETURA_SOCIAL.md` (a criar).
Este documento só fixa **estrutura e autoridade** — o que vai onde, quem decide o quê.
---
## Referências
- rathena `db/re/job_stats.yml` — schema base de jobs ([.bases/rathena/rathena-master/db/re/job_stats.yml](Server/.bases/rathena/rathena-master/db/re/job_stats.yml))
- rathena `src/map/status.cpp:status_calc_pc_` — recálculo de status ([linha 4996](Server/.bases/rathena/rathena-master/src/map/status.cpp))
- rathena `src/map/status.cpp:status_base_atk` — fórmula de ATK base ([linha 2424](Server/.bases/rathena/rathena-master/src/map/status.cpp))
- rathena `sql-files/main.sql:209-296` — tabela `char` flat ([main.sql](Server/.bases/rathena/rathena-master/sql-files/main.sql))
- rathena `src/map/pc.cpp:pc_statusup` — server-side stat allocation
- [`ARQUITETURA_SERVER_SELECT.md`](ARQUITETURA_SERVER_SELECT.md) — schema de `characters` e fluxos de criação no contexto de mundos

View File

@@ -0,0 +1,593 @@
# Arquitetura: Server Select + Mundos + Handoff CharServer↔WorldServer
> **Status:** plano de implementação + extensão da arquitetura. Quando aprovado, este documento vira `Clients/ZMMO/Docs/ARQUITETURA_SERVER_SELECT.md` (ou path equivalente em `Server/`) — é referência permanente, não plano descartável.
## Context
Hoje a tela de Server Select carrega após o login mas é um mock visual sem dados reais:
- `worldHost`/`worldPort` no `S_CHAR_SELECT_OK` está hardcoded em `127.0.0.1:27777`.
- `handoffToken` é gerado aleatório no CharServer mas **nada valida no WorldServer**.
- Não existe tabela `worlds` no schema; não existe opcode pra listar mundos.
- `characters` não tem `world_id` — qualquer char joga em qualquer mundo.
- WorldServer não reporta população/status; CharServer não sabe quem está online onde.
Esta frente desenha a arquitetura "de verdade": múltiplos mundos cadastrados, criação de personagem amarrada ao mundo escolhido, handoff seguro com ticket validado, e uma base que escale pra multi-node + multi-region. Cobre desde o desenho conceitual até execução em fases pequenas e verificáveis.
---
## Princípios arquiteturais (consolidado, após autocrítica)
### 1. Separação de responsabilidades
| Responsabilidade | Quem é dono | Justificativa |
|---|---|---|
| **Identidade do jogador** (account, JWT, sessões) | CharServer + MySQL global | Auth precisa ser único cross-region. JWT habilita multi-node sem state replication. |
| **Persistência de personagens** (chars, inventário, level) | CharServer + MySQL global | Habilita transfer, cross-world social, char list multi-mundo. Padrão AccelByte/FFXIV. |
| **Estado vivo de mundos** (pop, queue, status) | WorldServer → Valkey (regional) | Source-of-truth do mundo é quem hospeda o mundo. CharServer só lê e propaga. |
| **Simulação autoritativa** (movimento, combate, IA) | WorldServer C++ Unreal | Latência baixa + tick fixo + autoridade contra cheat. |
| **Tickets de handoff** (single-use, TTL curto) | CharServer emite, Valkey armazena, WorldServer consome | Padrão GameLift: lobby decide quem entra. |
### 2. Quatro canais de comunicação (sem sobreposição)
CharServer já roda dois listeners simultâneos (`buildServer` em `core/server.ts`): **uWebSockets.js** na porta 7100 (cliente Unreal, path `/zeus`) e **Fastify HTTPS** na porta 7101 (control plane). É a base do padrão AccelByte: WSS pra push real-time pro cliente, HTTPS pra chamadas pontuais e tráfego servidor-a-servidor.
| Canal | Quem fala com quem | Protocolo | Onde está | Uso |
|---|---|---|---|---|
| **A** | Cliente ↔ CharServer | WebSocket (WSS) binário Zeus | `wss://...:7100/zeus` | Login, char list/create/select, lista de mundos, push de status, queue updates, chat (futuro) |
| **B** | Cliente ↔ WorldServer | UDP binário Zeus | `UZeusNetworkSubsystem``ZeusNet/UdpServer` | Handshake + gameplay autoritativo. Cliente apresenta ticket no `C_CONNECT_REQUEST` |
| **C** | CharServer ↔ WorldServer | HTTPS REST (Fastify) | `:7101/interserver/*` | Claim, character RPC, audit, kick (mTLS na Fase 6) |
| **D** | Admin/CLI ↔ CharServer | HTTPS REST + CLI direto MySQL | `:7101/admin/*` e CLI `npm run world` | CRUD de mundos, métricas, health |
**Regras invioláveis:**
- Cliente Unreal **NUNCA** fala HTTP com o CharServer. Tudo é WSS via opcodes Zeus.
- Cliente Unreal **NUNCA** consulta o WorldServer pra ver pop/status. Sempre via CharServer (canal A).
- WorldServer **NUNCA** escreve direto no MySQL — sempre via canal C (validação + auditoria).
### 3. Pop tracking: três triggers combinados (não escolher um — usar todos)
WorldServer é o único que sabe a verdade sobre pop/queue/state. Reporta via **Valkey** (Fase 2+); na Fase 1 reporta via HTTP POST como fallback temporário.
| Trigger | O que faz | Frequência |
|---|---|---|
| **Event-driven** (player entrou/saiu) | `HINCRBY world:{id}:status pop ±1` + `PUBLISH world:events` | Imediato |
| **State-change** (online↔maintenance↔offline) | `HSET world:{id}:status state=...` + `PUBLISH world:events` | Imediato |
| **Periodic heartbeat** | `HSET world:{id}:status ...` + `PEXPIRE 30000` (reset TTL) | A cada 5s |
**Por que os três:** events dão real-time; periodic reconcilia se um PUBLISH foi perdido; TTL detecta world morto automaticamente em ~30s sem código extra.
### 4. Topologia geo-distribuída
```
┌────────────────────────────────────────┐
│ MySQL Primary (writes globais) │
│ ex.: us-east-1 ou Hetzner DE │
└──────────┬─────────────────────────────┘
│ async replication (<100ms)
┌───────────────┼───────────────┐
▼ ▼ ▼
read-replica read-replica read-replica
(NA) (SA) (EU)
│ │ │
┌──────┴──────┐ ┌──────┴──────┐ ┌──────┴──────┐
│ CharServer │ │ CharServer │ │ CharServer │
│ node × N │ │ node × N │ │ node × N │
│ Valkey (NA) │ │ Valkey (SA) │ │ Valkey (EU) │
└──────┬──────┘ └──────┬──────┘ └──────┬──────┘
na.zeusmmo… sa.zeusmmo… eu.zeusmmo…
▲ ▲ ▲
WorldServers WorldServers WorldServers
na região NA na região SA na região EU
▲ ▲ ▲
Clientes NA Clientes SA Clientes EU
(GeoDNS/CDN) (GeoDNS/CDN) (GeoDNS/CDN)
```
**Regras:**
1. WorldServer fala **apenas** com CharServer da sua região (`CharServerEndpoint` no `server.json`).
2. Cliente conecta no DNS regional via GeoDNS. Não cruza região durante sessão.
3. MySQL writes globais (primary central); reads na replica regional.
4. Valkey é por-região (estado vivo é local — sem cross-region sync).
5. Lista de mundos é regional por default (padrão FFXIV — cliente vê só os mundos da `account.region`).
6. `ZEUS_REGION` env var em cada CharServer node; `region` no `worlds.region` deve bater com a região do CharServer que recebe o claim do WorldServer.
| Região | DNS público | MySQL replica | Valkey cluster |
|---|---|---|---|
| Brasil/SA | `sa.zeusmmo.com.br` | sa-east-1 | cluster SA |
| North America | `na.zeusmmo.com.br` | us-east-1 | cluster NA |
| Europe | `eu.zeusmmo.com.br` | eu-central-1 | cluster EU |
| Asia | `as.zeusmmo.com.br` | ap-southeast-1 | cluster AS |
---
## Componentes-chave
### Identidade do mundo: admin-create + WorldServer-claim
Híbrido (estilo AccelByte Armada). Três alternativas e por que escolhemos esta:
| Padrão | Veredito |
|---|---|
| Auto-register em runtime | ✗ Restart cria ID novo, órfana chars com FK |
| Pre-config puro (estilo WoW) | ✗ Sem prova de identidade — qualquer processo manda heartbeat falso |
| **Admin-create + claim com secret** | ✓ ID estável + prova de posse + auditoria |
**Fluxo:**
1. Admin via CLI: `npm run world -- create --name=Aurora --host=... --port=... --region=br --cap=500` → CharServer gera `world_id` (UUID) + `world_secret` (32 bytes base64url). Output mostra os dois **uma vez**. `secret_hash` (Argon2id) é persistido em `worlds.secret_hash`.
2. Admin copia `world_id` + `world_secret` pro `Config/server.json` do WorldServer.
3. WorldServer no boot: `POST /interserver/worlds/{id}/claim { secret }`. CharServer valida Argon2id contra `secret_hash` + region match. Sucesso → `state=online`. Falha → `403` + WorldServer aborta startup.
4. WorldServer começa heartbeat (Fase 1 HTTP; Fase 2+ Valkey direto).
5. WorldServer crashado → TTL Valkey expira em 30s → `state=offline` automático.
### Ticket de handoff: opaque token single-use
**Hoje:** `handoffToken` é random sem validação. Inseguro.
**Padrão escolhido:** opaque token 32 bytes (crypto.randomBytes), **TTL 10s** (não 30 — janela menor pra replay), armazenado em Valkey, consumido via `GETDEL` no WorldServer (single-use).
```
CharServer ao emitir:
payload = { account_id, char_id, world_id, expires_at }
SETEX ticket:{token} 10 <payload-json>
INCR world:{world_id}:reserved ← reserva slot (anti-overbooking)
responde S_CHAR_SELECT_OK(worldHost, worldPort, token)
Cliente:
conecta UDP no World, envia C_CONNECT_REQUEST(nonce) + ticket
WorldServer:
GETDEL ticket:{token} ← consome
valida: world_id==EU? char_id válido? não expirou?
se OK:
DECR world:{world_id}:reserved ← libera reserva
HINCRBY world:{world_id}:status pop 1
PUBLISH world:events ...
fetch char data via canal C → spawn
se NOK:
S_CONNECT_REJECT
```
**Por que opaque + Valkey lookup (não JWT):** revogação trivial (DEL), single-use natural (GETDEL), payload menor no UDP (32 bytes vs ~200 do JWT). Round-trip Valkey de ~3-5ms é desprezível no handshake (acontece 1x por sessão de horas).
### Single sign-on enforcement (sem char duplicado in-world)
Sem isso, dois clientes podem entrar com mesmo char → item dup, corrupção. Acontece em **todo** MMO sem proteção.
```
CharServer ao emitir ticket (antes do SETEX):
active = GET char:active:{char_id}
se active existe (account já está in-world em algum mundo):
POST /interserver/worlds/{active.world_id}/kick { char_id, reason: "logged_in_elsewhere" }
aguarda confirmação (com timeout 3s)
se WorldServer não respondeu: DEL char:active:{char_id} forçado
prossegue com ticket
WorldServer ao spawnar:
SETEX char:active:{char_id} 120 '{"world_id":...,"node_id":...}'
refresh a cada 60s (renova TTL)
WorldServer ao despawnar (logout/crash):
DEL char:active:{char_id}
```
### Concurrency control no writeback (sem silent corruption)
Cenário: WorldServer cacheou char na memória; admin via SQL muda level. Próximo writeback do WorldServer sobrescreve. Solução padrão = optimistic locking via `version`.
```sql
characters: version INT UNSIGNED DEFAULT 0
```
```
WorldServer carrega char:
GET /interserver/characters/{id} → guarda version=N
WorldServer writeback:
POST /interserver/characters/{id}/checkpoint { pos, inv, ..., version: N }
CharServer SQL:
UPDATE characters SET ..., version=N+1 WHERE id=$id AND version=$N
se affected_rows=0:
409 Conflict
WorldServer recarrega + faz merge + retry
```
### Capacity com reserva (anti-overbooking)
`pop >= cap` ingênuo causa overbooking: 50 tickets emitidos não conectaram ainda; pop reporta 450/500; entra mais 50; quando todos conectarem → 501/500. Bug clássico de New World/WoW launch.
**Solução padrão:** ticket emitido **reserva slot** em Valkey:
```
Capacidade real = pop + reserved < cap
pop = HGET world:{id}:status pop
reserved = GET world:{id}:reserved (decai com expiração natural)
Emit ticket:
if pop + reserved >= cap: enfileira (Fase 4) ou reject WorldFull
else: SETEX ticket:{token} 10 ... + INCR world:{id}:reserved
WorldServer consome ticket:
DECR world:{id}:reserved + INCR pop
Ticket expira sem ser consumido (timeout):
CharServer monitora via keyspace notifications ou cleanup periódico → DECR world:{id}:reserved
```
### QueryPort: dois usos distintos (separar sempre)
| Uso | Quem consome | Como Zeus faz | Quando |
|---|---|---|---|
| **(A)** Estado vivo pro cliente do jogo | Cliente Unreal logado no CharServer | HTTPS interno (Fase 1) → push Valkey (Fase 2+). Cliente sempre via WSS/CharServer | Implementado nesta frente |
| **(B)** Visibilidade externa pública | Steam server browser, Battlemetrics, GameTracker | UDP A2S (protocolo Valve Source), porta 27015, rate-limited | Roadmap Fase 7 (Steam launch) |
WorldServer **não usa QueryPort UDP** pra (A) — gasto desnecessário e expõe estado interno. (B) é uma listener UDP separado, anônimo, rate-limited.
---
## Schema MySQL (Fase 1)
### Tabela `worlds`
```typescript
@Entity({ name: 'worlds' })
export class WorldEntity {
@ZUuid({ primary: true })
id!: Buffer; // UUID v4 binary(16)
@Column({ type: 'varchar', length: 64, unique: true })
name!: string;
@ZEnum(['na', 'sa', 'eu', 'br', 'as'])
region!: AccountRegion;
@Column({ type: 'varchar', length: 255 })
host!: string;
@Column({ type: 'smallint', unsigned: true })
port!: number;
@Column({ type: 'smallint', unsigned: true, default: 500 })
capacity!: number;
@ZEnum(['online', 'maintenance', 'offline'], { default: 'offline' })
state!: WorldState;
/** Hash Argon2id do world_secret. */
@Column({ name: 'secret_hash', type: 'varchar', length: 255 })
secretHash!: string;
@Column({ name: 'last_claim_at', type: 'datetime', nullable: true })
lastClaimAt!: Date | null;
@Column({ name: 'last_claim_node', type: 'varchar', length: 64, nullable: true })
lastClaimNode!: string | null;
@CreateDateColumn() createdAt!: Date;
@UpdateDateColumn() updatedAt!: Date;
}
```
### Alteração em `characters`
A entity atual ([Character.entity.ts](Server/ZeusCharServer/src/database/entities/MMO/Character.entity.ts)) tem só campos básicos (id, account_id, slot, name, class_id, level, exp, posição, appearance). A Fase 1 adiciona:
```sql
ALTER TABLE characters
-- Roteamento de mundo
ADD COLUMN world_id BINARY(16) NULL,
ADD CONSTRAINT fk_characters_world FOREIGN KEY (world_id) REFERENCES worlds(id) ON DELETE RESTRICT,
ADD INDEX idx_characters_account_world (account_id, world_id),
-- Concurrency control (writeback Fase 3)
ADD COLUMN version INT UNSIGNED NOT NULL DEFAULT 0,
-- Stats primários estilo Ragnarok (ver ARQUITETURA_CHARACTER_MODEL.md)
ADD COLUMN str SMALLINT UNSIGNED NOT NULL DEFAULT 1,
ADD COLUMN agi SMALLINT UNSIGNED NOT NULL DEFAULT 1,
ADD COLUMN vit SMALLINT UNSIGNED NOT NULL DEFAULT 1,
ADD COLUMN `int` SMALLINT UNSIGNED NOT NULL DEFAULT 1,
ADD COLUMN dex SMALLINT UNSIGNED NOT NULL DEFAULT 1,
ADD COLUMN luk SMALLINT UNSIGNED NOT NULL DEFAULT 1,
-- Progressão (já tem level/exp = base; adicionar job_level/job_exp)
ADD COLUMN job_level INT UNSIGNED NOT NULL DEFAULT 1,
ADD COLUMN job_exp BIGINT UNSIGNED NOT NULL DEFAULT 0,
-- HP/SP atuais e máximos (max é cache; recalcula em level/equip change)
ADD COLUMN hp INT UNSIGNED NOT NULL DEFAULT 40,
ADD COLUMN max_hp INT UNSIGNED NOT NULL DEFAULT 40,
ADD COLUMN sp INT UNSIGNED NOT NULL DEFAULT 11,
ADD COLUMN max_sp INT UNSIGNED NOT NULL DEFAULT 11,
-- Pontos não-gastos (allocação no level up)
ADD COLUMN status_point INT UNSIGNED NOT NULL DEFAULT 0,
ADD COLUMN skill_point INT UNSIGNED NOT NULL DEFAULT 0,
-- Moeda
ADD COLUMN zeny BIGINT UNSIGNED NOT NULL DEFAULT 0;
```
**Notas:**
- `world_id` é nullable (sem chars em prod hoje — evita downtime futuro). Validação no service: novos chars devem ter `world_id`.
- Estrutura flat segue rathena (`.bases/rathena/rathena-master/sql-files/main.sql:209-296`) — padrão da indústria emuladora. Sem entidade `character_stats` separada (1:1 FK seria JOIN inútil).
- Defaults vêm da row Novato no `jobs.yml` (ver doc Character Model).
- **Stats derivados (ATK/MATK/DEF/MDEF/hit/flee/crit/aspd) NÃO entram aqui** — são sempre recalculados em runtime no WorldServer via fórmulas. Persistir é bug fest (esquece atualizar quando muda equip/buff/level).
- `version` (optimistic locking) usado em writeback da Fase 3.
> **Para o detalhamento do modelo de personagem** (stats primários vs derivados, fórmulas de ATK/MATK/DEF, jobs.yml, stat allocation, leveling curve, classes Novato → especialização), ver doc dedicado [`ARQUITETURA_CHARACTER_MODEL.md`](ARQUITETURA_CHARACTER_MODEL.md).
### Schema Valkey (Fase 2+)
```
world:{id}:status HSET { pop, cap, queue_len, state, updated_at } TTL 30s (renovado a cada 5s)
world:{id}:reserved INTEGER (sem TTL — decai por DECR)
world:events PUB/SUB channel (transitions + player_in/out)
queue:world:{id} ZSET { account_id → timestamp_ms } (Fase 4, Valkey Streams)
char:active:{char_id} STRING '{world_id,node_id}' TTL 120s (renovado pelo WS)
ticket:{token} STRING '{account_id,char_id,world_id,expires_at}' TTL 10s, GETDEL
session:{jti} HSET { account_id, node_id, ip, expires_at } TTL = JWT TTL (Fase 5)
```
---
## Roadmap em fases
### Fase 1 — Lista de mundos estática + char por mundo (sem Valkey)
**Objetivo:** ServerSelect mostra mundos reais; CharSelect filtra por mundo; cada novo char nasce vinculado.
**MySQL:**
- Migration `worlds` + `characters.world_id` + `characters.version`.
- CLI `npm run world -- create/list/set-state/rotate-secret`.
**CharServer (TS):**
- `WorldRepository` + `WorldService` (list/get/setState/claim/heartbeat).
- Opcodes novos:
- `C_WORLD_LIST_REQUEST=2060` / `S_WORLD_LIST=2061` (region-aware — filtra por `ZEUS_REGION`).
- Estender `C_CHAR_LIST_REQUEST` com `world_id`.
- Estender `C_CHAR_CREATE` com `world_id` obrigatório.
- Endpoints HTTPS (canal C):
- `POST /interserver/worlds/{id}/claim { secret }` — Argon2id check + region check → `state=online`. **Rate-limited** (1 req/min por IP — anti crash-loop hammering).
- `POST /interserver/worlds/{id}/heartbeat { pop, queue_len, state }` — fallback HTTP antes do Valkey (UPDATE em `worlds.last_heartbeat_at` ou tabela `worlds_live` auxiliar).
- `S_CHAR_SELECT` valida `char.world_id == world.id` e `world.state == online`. Reject reasons novos:
- `WorldOffline=8`, `WorldMaintenance=9`, `WorldFull=10`, `WorldRegionMismatch=11`.
- Devolve `worldHost/Port` do mundo escolhido (não mais hardcoded).
**Cliente Unreal:**
- Novo estado `EZMMOFrontEndState::CharSelect` entre ServerSelect e ingame.
- `UUIServerSelectScreen_Base`: pede lista, popula cards, clique → `SelectedWorldId` no `UUIFrontEndFlowSubsystem` + transição CharSelect.
- `UUICharSelectScreen_Base` (NOVO): lista filtrada por `SelectedWorldId`, criar/deletar/entrar.
- `CharServerOpcodes.h`: novos opcodes + reasons.
**Métricas Prometheus (mínimas):**
- `char_auth_total{result}`, `char_select_total{result}`, `world_list_request_total`.
**Files críticos:**
- `Server/ZeusCharServer/src/database/entities/MMO/World.entity.ts` (NOVO)
- `Server/ZeusCharServer/src/database/entities/MMO/Character.entity.ts` (add `world_id`, `version`)
- `Server/ZeusCharServer/src/services/world.service.ts` (NOVO)
- `Server/ZeusCharServer/src/http/routes/interserver.ts` (estender)
- `Server/ZeusCharServer/src/services/char-{list,create,select}.service.ts`
- `Server/ZeusCharServer/scripts/world-cli.ts` (NOVO)
- `Server/ZeusCharServer/src/protocol/CharOpcodes.ts`
- `Clients/ZMMO/Source/ZMMO/Game/UI/FrontEnd/UIServerSelectScreen_Base.{h,cpp}`
- `Clients/ZMMO/Source/ZMMO/Game/UI/FrontEnd/UICharSelectScreen_Base.{h,cpp}` (NOVO)
- `Clients/ZMMO/Source/ZMMO/Data/UI/FrontEndTypes.h`
### Fase 2 — Valkey + heartbeat real
**Objetivo:** pop/state real-time no ServerSelect; UI atualiza sem refresh.
**Setup:**
- `docker-compose.yml` com `valkey/valkey:7-alpine`.
- `ioredis` no CharServer (API 100% compatível com Valkey 7.2).
- Cliente Valkey C++ no WorldServer (`hiredis` — C, popular; ou `cpp_redis` — header-only, mais moderno; decidir na Fase 2).
**WorldServer (C++):**
- Worker thread separada (não bloqueia game loop):
- On player_connected/disconnected: `HINCRBY` + `PUBLISH`.
- On state_change: `HSET state` + `PUBLISH`.
- Periodic 5s: refresh `HSET` completo + `PEXPIRE 30000`.
- `WorldId` validado contra `worlds` no boot via HTTP claim (Fase 1).
**CharServer (TS):**
- `ValkeyService` singleton (ioredis client + subscriber).
- `WorldService.getLiveStatus(id)` lê do Valkey (fallback `state=offline` se key não existe).
- Novo opcode `S_WORLD_STATUS_UPDATE=2062` — push pra cliente quando recebe `world:events`.
- `S_WORLD_LIST` agora inclui `pop/queue_len/state` do Valkey no payload inicial.
**Cliente:**
- ServerSelect: subscribe na ativação; listener `S_WORLD_STATUS_UPDATE` atualiza cards em tempo real.
**Métricas:** `world_heartbeat_age_seconds{world_id}` (alerta >30s), `world_population{world_id}`.
### Fase 3 — Ticket validado + writeback de char + concurrency
**Objetivo:** handoff seguro + persistência real do gameplay sem corrupção.
**CharServer:**
- `TicketService.issue({account_id, char_id, world_id})`: `crypto.randomBytes(32)``SETEX ticket:{token} 10` + `INCR world:{id}:reserved`.
- `S_CHAR_SELECT_OK` envia ticket emitido (substitui o random anterior).
- Endpoints novos (canal C):
- `GET /interserver/characters/{id}` — full state (pos, inv, stats, **version**).
- `POST /interserver/characters/{id}/checkpoint { ..., version }` — optimistic UPDATE; 409 se conflict.
- `POST /interserver/audit { type, account_id, char_id, payload }` — audit log (level up, item raro, trade).
**WorldServer:**
- Handshake UDP lê ticket → `GETDEL ticket:{token}` no Valkey → valida campos.
- Se válido: `DECR world:{id}:reserved` + `INCR pop` + `GET /interserver/characters/{id}` → spawn.
- Worker 60s + on logout: `POST /interserver/characters/{id}/checkpoint`. Em 409 → recarrega + merge + retry.
**Cliente:** transparente (só passa o ticket que recebeu).
**Métricas:** `ticket_{issued,consumed,expired}_total`, `character_writeback_{ok,conflict}_total`.
### Fase 3.5 — Single sign-on enforcement
**Objetivo:** impedir char duplicado in-world (item dup, corrupção).
**CharServer:**
- Antes de emitir ticket: `GET char:active:{char_id}`. Se existe e é mundo diferente → `POST /interserver/worlds/{active.world_id}/kick { char_id }` com timeout 3s. Se timeout → `DEL char:active:{char_id}` forçado (assumir world morreu) + log warn.
**WorldServer:**
- On spawn: `SETEX char:active:{char_id} 120 '{world_id, node_id}'`.
- Worker 60s: refresh TTL (`PEXPIRE`).
- On despawn/logout/crash recover: `DEL char:active:{char_id}`.
- Endpoint `POST /admin/kick { char_id, reason }`: força disconnect daquele char.
**Métricas:** `sso_kick_total{reason=logged_in_elsewhere|stale}`.
### Fase 4 — Queue com Valkey Streams (não PUB/SUB)
**Objetivo:** quando `pop + reserved >= cap`, enfileirar em vez de rejeitar.
**Por que Streams e não PUB/SUB:** PUB/SUB é fire-and-forget; CharServer reiniciando perde eventos `slot-free` → jogadores ficam eternamente na fila. Streams (XADD/XREAD/XACK) persiste mensagens, consumer pode catch-up após restart.
**CharServer:**
- Em `S_CHAR_SELECT` com pop cheio: `ZADD queue:world:{id} {ts_ms} {account_id}` + responde `S_CHAR_QUEUED(position, ETA)`.
- Worker push 5s: `S_QUEUE_UPDATE` pra cada um na fila (com posição via `ZRANK`).
- Consumer da stream `world:slot-free:{id}`: `ZPOPMIN` da queue → emite ticket → `S_CHAR_QUEUE_READY(ticket, worldHost, worldPort)`.
- Cleanup de zombie entries: WS disconnect → `ZREM` do account_id em todas as filas.
**WorldServer:**
- Em logout/disconnect: `XADD world:slot-free:{id} * world_id={id}` (Valkey Stream).
**Cliente:** tela de fila (overlay), opção "Cancelar" → `C_CHAR_QUEUE_CANCEL`.
**Métricas:** `queue_length{world_id}`, `queue_wait_seconds_p99{world_id}`.
### Fase 5 — Multi-node CharServer
**Objetivo:** rodar N instâncias atrás de LB dentro de uma região.
- Sticky session por cookie no gateway (HAProxy/Nginx) — canal A WSS.
- `session:{jti}` em Valkey HSET — revogação JWT global via `PUBLISH session:revoke`.
- Health endpoint pro LB; readiness probe diferenciado.
- **Connection draining**: SIGTERM handler seta `draining=true` flag, recusa novas conexões WSS mas mantém existentes até timeout (60s).
**Métricas:** `ws_connections_active{node_id}`, `node_draining{node_id}`.
### Fase 6 — Multi-region
**Objetivo:** roll-out NA/SA/EU/AS com DB global.
- MySQL Primary central + read replicas regionais (async replication).
- Valkey cluster por região (sem cross-region sync — cada região é uma ilha de estado vivo).
- DNS regional via GeoDNS (Route53/Cloudflare) ou subdomain estático.
- WorldServer `CharServerEndpoint` aponta pro hostname regional.
- Worlds filtrados por região automaticamente (`WHERE region = $ZEUS_REGION`).
- Read-your-own-writes: flag `READ_PRIMARY=1` no service layer pra reads críticos imediatamente após write.
- World transfer cross-region: endpoint admin `POST /admin/characters/{id}/transfer { target_world_id }`.
- **mTLS interserver** (canal C) — cada WorldServer com cert único, CharServer valida chain. Substitui shared secret pra cross-region.
- **JWT key rotation** via JWKS endpoint (`GET /.well-known/jwks.json`) com `kid` header.
- **Liveness probe externa** (não só heartbeat self-reported): CharServer faz GET `/health` no WorldServer a cada 30s — detecta congelado (heartbeat fluindo + game thread morta).
- **Disaster recovery:** GeoDNS roteia pra região mais próxima em catástrofe. Usuários re-loginam.
### Fase 7 — WorldServer QueryPort A2S público (sob demanda)
**Objetivo:** expor WorldServer pra Steam server browser, Battlemetrics, GameTracker.
- Listener UDP separado (porta 27015 default), independente do gameplay UDP.
- Protocolo Valve A2S: `A2S_INFO` (0x54), `A2S_PLAYER` (0x55), `A2S_RULES` (0x56) com challenge anti-spoofing (0x41, obrigatório desde 2020).
- Rate-limit por IP (token bucket 30 req/s).
- Registro no Steam Master Server (`hl2master.steampowered.com:27011`).
- **Nunca compartilha identidade com canal C** — stateless, anônimo, jamais expõe `world_secret`.
- Flag `enable_query_port` no `server.json` (default off em dev/CI).
- Ref: https://developer.valvesoftware.com/wiki/Server_queries
---
## Decisões consolidadas
1. **CharServer dono dos `characters`** (MySQL global). Coluna `world_id` (FK) + `version` (optimistic locking).
2. **WorldServer stateless** quanto a persistência. Cache em memória + writeback ~60s.
3. **Lista de mundos:** `worlds` no MySQL (durável). Estado vivo (pop/queue/state) em Valkey (Fase 2+).
4. **Heartbeat WorldServer:** três triggers combinados (event + state-change + periodic 5s) escrevendo direto no Valkey (Fase 2+); HTTP POST como fallback na Fase 1.
5. **Mundo em manutenção:** sempre acessível em ServerSelect/CharSelect; botão "Entrar" rejeita.
6. **Char list filtrada por mundo:** escolha do mundo na ServerSelect → CharSelect com chars desse mundo. Criação inclui `world_id`.
7. **Ticket de handoff:** opaque 32-byte token, TTL 10s, `GETDEL` no WorldServer, com **reserva de slot** (`INCR world:{id}:reserved`).
8. **SSO enforcement:** `char:active:{char_id}` em Valkey + kick old session via canal C antes de emitir ticket novo.
9. **Concurrency control:** coluna `version` em `characters`, optimistic UPDATE no writeback (409 → retry com merge).
10. **Queue:** MVP sem fila (Fase 1-3 → reject `WorldFull`); **Fase 4 implementa via Valkey Streams** (não PUB/SUB) com cleanup de zombies + reserva integrada.
11. **Valkey** (fork BSD do Redis 7.2 mantido pela Linux Foundation): a partir da Fase 2. `ioredis` no Node funciona sem mudança. Docker `valkey/valkey:7-alpine`.
12. **`world_id`:** admin-create + WorldServer-claim com Argon2id secret (padrão AccelByte Armada). Rate-limited (anti crash-loop).
13. **Topologia geo-distribuída:** Valkey + CharServer por região; MySQL primary global + read replicas regionais. Multi-region é Fase 6.
14. **QueryPort:** UDP A2S público apenas pra visibilidade externa (Steam/Battlemetrics) na Fase 7. Estado pro cliente do jogo sempre via WSS/CharServer.
15. **mTLS interserver:** Fase 6 (substitui shared secret pra cross-region).
16. **JWT key rotation:** JWKS endpoint na Fase 6.
17. **Métricas Prometheus:** lista mínima em cada fase (`char_auth_total`, `world_heartbeat_age_seconds`, `ticket_*_total`, `queue_length`, etc.).
---
## Verificação por fase
**Fase 1:**
- `npm run world -- create --name="Aurora" --host=127.0.0.1 --port=27777 --region=br --cap=500` → imprime `world_id` + `world_secret`.
- `npm run world -- create --name="Volcanus" --host=127.0.0.1 --port=27778 --region=na --cap=500`.
- `npm run world -- list` mostra ambos com `state=offline`.
- WorldServer com `world_id`+`world_secret` no `Config/server.json` sobe → `POST /interserver/worlds/{id}/claim``204` + log "claimed". `state` no DB vira `online`.
- Claim com secret errado → `403` + abort.
- PIE login → ServerSelect: 2 cards reais.
- Clique em Aurora → CharSelect com chars de `world_id=Aurora` (filtrado).
- Criar Personagem → INSERT com `world_id=Aurora`.
- `npm run world -- set-state <id> maintenance` → entrar → reject `WorldMaintenance` (CharSelect ainda funciona).
**Fase 2:**
- `docker-compose up -d valkey` sobe Valkey.
- `valkey-cli HGETALL world:{id}:status` mostra pop fresh.
- Player conecta → ServerSelect: pop incrementa em tempo real sem refresh.
- Matar WorldServer → 30s depois ServerSelect mostra "Offline".
**Fase 3:**
- WorldServer log "ticket validado, account=X char=Y".
- Reuse de mesmo ticket → reject.
- Editar inv in-game → 60s depois `SELECT inv FROM characters` mostra novo state.
- Editar level via SQL direto durante gameplay → writeback do WorldServer dá 409 → recarrega + merge.
**Fase 3.5:**
- Logar com char Y no PIE 1.
- Logar mesmo char Y no PIE 2 → CharServer kicka PIE 1 antes de emitir ticket. `S_DISCONNECT` no PIE 1 com motivo "logged_in_elsewhere".
**Fase 4:**
- `npm run world -- set-state <id> ...` setar `cap=1`.
- Player A entra → ok.
- Player B entra → fila posição 1.
- Player A logout → Player B recebe ticket + entra.
- Restart CharServer durante fila → Player B continua na fila (Streams persistiu).
**Fase 5:**
- 2 instâncias `npm run dev` em portas diferentes + nginx LB.
- Login → matar node ativo → reconnect cai no outro → sessão preserva (Valkey).
- `kill -TERM` no node ativo → drena (sem cortar conexão imediatamente) → desce após timeout.
**Fase 6:**
- Subir CharServer SA + CharServer NA + Valkey SA + Valkey NA + read replicas.
- WorldServer SA claim em CharServer SA: ok.
- WorldServer SA tentar claim em CharServer NA: 403 region mismatch.
- Cliente conecta `sa.zeusmmo.com.br`: vê só mundos SA.
---
## Notas e riscos (após autocrítica)
- **Live Coding ≠ UCLASS nova**: cada nova classe C++ no cliente exige rebuild completo + restart do editor. Planejar fazer todas as classes novas de uma fase no mesmo build.
- **Reconnect WSS no cliente:** `UZeusCharServerSubsystem` hoje não tem retry/reconnect. Gap não-trivial — implementar antes da Fase 5 (multi-node faz failover frequente).
- **WorldServer congelado vs morto:** heartbeat continua fluindo se game thread trava mas worker thread vive. Liveness probe externa (Fase 6) detecta isso. Fase 1-5 fica como risco aceito.
- **Argon2id no claim:** custo 50-200ms por hash. WorldServer faz 1x no boot — irrelevante. Crash-loop hammering → rate limit no endpoint (1/min/IP).
- **Replication lag MySQL:** assumido <100ms (verdade em RDS/Aurora). VPS amador pode ser 1-5s. Medir antes da Fase 6.
- **Limite de capacity por mundo:** ~500-1500 players com WorldServer monolítico. Acima disso precisa **world sharding interno** (zone servers) — Fase 8+ se necessário. Documentar como limite atual.
- **Chat global, party, friends:** fora desta frente. Entram como **serviço paralelo no CharServer** (canal A, opcodes 3000+) depois da Fase 3.
- **Anti-cheat client-side (EAC/BattlEye/VAC):** decisão comercial pós-launch. Fora de escopo.
- **Area of Interest no WorldServer:** indispensável pra >100 players concorrentes. Não bloqueia Fase 1-5 (dev tem 1-10 players). Documentar como pré-requisito de teste de carga.
- **CI/CD multi-region:** Fase 6 pressupõe Kubernetes ou equivalente. Decisão de ops, fora desta arquitetura.
---
## O que esta frente entrega vs roadmap completo
**Esta frente (Fases 1-3.5):** ServerSelect funcional + char por mundo + handoff seguro + SSO + writeback sem corrupção. Suficiente pra dev real com 1-50 players concorrentes em 1 região, 1-5 mundos.
**Roadmap longo (Fases 4-7):** queue, multi-node, multi-region, A2S público. Cada uma incremental — não quebra as anteriores.
**Fora do roadmap (frentes paralelas):** chat global/party/friends, anti-cheat client-side, world sharding interno (>1000 players), CI/CD K8s, observability completa (tracing distribuído), disaster recovery automatizado.

File diff suppressed because one or more lines are too long

View File

@@ -1,10 +1,10 @@
[/Script/EngineSettings.GameMapsSettings] [/Script/EngineSettings.GameMapsSettings]
GameDefaultMap=/Game/ThirdPerson/Lvl_ThirdPerson.Lvl_ThirdPerson GameDefaultMap=/Game/ZMMO/Maps/FrontEnd/L_FrontEnd.L_FrontEnd
EditorStartupMap=/Game/ThirdPerson/Lvl_ThirdPerson.Lvl_ThirdPerson EditorStartupMap=/Game/ZMMO/Maps/FrontEnd/L_FrontEnd.L_FrontEnd
; GameMode/PlayerController/PlayerCharacter agora moram em C++ (Game/Modes/Entity/Controller). ; GameMode/PlayerController/PlayerCharacter agora moram em C++ (Game/Modes/Entity/Controller).
; O motor pode spawnar AZMMOGameMode directamente; um BP filho continua opcional. ; O motor pode spawnar AZeusGameMode directamente; um BP filho continua opcional.
GlobalDefaultGameMode=/Script/ZMMO.ZMMOGameMode GlobalDefaultGameMode=/Script/ZMMO.ZeusGameMode
GameInstanceClass=/Script/ZMMO.ZMMOGameInstance GameInstanceClass=/Script/ZMMO.ZeusGameInstance
[/Script/Engine.RendererSettings] [/Script/Engine.RendererSettings]
r.ReflectionMethod=1 r.ReflectionMethod=1
@@ -75,14 +75,64 @@ DefaultGraphicsPerformance=Maximum
AppliedDefaultGraphicsPerformance=Maximum AppliedDefaultGraphicsPerformance=Maximum
[/Script/Engine.Engine] [/Script/Engine.Engine]
; CommonUI: viewport client que roteia input (teclado/gamepad) e foco para a UI.
GameViewportClientClassName=/Script/CommonUI.CommonGameViewportClient
+ActiveGameNameRedirects=(OldGameName="TP_ThirdPerson",NewGameName="/Script/ZMMO") +ActiveGameNameRedirects=(OldGameName="TP_ThirdPerson",NewGameName="/Script/ZMMO")
+ActiveGameNameRedirects=(OldGameName="/Script/TP_ThirdPerson",NewGameName="/Script/ZMMO") +ActiveGameNameRedirects=(OldGameName="/Script/TP_ThirdPerson",NewGameName="/Script/ZMMO")
; Redirects do template antigo para a nova arquitectura (Game/Modes/Entity/Controller). ; Redirects do template antigo para a nova arquitectura (Game/Modes/Entity/Controller).
; ZMMOCharacter foi reescrito como ZMMOPlayerCharacter (ACharacter local com CMC livre). ; Pipeline: TP_ThirdPerson* -> ZMMO* (legado) -> Zeus* (atual). Os redirects de
+ActiveClassRedirects=(OldClassName="TP_ThirdPersonPlayerController",NewClassName="ZMMOPlayerController") ; ZMMO*->Zeus* preservam asset/BP refs criadas durante o periodo ZMMO* (Batches 1-3).
+ActiveClassRedirects=(OldClassName="TP_ThirdPersonGameMode",NewClassName="ZMMOGameMode") +ActiveClassRedirects=(OldClassName="TP_ThirdPersonPlayerController",NewClassName="ZeusPlayerController")
+ActiveClassRedirects=(OldClassName="TP_ThirdPersonCharacter",NewClassName="ZMMOPlayerCharacter") +ActiveClassRedirects=(OldClassName="TP_ThirdPersonGameMode",NewClassName="ZeusGameMode")
+ActiveClassRedirects=(OldClassName="ZMMOCharacter",NewClassName="ZMMOPlayerCharacter") +ActiveClassRedirects=(OldClassName="TP_ThirdPersonCharacter",NewClassName="ZeusCharacter")
+ActiveClassRedirects=(OldClassName="ZMMOCharacter",NewClassName="ZeusCharacter")
+ActiveClassRedirects=(OldClassName="ZMMOPlayerCharacter",NewClassName="ZeusCharacter")
+ActiveClassRedirects=(OldClassName="ZMMOPlayerController",NewClassName="ZeusPlayerController")
+ActiveClassRedirects=(OldClassName="ZMMOFrontEndPlayerController",NewClassName="ZeusFrontEndPlayerController")
+ActiveClassRedirects=(OldClassName="ZMMOGameMode",NewClassName="ZeusGameMode")
+ActiveClassRedirects=(OldClassName="ZMMOFrontEndGameMode",NewClassName="ZeusFrontEndGameMode")
+ActiveClassRedirects=(OldClassName="ZMMOGameInstance",NewClassName="ZeusGameInstance")
+ActiveClassRedirects=(OldClassName="ZMMOHUD",NewClassName="ZeusHUD")
+ActiveClassRedirects=(OldClassName="ZMMOPlayerState",NewClassName="ZeusPlayerState")
+ActiveClassRedirects=(OldClassName="ZMMOEntity",NewClassName="ZeusEntity")
+ActiveClassRedirects=(OldClassName="ZMMOPlayerProxy",NewClassName="ZeusPlayerProxy")
+ActiveClassRedirects=(OldClassName="ZMMOProxyMovementComponent",NewClassName="ZeusProxyMovementComponent")
+ActiveClassRedirects=(OldClassName="ZMMOWorldSubsystem",NewClassName="ZeusWorldSubsystem")
+ActiveClassRedirects=(OldClassName="ZMMOThemeDataAsset",NewClassName="ZeusThemeDataAsset")
+ActiveClassRedirects=(OldClassName="ZMMOThemeSubsystem",NewClassName="ZeusThemeSubsystem")
+ActiveClassRedirects=(OldClassName="ZMMOLoginSaveGame",NewClassName="ZeusLoginSaveGame")
+ActiveClassRedirects=(OldClassName="ZMMOHudWidget",NewClassName="ZeusHudWidget")
+ActiveClassRedirects=(OldClassName="ZMMOHudPlayerVitalsWidget",NewClassName="ZeusHudPlayerVitalsWidget")
+ActiveClassRedirects=(OldClassName="ZMMOLoadingProfilesDataAsset",NewClassName="ZeusLoadingProfilesDataAsset")
[CoreRedirects]
; Migração v1→v2 do FUIStylePanel: TMap<EUIPanelTexture,...> (Enum) → TMap<FName,...>.
; Os campos *_DEPRECATED preservam a leitura do asset antigo; PostSerialize copia
; pros novos ByName e após re-save os legacy ficam vazios. Os redirects podem
; ser removidos quando todos os DTs do projeto tiverem sido re-salvos.
+PropertyRedirects=(OldName="/Script/ZMMO.UIStylePanel.PanelBackground",NewName="/Script/ZMMO.UIStylePanel.PanelBackground_DEPRECATED")
+PropertyRedirects=(OldName="/Script/ZMMO.UIStylePanel.PanelOutline",NewName="/Script/ZMMO.UIStylePanel.PanelOutline_DEPRECATED")
+PropertyRedirects=(OldName="/Script/ZMMO.UIStylePanel.PanelOutlineEffect",NewName="/Script/ZMMO.UIStylePanel.PanelOutlineEffect_DEPRECATED")
; --- M7: Modulo cliente "ZMMOJobs" foi renomeado pra "ZeusJobs" + classe
; UZMMOJobDataAsset -> UZeusJobDataAsset. Os 7 DA_Job_* em /Game/ZMMO/Data/Jobs/
; foram salvos apontando pra /Script/ZMMOJobs.ZMMOJobDataAsset (modulo + classe
; antigos). ActiveClassRedirects so' troca o NOME da classe, nao o package;
; precisamos de CoreRedirects com path completo + PackageRedirect.
+PackageRedirects=(OldName="/Script/ZMMOJobs",NewName="/Script/ZeusJobs")
+ClassRedirects=(OldName="/Script/ZMMOJobs.ZMMOJobDataAsset",NewName="/Script/ZeusJobs.ZeusJobDataAsset")
; --- M7: USTRUCTs renomeadas (mesmo modulo ZMMO). DTs salvos com Row Structure
; apontando pro nome antigo (ZMMOMapDef, ZMMOLoadingTipRow). USTRUCT usa
; +StructRedirects (nao +ClassRedirects).
+StructRedirects=(OldName="/Script/ZMMO.ZMMOMapDef",NewName="/Script/ZMMO.ZeusMapDef")
+StructRedirects=(OldName="/Script/ZMMO.ZMMOLoadingTipRow",NewName="/Script/ZMMO.ZeusLoadingTipRow")
; DA_LoadingProfiles salvo com TMap<EZMMOLoadingContext, FZMMOLoadingProfile>;
; FZMMOLoadingProfile contem TArray<FZMMOLoadingStepDef>. Precisa de 3 redirects
; (1 enum + 2 struct) pra carregar sem warnings nem perda de dados.
+EnumRedirects=(OldName="/Script/ZMMO.EZMMOLoadingContext",NewName="/Script/ZMMO.EZeusLoadingContext")
+EnumRedirects=(OldName="/Script/ZMMO.EZMMOLoadingStepStatus",NewName="/Script/ZMMO.EZeusLoadingStepStatus")
+StructRedirects=(OldName="/Script/ZMMO.ZMMOLoadingProfile",NewName="/Script/ZMMO.ZeusLoadingProfile")
+StructRedirects=(OldName="/Script/ZMMO.ZMMOLoadingStepDef",NewName="/Script/ZMMO.ZeusLoadingStepDef")
[/Script/AndroidFileServerEditor.AndroidFileServerRuntimeSettings] [/Script/AndroidFileServerEditor.AndroidFileServerRuntimeSettings]
bEnablePlugin=True bEnablePlugin=True
@@ -98,3 +148,60 @@ ConnectionType=USBOnly
bUseManualIPAddress=False bUseManualIPAddress=False
ManualIPAddress= ManualIPAddress=
[/Script/Engine.CollisionProfile]
-Profiles=(Name="NoCollision",CollisionEnabled=NoCollision,ObjectTypeName="WorldStatic",CustomResponses=((Channel="Visibility",Response=ECR_Ignore),(Channel="Camera",Response=ECR_Ignore)),HelpMessage="No collision",bCanModify=False)
-Profiles=(Name="BlockAll",CollisionEnabled=QueryAndPhysics,ObjectTypeName="WorldStatic",CustomResponses=,HelpMessage="WorldStatic object that blocks all actors by default. All new custom channels will use its own default response. ",bCanModify=False)
-Profiles=(Name="OverlapAll",CollisionEnabled=QueryOnly,ObjectTypeName="WorldStatic",CustomResponses=((Channel="WorldStatic",Response=ECR_Overlap),(Channel="Pawn",Response=ECR_Overlap),(Channel="Visibility",Response=ECR_Overlap),(Channel="WorldDynamic",Response=ECR_Overlap),(Channel="Camera",Response=ECR_Overlap),(Channel="PhysicsBody",Response=ECR_Overlap),(Channel="Vehicle",Response=ECR_Overlap),(Channel="Destructible",Response=ECR_Overlap)),HelpMessage="WorldStatic object that overlaps all actors by default. All new custom channels will use its own default response. ",bCanModify=False)
-Profiles=(Name="BlockAllDynamic",CollisionEnabled=QueryAndPhysics,ObjectTypeName="WorldDynamic",CustomResponses=,HelpMessage="WorldDynamic object that blocks all actors by default. All new custom channels will use its own default response. ",bCanModify=False)
-Profiles=(Name="OverlapAllDynamic",CollisionEnabled=QueryOnly,ObjectTypeName="WorldDynamic",CustomResponses=((Channel="WorldStatic",Response=ECR_Overlap),(Channel="Pawn",Response=ECR_Overlap),(Channel="Visibility",Response=ECR_Overlap),(Channel="WorldDynamic",Response=ECR_Overlap),(Channel="Camera",Response=ECR_Overlap),(Channel="PhysicsBody",Response=ECR_Overlap),(Channel="Vehicle",Response=ECR_Overlap),(Channel="Destructible",Response=ECR_Overlap)),HelpMessage="WorldDynamic object that overlaps all actors by default. All new custom channels will use its own default response. ",bCanModify=False)
-Profiles=(Name="IgnoreOnlyPawn",CollisionEnabled=QueryOnly,ObjectTypeName="WorldDynamic",CustomResponses=((Channel="Pawn",Response=ECR_Ignore),(Channel="Vehicle",Response=ECR_Ignore)),HelpMessage="WorldDynamic object that ignores Pawn and Vehicle. All other channels will be set to default.",bCanModify=False)
-Profiles=(Name="OverlapOnlyPawn",CollisionEnabled=QueryOnly,ObjectTypeName="WorldDynamic",CustomResponses=((Channel="Pawn",Response=ECR_Overlap),(Channel="Vehicle",Response=ECR_Overlap),(Channel="Camera",Response=ECR_Ignore)),HelpMessage="WorldDynamic object that overlaps Pawn, Camera, and Vehicle. All other channels will be set to default. ",bCanModify=False)
-Profiles=(Name="Pawn",CollisionEnabled=QueryAndPhysics,ObjectTypeName="Pawn",CustomResponses=((Channel="Visibility",Response=ECR_Ignore)),HelpMessage="Pawn object. Can be used for capsule of any playerable character or AI. ",bCanModify=False)
-Profiles=(Name="Spectator",CollisionEnabled=QueryOnly,ObjectTypeName="Pawn",CustomResponses=((Channel="WorldStatic",Response=ECR_Block),(Channel="Pawn",Response=ECR_Ignore),(Channel="Visibility",Response=ECR_Ignore),(Channel="WorldDynamic",Response=ECR_Ignore),(Channel="Camera",Response=ECR_Ignore),(Channel="PhysicsBody",Response=ECR_Ignore),(Channel="Vehicle",Response=ECR_Ignore),(Channel="Destructible",Response=ECR_Ignore)),HelpMessage="Pawn object that ignores all other actors except WorldStatic.",bCanModify=False)
-Profiles=(Name="CharacterMesh",CollisionEnabled=QueryOnly,ObjectTypeName="Pawn",CustomResponses=((Channel="Pawn",Response=ECR_Ignore),(Channel="Vehicle",Response=ECR_Ignore),(Channel="Visibility",Response=ECR_Ignore)),HelpMessage="Pawn object that is used for Character Mesh. All other channels will be set to default.",bCanModify=False)
-Profiles=(Name="PhysicsActor",CollisionEnabled=QueryAndPhysics,ObjectTypeName="PhysicsBody",CustomResponses=,HelpMessage="Simulating actors",bCanModify=False)
-Profiles=(Name="Destructible",CollisionEnabled=QueryAndPhysics,ObjectTypeName="Destructible",CustomResponses=,HelpMessage="Destructible actors",bCanModify=False)
-Profiles=(Name="InvisibleWall",CollisionEnabled=QueryAndPhysics,ObjectTypeName="WorldStatic",CustomResponses=((Channel="Visibility",Response=ECR_Ignore)),HelpMessage="WorldStatic object that is invisible.",bCanModify=False)
-Profiles=(Name="InvisibleWallDynamic",CollisionEnabled=QueryAndPhysics,ObjectTypeName="WorldDynamic",CustomResponses=((Channel="Visibility",Response=ECR_Ignore)),HelpMessage="WorldDynamic object that is invisible.",bCanModify=False)
-Profiles=(Name="Trigger",CollisionEnabled=QueryOnly,ObjectTypeName="WorldDynamic",CustomResponses=((Channel="WorldStatic",Response=ECR_Overlap),(Channel="Pawn",Response=ECR_Overlap),(Channel="Visibility",Response=ECR_Ignore),(Channel="WorldDynamic",Response=ECR_Overlap),(Channel="Camera",Response=ECR_Overlap),(Channel="PhysicsBody",Response=ECR_Overlap),(Channel="Vehicle",Response=ECR_Overlap),(Channel="Destructible",Response=ECR_Overlap)),HelpMessage="WorldDynamic object that is used for trigger. All other channels will be set to default.",bCanModify=False)
-Profiles=(Name="Ragdoll",CollisionEnabled=QueryAndPhysics,ObjectTypeName="PhysicsBody",CustomResponses=((Channel="Pawn",Response=ECR_Ignore),(Channel="Visibility",Response=ECR_Ignore)),HelpMessage="Simulating Skeletal Mesh Component. All other channels will be set to default.",bCanModify=False)
-Profiles=(Name="Vehicle",CollisionEnabled=QueryAndPhysics,ObjectTypeName="Vehicle",CustomResponses=,HelpMessage="Vehicle object that blocks Vehicle, WorldStatic, and WorldDynamic. All other channels will be set to default.",bCanModify=False)
-Profiles=(Name="UI",CollisionEnabled=QueryOnly,ObjectTypeName="WorldDynamic",CustomResponses=((Channel="WorldStatic",Response=ECR_Overlap),(Channel="Pawn",Response=ECR_Overlap),(Channel="Visibility",Response=ECR_Block),(Channel="WorldDynamic",Response=ECR_Overlap),(Channel="Camera",Response=ECR_Overlap),(Channel="PhysicsBody",Response=ECR_Overlap),(Channel="Vehicle",Response=ECR_Overlap),(Channel="Destructible",Response=ECR_Overlap)),HelpMessage="WorldStatic object that overlaps all actors by default. All new custom channels will use its own default response. ",bCanModify=False)
+Profiles=(Name="NoCollision",CollisionEnabled=NoCollision,bCanModify=False,ObjectTypeName="WorldStatic",CustomResponses=((Channel="Visibility",Response=ECR_Ignore),(Channel="Camera",Response=ECR_Ignore)),HelpMessage="No collision")
+Profiles=(Name="BlockAll",CollisionEnabled=QueryAndPhysics,bCanModify=False,ObjectTypeName="WorldStatic",CustomResponses=,HelpMessage="WorldStatic object that blocks all actors by default. All new custom channels will use its own default response. ")
+Profiles=(Name="OverlapAll",CollisionEnabled=QueryOnly,bCanModify=False,ObjectTypeName="WorldStatic",CustomResponses=((Channel="WorldStatic",Response=ECR_Overlap),(Channel="Pawn",Response=ECR_Overlap),(Channel="Visibility",Response=ECR_Overlap),(Channel="WorldDynamic",Response=ECR_Overlap),(Channel="Camera",Response=ECR_Overlap),(Channel="PhysicsBody",Response=ECR_Overlap),(Channel="Vehicle",Response=ECR_Overlap),(Channel="Destructible",Response=ECR_Overlap)),HelpMessage="WorldStatic object that overlaps all actors by default. All new custom channels will use its own default response. ")
+Profiles=(Name="BlockAllDynamic",CollisionEnabled=QueryAndPhysics,bCanModify=False,ObjectTypeName="WorldDynamic",CustomResponses=,HelpMessage="WorldDynamic object that blocks all actors by default. All new custom channels will use its own default response. ")
+Profiles=(Name="OverlapAllDynamic",CollisionEnabled=QueryOnly,bCanModify=False,ObjectTypeName="WorldDynamic",CustomResponses=((Channel="WorldStatic",Response=ECR_Overlap),(Channel="Pawn",Response=ECR_Overlap),(Channel="Visibility",Response=ECR_Overlap),(Channel="WorldDynamic",Response=ECR_Overlap),(Channel="Camera",Response=ECR_Overlap),(Channel="PhysicsBody",Response=ECR_Overlap),(Channel="Vehicle",Response=ECR_Overlap),(Channel="Destructible",Response=ECR_Overlap)),HelpMessage="WorldDynamic object that overlaps all actors by default. All new custom channels will use its own default response. ")
+Profiles=(Name="IgnoreOnlyPawn",CollisionEnabled=QueryOnly,bCanModify=False,ObjectTypeName="WorldDynamic",CustomResponses=((Channel="Pawn",Response=ECR_Ignore),(Channel="Vehicle",Response=ECR_Ignore)),HelpMessage="WorldDynamic object that ignores Pawn and Vehicle. All other channels will be set to default.")
+Profiles=(Name="OverlapOnlyPawn",CollisionEnabled=QueryOnly,bCanModify=False,ObjectTypeName="WorldDynamic",CustomResponses=((Channel="Pawn",Response=ECR_Overlap),(Channel="Vehicle",Response=ECR_Overlap),(Channel="Camera",Response=ECR_Ignore)),HelpMessage="WorldDynamic object that overlaps Pawn, Camera, and Vehicle. All other channels will be set to default. ")
+Profiles=(Name="Pawn",CollisionEnabled=QueryAndPhysics,bCanModify=False,ObjectTypeName="Pawn",CustomResponses=((Channel="Visibility",Response=ECR_Ignore)),HelpMessage="Pawn object. Can be used for capsule of any playerable character or AI. ")
+Profiles=(Name="Spectator",CollisionEnabled=QueryOnly,bCanModify=False,ObjectTypeName="Pawn",CustomResponses=((Channel="WorldStatic"),(Channel="Pawn",Response=ECR_Ignore),(Channel="Visibility",Response=ECR_Ignore),(Channel="WorldDynamic",Response=ECR_Ignore),(Channel="Camera",Response=ECR_Ignore),(Channel="PhysicsBody",Response=ECR_Ignore),(Channel="Vehicle",Response=ECR_Ignore),(Channel="Destructible",Response=ECR_Ignore)),HelpMessage="Pawn object that ignores all other actors except WorldStatic.")
+Profiles=(Name="CharacterMesh",CollisionEnabled=QueryOnly,bCanModify=False,ObjectTypeName="Pawn",CustomResponses=((Channel="Pawn",Response=ECR_Ignore),(Channel="Vehicle",Response=ECR_Ignore),(Channel="Visibility",Response=ECR_Ignore)),HelpMessage="Pawn object that is used for Character Mesh. All other channels will be set to default.")
+Profiles=(Name="PhysicsActor",CollisionEnabled=QueryAndPhysics,bCanModify=False,ObjectTypeName="PhysicsBody",CustomResponses=,HelpMessage="Simulating actors")
+Profiles=(Name="Destructible",CollisionEnabled=QueryAndPhysics,bCanModify=False,ObjectTypeName="Destructible",CustomResponses=,HelpMessage="Destructible actors")
+Profiles=(Name="InvisibleWall",CollisionEnabled=QueryAndPhysics,bCanModify=False,ObjectTypeName="WorldStatic",CustomResponses=((Channel="Visibility",Response=ECR_Ignore)),HelpMessage="WorldStatic object that is invisible.")
+Profiles=(Name="InvisibleWallDynamic",CollisionEnabled=QueryAndPhysics,bCanModify=False,ObjectTypeName="WorldDynamic",CustomResponses=((Channel="Visibility",Response=ECR_Ignore)),HelpMessage="WorldDynamic object that is invisible.")
+Profiles=(Name="Trigger",CollisionEnabled=QueryOnly,bCanModify=False,ObjectTypeName="WorldDynamic",CustomResponses=((Channel="WorldStatic",Response=ECR_Overlap),(Channel="Pawn",Response=ECR_Overlap),(Channel="Visibility",Response=ECR_Ignore),(Channel="WorldDynamic",Response=ECR_Overlap),(Channel="Camera",Response=ECR_Overlap),(Channel="PhysicsBody",Response=ECR_Overlap),(Channel="Vehicle",Response=ECR_Overlap),(Channel="Destructible",Response=ECR_Overlap)),HelpMessage="WorldDynamic object that is used for trigger. All other channels will be set to default.")
+Profiles=(Name="Ragdoll",CollisionEnabled=QueryAndPhysics,bCanModify=False,ObjectTypeName="PhysicsBody",CustomResponses=((Channel="Pawn",Response=ECR_Ignore),(Channel="Visibility",Response=ECR_Ignore)),HelpMessage="Simulating Skeletal Mesh Component. All other channels will be set to default.")
+Profiles=(Name="Vehicle",CollisionEnabled=QueryAndPhysics,bCanModify=False,ObjectTypeName="Vehicle",CustomResponses=,HelpMessage="Vehicle object that blocks Vehicle, WorldStatic, and WorldDynamic. All other channels will be set to default.")
+Profiles=(Name="UI",CollisionEnabled=QueryOnly,bCanModify=False,ObjectTypeName="WorldDynamic",CustomResponses=((Channel="WorldStatic",Response=ECR_Overlap),(Channel="Pawn",Response=ECR_Overlap),(Channel="Visibility"),(Channel="WorldDynamic",Response=ECR_Overlap),(Channel="Camera",Response=ECR_Overlap),(Channel="PhysicsBody",Response=ECR_Overlap),(Channel="Vehicle",Response=ECR_Overlap),(Channel="Destructible",Response=ECR_Overlap)),HelpMessage="WorldStatic object that overlaps all actors by default. All new custom channels will use its own default response. ")
+Profiles=(Name="WaterBodyCollision",CollisionEnabled=QueryOnly,bCanModify=False,ObjectTypeName="WorldStatic",CustomResponses=((Channel="WorldDynamic",Response=ECR_Overlap),(Channel="Pawn",Response=ECR_Overlap),(Channel="Visibility",Response=ECR_Ignore),(Channel="Camera",Response=ECR_Ignore),(Channel="PhysicsBody",Response=ECR_Overlap),(Channel="Vehicle",Response=ECR_Overlap),(Channel="Destructible",Response=ECR_Overlap)),HelpMessage="Default Water Collision Profile (Created by Water Plugin)")
-ProfileRedirects=(OldName="BlockingVolume",NewName="InvisibleWall")
-ProfileRedirects=(OldName="InterpActor",NewName="IgnoreOnlyPawn")
-ProfileRedirects=(OldName="StaticMeshComponent",NewName="BlockAllDynamic")
-ProfileRedirects=(OldName="SkeletalMeshActor",NewName="PhysicsActor")
-ProfileRedirects=(OldName="InvisibleActor",NewName="InvisibleWallDynamic")
+ProfileRedirects=(OldName="BlockingVolume",NewName="InvisibleWall")
+ProfileRedirects=(OldName="InterpActor",NewName="IgnoreOnlyPawn")
+ProfileRedirects=(OldName="StaticMeshComponent",NewName="BlockAllDynamic")
+ProfileRedirects=(OldName="SkeletalMeshActor",NewName="PhysicsActor")
+ProfileRedirects=(OldName="InvisibleActor",NewName="InvisibleWallDynamic")
-CollisionChannelRedirects=(OldName="Static",NewName="WorldStatic")
-CollisionChannelRedirects=(OldName="Dynamic",NewName="WorldDynamic")
-CollisionChannelRedirects=(OldName="VehicleMovement",NewName="Vehicle")
-CollisionChannelRedirects=(OldName="PawnMovement",NewName="Pawn")
+CollisionChannelRedirects=(OldName="Static",NewName="WorldStatic")
+CollisionChannelRedirects=(OldName="Dynamic",NewName="WorldDynamic")
+CollisionChannelRedirects=(OldName="VehicleMovement",NewName="Vehicle")
+CollisionChannelRedirects=(OldName="PawnMovement",NewName="Pawn")

View File

@@ -1,3 +1,102 @@
[/Script/EngineSettings.GeneralProjectSettings] [/Script/EngineSettings.GeneralProjectSettings]
ProjectID=FC3E256F43B2AFD43009F4949B0814BE ProjectID=FC3E256F43B2AFD43009F4949B0814BE
ProjectName=Third Person Game Template ProjectName=Third Person Game Template
; -----------------------------------------------------------------------------
; ZMMO Theme subsystem (see Source/ZMMO/Game/UI/ZeusThemeSubsystem.h and
; ARQUITETURA.md §1.10 / §4.7).
;
; - DefaultThemeAsset is the fallback theme (must define every EZeusThemeKey).
; - ThemeRegistry maps ThemeId -> seasonal DA_Theme_* asset.
; - CalendarTable points to DT_ThemeCalendar (FThemeCalendarRow rows).
; - UIStyleTable points to DT_UI_Styles (FUIStyleRow rows): tokens de estilo
; (cores/fontes/dimensões) keyed por ThemeId; linha "Default" é o fallback.
; - DevThemeOverride forces a theme in non-Shipping builds only. Keep commented
; in production.
;
; Paths use the /Game prefix (cooked Content namespace). Uncomment after the
; assets are actually created in the editor.
; -----------------------------------------------------------------------------
[/Script/ZMMO.ZeusThemeSubsystem]
;DefaultThemeAsset=/Game/ZMMO/UI/Themes/DA_Theme_Default.DA_Theme_Default
;+ThemeRegistry=(("Christmas", "/Game/ZMMO/UI/Themes/DA_Theme_Christmas.DA_Theme_Christmas"))
;+ThemeRegistry=(("Halloween", "/Game/ZMMO/UI/Themes/DA_Theme_Halloween.DA_Theme_Halloween"))
;CalendarTable=/Game/ZMMO/UI/Themes/DT_ThemeCalendar.DT_ThemeCalendar
UIStyleTable=/Game/ZMMO/Data/UI/DT_UI_Styles.DT_UI_Styles
;DevThemeOverride=Christmas
[/Script/ZMMO.UIFrontEndFlowSubsystem]
; Mapa estado->tela. O DA já existe (RootLayoutClass = WBP_PrimaryGameLayout);
; StateScreens fica vazio até os WBPs de página virem do Zeus UMG Forge (§4.8).
ScreenSetAsset=/Game/ZMMO/UI/FrontEnd/DA_FrontEndScreenSet.DA_FrontEndScreenSet
; Fase 4 — char spawn DB-driven. DT_Maps espelha o `maps_config.json` do
; server (Server/ZeusServerEngine/Config/DataTables/maps_config.json) — o
; cliente resolve `mapId -> ClientLevel/spawns` via FZeusMapDef rows.
MapsTableAsset=/Game/ZMMO/Data/World/DT_Maps.DT_Maps
; Loading dinâmico (etapas + dicas). DA_LoadingProfiles tem 1 perfil por
; contexto (EZeusLoadingContext); DT_LoadingTips rotaciona durante o load.
; Cliente local marca etapas via eventos (sem opcode novo).
LoadingProfilesAsset=/Game/ZMMO/Data/UI/Loading/DA_LoadingProfiles.DA_LoadingProfiles
LoadingTipsAsset=/Game/ZMMO/Data/UI/Loading/DT_LoadingTips.DT_LoadingTips
; -----------------------------------------------------------------------------
; UI in-game (PR 19+). Espelho do front-end pattern:
; - ScreenSetAsset mapeia EZeusInGameUIState -> WBP (Playing -> WBP_HUD,
; StatusWindow -> WBP_StatusWindow, etc.)
; - Subsystem orquestra: AZeusHUD::BeginPlay chama StartInGame que vai pra
; Playing.
; Asset criado via MCP em /Game/ZMMO/UI/InGame/DA_InGameScreenSet.
; -----------------------------------------------------------------------------
[/Script/ZMMO.UIInGameFlowSubsystem]
ScreenSetAsset=/Game/ZMMO/UI/InGame/DA_InGameScreenSet.DA_InGameScreenSet
; -----------------------------------------------------------------------------
; PlayerState Component Registry (PR 19+). Cada módulo MMO registra seus
; UActorComponent aqui. O AZeusPlayerState lê esta lista no construtor e
; instancia cada classe como subobject — pattern Open-Closed: adicionar
; Inventory/Skills/Guild = mais uma linha aqui, sem editar AZeusPlayerState.
;
; Convenção: usa o path /Script/<Module>.<ClassName> (sem o "U" do prefixo
; C++; UClass resolve pelo nome curto).
; -----------------------------------------------------------------------------
[/Script/ZMMO.ZeusPlayerState]
; UZeusGASComponent (plugin ZeusGAS) — dono unico dos atributos do char (M7).
; Owns UZeusAbilitySystemComponent + UZeusAttributeSet (26 atributos
; espelhados do server CharAttributeSet). Bridge via UZeusGASNetworkHandler
; (UWorldSubsystem) subscribe nos delegates do UZeusNetworkSubsystem.
+ComponentClasses=/Script/ZeusGAS.ZeusGASComponent
; === GAS Cue paths (Batch 2.5 — S_ABILITY_CUE multicast cosmetico) ===
; UGameplayCueManager scaneia esses paths no boot pra mapear FGameplayTag
; -> AGameplayCueNotify_Actor BP. UZeusGASComponent::DispatchAbilityCue chama
; ASC->ExecuteGameplayCueLocal(CueTag, params) -> manager resolve pelo path.
[/Script/GameplayAbilities.AbilitySystemGlobals]
+GameplayCueNotifyPaths="/Game/ZMMO/GAS/Cues"
[/Script/Engine.AssetManagerSettings]
-PrimaryAssetTypesToScan=(PrimaryAssetType="Map",AssetBaseClass=/Script/Engine.World,bHasBlueprintClasses=False,bIsEditorOnly=True,Directories=((Path="/Game/Maps")),SpecificAssets=,Rules=(Priority=-1,ChunkId=-1,bApplyRecursively=True,CookRule=Unknown))
-PrimaryAssetTypesToScan=(PrimaryAssetType="PrimaryAssetLabel",AssetBaseClass=/Script/Engine.PrimaryAssetLabel,bHasBlueprintClasses=False,bIsEditorOnly=True,Directories=((Path="/Game")),SpecificAssets=,Rules=(Priority=-1,ChunkId=-1,bApplyRecursively=True,CookRule=Unknown))
+PrimaryAssetTypesToScan=(PrimaryAssetType="Map",AssetBaseClass="/Script/Engine.World",bHasBlueprintClasses=False,bIsEditorOnly=True,Directories=((Path="/Game/Maps")),SpecificAssets=,Rules=(Priority=-1,ChunkId=-1,bApplyRecursively=True,CookRule=Unknown))
+PrimaryAssetTypesToScan=(PrimaryAssetType="PrimaryAssetLabel",AssetBaseClass="/Script/Engine.PrimaryAssetLabel",bHasBlueprintClasses=False,bIsEditorOnly=True,Directories=((Path="/Game")),SpecificAssets=,Rules=(Priority=-1,ChunkId=-1,bApplyRecursively=True,CookRule=Unknown))
+PrimaryAssetTypesToScan=(PrimaryAssetType="GameFeatureData",AssetBaseClass="/Script/GameFeatures.GameFeatureData",bHasBlueprintClasses=False,bIsEditorOnly=False,Directories=,SpecificAssets=,Rules=(Priority=-1,ChunkId=-1,bApplyRecursively=True,CookRule=AlwaysCook))
bOnlyCookProductionAssets=False
bShouldManagerDetermineTypeAndName=False
bShouldGuessTypeAndNameInEditor=True
bShouldAcquireMissingChunksOnLoad=False
bShouldWarnAboutInvalidAssets=True
MetaDataTagsForAssetRegistry=()
[/Script/GameplayAbilities.AbilitySystemGlobals]
bUseDebugTargetFromHud=true
[ConsoleVariables]
; mostra cooldowns, ativações, falhas
AbilitySystem.LogAbilityTriggers=1
; mostra effects aplicados/removidos com magnitude
AbilitySystem.DebugAbilityTags=1

19
Config/DefaultZeusV1.ini Normal file
View File

@@ -0,0 +1,19 @@
; DefaultZeusV1.ini
; Config inicial pra UZeusNetworkingClientSubsystem (P9-6/P9-7 plugin).
; Editar via UE Project Settings -> Plugins -> Zeus V1 OU editar este arquivo
; e relancar o editor.
;
; PORT 7777: ZeusGateway. NodeService popula routes via charId lookup quando
; cliente conecta (validado: handshake routed charId=7 -> SP_01 cell 0). Fluxo
; padrao de producao -- UE conecta no Gateway, Gateway resolve cell ownership
; via lease em Valkey + encaminha pro ZS correto.
;
; ALTERNATIVO (debug only -- bug aberto pendente investigacao):
; ServerPort=9001 conecta DIRETO em SP_01 mas hoje pacote UE nao chega no
; server (FUdpSocketBuilder + Windows multi-NIC loopback issue). Use 7777.
[/Script/ZeusNetwork.ZeusNetworkingClientSubsystem]
ServerHost=127.0.0.1
ServerPort=7777
bUseZeusNetworkingV1=True
bAutoConnectOnStart=True

View File

@@ -0,0 +1,114 @@
; Gerado por ZeusTool tags sync -- tags do ZeusServer p/ o cliente UE.
; NAO editar a mao: regenere com `ZeusTool tags sync <json> <ini>`.
[/Script/GameplayTags.GameplayTagsList]
GameplayTagList=(Tag="GameplayCue",DevComment="")
GameplayTagList=(Tag="GameplayCue.Zeus",DevComment="")
GameplayTagList=(Tag="GameplayCue.Zeus.Melee",DevComment="")
GameplayTagList=(Tag="GameplayCue.Zeus.Melee.Kick",DevComment="")
GameplayTagList=(Tag="GameplayCue.Zeus.Combat",DevComment="Categoria root de cues compartilhados por TODO melee (reutilizaveis)")
GameplayTagList=(Tag="GameplayCue.Zeus.Combat.Hit",DevComment="Familia de cues do impacto de hit")
GameplayTagList=(Tag="GameplayCue.Zeus.Combat.Hit.Impact",DevComment="Broadcast AOI - SFX impact + VFX sparks na location (todos ouvem)")
GameplayTagList=(Tag="GameplayCue.Zeus.Combat.Hit.React",DevComment="Broadcast AOI - AnimMontage de hit react no target (todos veem)")
GameplayTagList=(Tag="GameplayCue.Zeus.Combat.Hit.Self",DevComment="Unicast - feedback privado do alvo (camera shake, hit flash, rumble)")
GameplayTagList=(Tag="GameplayCue.Zeus.Melee.Kick.Target",DevComment="")
GameplayTagList=(Tag="GameplayCue.Zeus.Movement",DevComment="")
GameplayTagList=(Tag="GameplayCue.Zeus.Movement.Dash",DevComment="")
GameplayTagList=(Tag="Zeus",DevComment="")
GameplayTagList=(Tag="Zeus.Ability",DevComment="")
GameplayTagList=(Tag="Zeus.Ability.Casting",DevComment="")
GameplayTagList=(Tag="Zeus.Ability.Channeling",DevComment="")
GameplayTagList=(Tag="Zeus.Ability.Melee",DevComment="")
GameplayTagList=(Tag="Zeus.Ability.Melee.Kick",DevComment="")
GameplayTagList=(Tag="Zeus.Ability.Movement",DevComment="")
GameplayTagList=(Tag="Zeus.Ability.Movement.Dash",DevComment="")
GameplayTagList=(Tag="Zeus.Ability.Recovery",DevComment="")
GameplayTagList=(Tag="Zeus.Ability.Spell",DevComment="")
GameplayTagList=(Tag="Zeus.Ability.Spell.FireballRain",DevComment="")
GameplayTagList=(Tag="Zeus.Attribute",DevComment="")
GameplayTagList=(Tag="Zeus.Attribute.Agi",DevComment="")
GameplayTagList=(Tag="Zeus.Attribute.Aspd",DevComment="")
GameplayTagList=(Tag="Zeus.Attribute.Atk",DevComment="")
GameplayTagList=(Tag="Zeus.Attribute.BaseExp",DevComment="")
GameplayTagList=(Tag="Zeus.Attribute.BaseLevel",DevComment="")
GameplayTagList=(Tag="Zeus.Attribute.Crit",DevComment="")
GameplayTagList=(Tag="Zeus.Attribute.Def",DevComment="")
GameplayTagList=(Tag="Zeus.Attribute.Dex",DevComment="")
GameplayTagList=(Tag="Zeus.Attribute.Flee",DevComment="")
GameplayTagList=(Tag="Zeus.Attribute.Hit",DevComment="")
GameplayTagList=(Tag="Zeus.Attribute.Hp",DevComment="")
GameplayTagList=(Tag="Zeus.Attribute.HpRegenRate",DevComment="")
GameplayTagList=(Tag="Zeus.Attribute.Int",DevComment="")
GameplayTagList=(Tag="Zeus.Attribute.JobExp",DevComment="")
GameplayTagList=(Tag="Zeus.Attribute.JobLevel",DevComment="")
GameplayTagList=(Tag="Zeus.Attribute.Luk",DevComment="")
GameplayTagList=(Tag="Zeus.Attribute.Matk",DevComment="")
GameplayTagList=(Tag="Zeus.Attribute.MatkMax",DevComment="")
GameplayTagList=(Tag="Zeus.Attribute.MatkMin",DevComment="")
GameplayTagList=(Tag="Zeus.Attribute.MaxHp",DevComment="")
GameplayTagList=(Tag="Zeus.Attribute.MaxSp",DevComment="")
GameplayTagList=(Tag="Zeus.Attribute.Mdef",DevComment="")
GameplayTagList=(Tag="Zeus.Attribute.SkillPoint",DevComment="")
GameplayTagList=(Tag="Zeus.Attribute.Sp",DevComment="")
GameplayTagList=(Tag="Zeus.Attribute.SpRegenRate",DevComment="")
GameplayTagList=(Tag="Zeus.Attribute.Stamina",DevComment="")
GameplayTagList=(Tag="Zeus.Attribute.StaminaRegenRate",DevComment="")
GameplayTagList=(Tag="Zeus.Attribute.StatusPoint",DevComment="")
GameplayTagList=(Tag="Zeus.Attribute.Str",DevComment="")
GameplayTagList=(Tag="Zeus.Attribute.Vit",DevComment="")
GameplayTagList=(Tag="Zeus.Combat",DevComment="")
GameplayTagList=(Tag="Zeus.Combat.Blocking",DevComment="")
GameplayTagList=(Tag="Zeus.Combat.Dodging",DevComment="")
GameplayTagList=(Tag="Zeus.Combat.InCombat",DevComment="")
GameplayTagList=(Tag="Zeus.Combat.LockOn",DevComment="")
GameplayTagList=(Tag="Zeus.Combo",DevComment="")
GameplayTagList=(Tag="Zeus.Combo.Slot1",DevComment="")
GameplayTagList=(Tag="Zeus.Combo.Slot2",DevComment="")
GameplayTagList=(Tag="Zeus.Combo.Slot3",DevComment="")
GameplayTagList=(Tag="Zeus.Combo.Window",DevComment="")
GameplayTagList=(Tag="Zeus.Cooldown",DevComment="")
GameplayTagList=(Tag="Zeus.Cooldown.FireballRain",DevComment="")
GameplayTagList=(Tag="Zeus.Damage",DevComment="")
GameplayTagList=(Tag="Zeus.Damage.Earth",DevComment="")
GameplayTagList=(Tag="Zeus.Damage.Fire",DevComment="")
GameplayTagList=(Tag="Zeus.Damage.Holy",DevComment="")
GameplayTagList=(Tag="Zeus.Damage.Magical",DevComment="")
GameplayTagList=(Tag="Zeus.Damage.Physical",DevComment="")
GameplayTagList=(Tag="Zeus.Damage.Poison",DevComment="")
GameplayTagList=(Tag="Zeus.Damage.Shadow",DevComment="")
GameplayTagList=(Tag="Zeus.Damage.Water",DevComment="")
GameplayTagList=(Tag="Zeus.Damage.Wind",DevComment="")
GameplayTagList=(Tag="Zeus.Effect",DevComment="")
GameplayTagList=(Tag="Zeus.Effect.Buff",DevComment="")
GameplayTagList=(Tag="Zeus.Effect.Buff.StrUp",DevComment="")
GameplayTagList=(Tag="Zeus.Effect.Cooldown",DevComment="")
GameplayTagList=(Tag="Zeus.Effect.Cooldown.Dash",DevComment="")
GameplayTagList=(Tag="Zeus.Effect.Cooldown.FireballRain",DevComment="")
GameplayTagList=(Tag="Zeus.Effect.Cooldown.Kick",DevComment="")
GameplayTagList=(Tag="Zeus.Effect.Cost",DevComment="")
GameplayTagList=(Tag="Zeus.Effect.Cost.Sp_10",DevComment="")
GameplayTagList=(Tag="Zeus.Effect.Cost.Sp_50",DevComment="")
GameplayTagList=(Tag="Zeus.Effect.Cost.Stamina_5",DevComment="")
GameplayTagList=(Tag="Zeus.Effect.Damage",DevComment="")
GameplayTagList=(Tag="Zeus.Effect.Damage.Kick",DevComment="")
GameplayTagList=(Tag="Zeus.State",DevComment="")
GameplayTagList=(Tag="Zeus.State.NoHpRegen",DevComment="")
GameplayTagList=(Tag="Zeus.State.NoRegen",DevComment="")
GameplayTagList=(Tag="Zeus.State.NoSpRegen",DevComment="")
GameplayTagList=(Tag="Zeus.State.NoStaminaRegen",DevComment="")
GameplayTagList=(Tag="Zeus.Status",DevComment="")
GameplayTagList=(Tag="Zeus.Status.Buff",DevComment="")
GameplayTagList=(Tag="Zeus.Status.CC",DevComment="")
GameplayTagList=(Tag="Zeus.Status.CC.Disarmed",DevComment="")
GameplayTagList=(Tag="Zeus.Status.CC.Feared",DevComment="")
GameplayTagList=(Tag="Zeus.Status.CC.Frozen",DevComment="")
GameplayTagList=(Tag="Zeus.Status.CC.KnockedDown",DevComment="")
GameplayTagList=(Tag="Zeus.Status.CC.Rooted",DevComment="")
GameplayTagList=(Tag="Zeus.Status.CC.Silenced",DevComment="")
GameplayTagList=(Tag="Zeus.Status.CC.Stunned",DevComment="")
GameplayTagList=(Tag="Zeus.Status.Dead",DevComment="")
GameplayTagList=(Tag="Zeus.Status.Debuff",DevComment="")
GameplayTagList=(Tag="Zeus.Status.Invulnerable",DevComment="")
GameplayTagList=(Tag="Zeus.Status.Regen",DevComment="")
GameplayTagList=(Tag="Zeus.Status.Regen.Blocked",DevComment="")
GameplayTagList=(Tag="Zeus.Status.Regen.Boosted",DevComment="")
GameplayTagList=(Tag="Zeus.Status.Regen.Poisoned",DevComment="")

BIN
Content/BP_TestActor.uasset Normal file

Binary file not shown.

Binary file not shown.

View File

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Some files were not shown because too many files have changed in this diff Show More