> For the complete documentation index, see [llms.txt](https://claude.fractalgamestudios.es/llms.txt). Markdown versions of documentation pages are available by appending `.md` to page URLs; this page is available as [Markdown](https://claude.fractalgamestudios.es/bolas/game-info/pulido-visual-y-ui-parte-1.md).

# Pulido visual y UI (parte 1)

### 2026-08-02 (pulido visual: iconos aplastados en WeaponsHud, MoneyDisplay con borde negro y una línea gris fea, botones del menú todos del mismo azul plano sin relación con su función, y Theme.lua ampliado/documentado como archivo central de diseño)

Nuevo `/goal`, pedido explícito de crear un archivo central donde tocar colores/fuente/etc. de toda la interfaz, más 3 bugs visuales concretos.

#### 0. `Theme.lua` ya existía -- ampliado y documentado como EL archivo a tocar

`ReplicatedStorage.Front.Shared.Theme` ya era, de hecho, la fuente unica de colores/fuentes/radios/bordes que consume TODA la interfaz React (se viene construyendo asi desde varias sesiones atras). El usuario no sabia que ya existia. En vez de crear un archivo duplicado que hubiera quedado desconectado de lo que de verdad se renderiza, se amplio y documento ESTE archivo con una cabecera explicita ("ARCHIVO DE DISEÑO DEL JUEGO -- toca AQUI...") y una guia rapida de que clave controla que, para que sea obvio que es el sitio correcto. Añadidas claves nuevas usadas por los fixes de abajo: `Theme.Colors.Neutral`/`NeutralDark` (gris de sistema, no existia ningun color "sin significado semantico" para acciones como Ajustes), `Theme.Colors.Money`/`MoneyDark` (dorado, para el HUD de dinero) y `Theme.Colors.MenuAccents` (mapa boton→variant por FUNCION, ver mas abajo).

#### 1. Iconos "aplastados por arriba" -- bug real confirmado en `WeaponsHud`, no en los botones del menu

Investigado con medidas de pixeles reales (`AbsoluteSize`), no solo revision visual: los iconos de los 4 botones del menu (`Buttons.Button03`) ya tenian su propio `UIAspectRatioConstraint` y salian perfectamente cuadrados (111.8×111.8 el boton, 100.6×100.6 el icono) -- no eran el origen del problema pese a que tambien se les acuso en el pedido.

El bug real estaba en `WeaponsHud` (construido la sesion anterior): el `ImageLabel` del icono de cada arma tenia `Size = UDim2.fromScale(0.4, 0.4)` calculado sobre su `Content` Frame, que **no es cuadrado** (las celdas de la rejilla 3x3 son mas anchas que altas) -- sin ningun `UIAspectRatioConstraint`, el icono cuadrado de origen se ESTIRABA (`ScaleType` por defecto de Roblox es `Stretch`) para rellenar ese hueco no cuadrado, aplastandolo verticalmente. Arreglado añadiendo el mismo `UIAspectRatioConstraint(AspectRatio=1)` que ya usa `Button03` para su propio icono. **Validado en playtest**: `Icon.AbsoluteSize` paso a ser exactamente cuadrado (38.6×38.6) dentro de una celda no cuadrada (137×107) -- confirmado por medicion directa, no solo por captura de pantalla.

**Nota honesta para el usuario:** con el bug de layout ya arreglado, si algun icono concreto TODAVIA se ve "plano" es porque asi esta compuesto el propio dibujo del asset (`rbxassetid`) que se eligio en una sesion anterior (recuperado del HUD nativo antiguo, `StarterGui.BallsHud`) -- eso ya no es un bug de codigo, haria falta buscar otro icono en el Creator Store para ese caso puntual. No se ha tocado ningun `rbxassetid` en este pase.

#### 2. Botones del menu "todos azules planos sin sentido"

Causa: `BigMenuButton` (en `Front.Modules.HUD.HUD`) llamaba a `Button03` con `variant = "primary"` **fijo para los 4 botones**, sin relacion con lo que hace cada uno -- Personalizar, Teleport, Ajustes y Arma salian todos con el mismo azul/cian. Cualquier variedad visual que se veia (ej. el boton "Arma" con fondo rojizo) era pura casualidad del color YA incluido dentro de la imagen del propio icono (el asset del icono de espada trae su propio fondo rojo dibujado), no un diseño deliberado del boton en si.

**Arreglo:** `BigMenuButton` acepta ahora un `variant` por instancia; cada uno de los 4 botones en `HUD.lua` pasa el suyo desde el nuevo `Theme.Colors.MenuAccents` (nombrado por FUNCION, no por color, para que sea obvio que tocar si se añade/quita un boton): `Customize="primary"` (cian, acento de marca), `Teleport="secondary"` (azul Info, sugiere viaje), `Settings="neutral"` (gris de sistema, nuevo variant `neutral` añadido a `Buttons.THEMES`), `Weapon="danger"` (rojo, combate -- ahora coincide de verdad, a proposito, con el icono de espada). Validado en playtest: captura confirma 4 botones visualmente distintos y con sentido (cian/azul/gris/rojo) en vez de 4 cuadrados iguales.

#### 3. MoneyDisplay: borde negro feo + linea gris sobre el dinero

Causa exacta de ambas quejas, confirmada leyendo el propio codigo: el fondo del "Top" card era `Theme.Colors.Surface` (casi negro, `RGB(18,18,26)`) con un `UIStroke` cian generico (mismo acento de marca que el resto de la interfaz, sin relacion con "dinero") -- eso es el "borde negro" reportado. La "linea gris" era un `Frame` de "Highlight" plano (blanco al 15% de opacidad, sin degradado, con bordes duros) posicionado atravesando la tarjeta justo por encima del numero -- al no tener ningun gradiente, se veia exactamente como una franja/linea recta en vez de un brillo.

**Arreglo:** acento cambiado de cian generico a `Theme.Colors.Money` (dorado, nuevo en el Theme -- tiene sentido semantico real para "dinero"), aplicado al borde y al color del texto del monto. La franja de "brillo" sustituida por un `UIGradient` de transparencia real (mas opaco arriba, se desvanece hacia abajo, sin bordes duros) -- ya no se lee como una linea. `Base` (la sombra/bisel inferior) paso de `PrimaryDark` (azul marino) a `MoneyDark` (marron oscuro dorado), coherente con el nuevo acento. Validado en playtest: captura confirma borde dorado limpio, sin ninguna franja/linea visible, "$3825" en dorado en vez de cian.

#### Validacion general (playtest completo, no solo revision de codigo)

Capturas de pantalla en el Lobby real confirman los 4 botones con colores distintos y con sentido, el HUD de dinero con el nuevo acento dorado sin la linea gris ni el borde negro, y el panel "ARMAS" con los iconos ya perfectamente cuadrados (medido en pixeles). Sin errores ni warnings nuevos en `get_runtime_logs` en ningun punto de la prueba.

**Archivos tocados:** `ReplicatedStorage.Front.Shared.Theme` (documentado como archivo central, `Neutral`/`NeutralDark`/`Money`/`MoneyDark`/`MenuAccents` nuevos), `ReplicatedStorage.Front.Shared.Components.Buttons` (nuevo variant `neutral`), `ReplicatedStorage.Front.Modules.HUD.HUD` (`BigMenuButton` acepta `variant`, los 4 botones usan `Theme.Colors.MenuAccents`, `MoneyDisplay` rediseñado), `ReplicatedStorage.Front.Modules.WeaponsHud.WeaponsHud` (`UIAspectRatioConstraint` en el icono).

***

### 2026-08-02 (el daño de Unarmed se reiniciaba a 1 en combate real, no solo en el HUD; reescritura del selector de arma nativo en React-Roblox: no cerraba y mostraba todas las armas como poseídas)

Nuevo `/goal`, dos pedidos sin relación entre sí: (1) durante la partida el daño de Unarmed en el HUD se reiniciaba a 1 repetidamente, con duda explícita del usuario sobre si también pasaba físicamente (sí); (2) reescribir en React-Roblox el HUD nativo de selección de arma (`StarterGui.BallsHud`, el que abre el botón "Arma" del menú) porque no se podía cerrar y mostraba las 9 armas como si todas estuvieran desbloqueadas.

#### 1. Daño de Unarmed reiniciándose a 1 — dos bugs reales combinados, confirmado que afectaba también al daño físico real

`ServerStorage.Classes.WeaponScaling.Unarmed.OnHit` calcula el daño a partir de la velocidad física ACTUAL de la bola en el instante del golpe (`ball.AssemblyLinearVelocity.Magnitude`, clamped al atributo `Speed`, que es el "nivel" acumulado +5/golpe) y, tras calcular el daño, incrementa `Speed` en +5 — el sistema de progresión "pega mas fuerte cuanto mas rapido va" ya documentado. Dos bugs distintos deshacían ese incremento casi inmediatamente:

1. **`Unarmed.lua` (`handleUnarmedFreeze`)**: al descongelar al atacante tras el golpe (0.3s), la velocidad de salida se fijaba SIEMPRE a `BASE_SPEED_UNIT` (45) — la constante base, no el nivel real ya alcanzado (`ball:GetAttribute("Speed")`, ya incrementado en ese punto). La bola volvia a moverse a velocidad base justo al descongelarse, así que el SIGUIENTE golpe volvía a calcular el daño desde velocidad base (\~1 de daño) sin importar cuanto nivel llevara acumulado.
2. **`WeaponSystem.lua` (`applyRepulsion`)**, bug mayor y mas directo: tras CUALQUIER golpe (llamado 0.3s despues, al resolver el knockback), si el atacante tenia `WeaponType == "Unarmed"` habia una linea `attacker:SetAttribute("Speed", 45)` — **reseteaba el atributo `Speed` (el nivel acumulado) a la base en CADA golpe**, sin ninguna razon fisica real detras (el impulso de knockback ya se habia calculado antes con la velocidad real, esta linea solo tocaba el atributo persistido). Esto deshacia por completo el incremento que `Unarmed.OnHit` acababa de aplicar un instante antes.

Confirmado el alcance real (no solo visual): el atributo `Speed` tambien es el TOPE que usa el propio motor de movimiento de la bola (`BallFactory`, `targetSpeedCap = ball:GetAttribute("Speed")`, `CurrentEngineSpeed` rampa hacia el) — asi que el bug #2 tambien limitaba la velocidad FISICA real de persecucion de la bola, no solo el numero de daño mostrado. Respondiendo la duda explicita del usuario: si, afectaba de verdad al daño real aplicado (el mismo `finalDamage` que se resta de la vida del rival), no solo al HUD.

**Arreglo:** eliminada por completo la linea de reseteo en `applyRepulsion` (no tenia ninguna funcion fisica, solo estropeaba la progresion); `handleUnarmedFreeze` ahora sale del freeze a la velocidad YA nivelada (`attacker:GetAttribute("Speed")`) en vez de la base fija.

**Validado en playtest con datos reales del servidor** (no solo revision de codigo): equipado Unarmed real, iniciada una partida 1v1 local, muestreado `Speed`/`CurrentDamage` de la bola cada segundo durante 20s de combate real contra un bot. `Speed` crecio de forma sostenida y monotona durante toda la prueba (50→85→95→100→105→110→115→120→125→130→135→140→145→150), **sin resetearse nunca** a la base -- antes se habria quedado fijo cerca de 45-50 para siempre. `CurrentDamage` alcanzo picos reales de hasta 10.3 reflejando ese nivel (los valores puntuales en 1 que siguen apareciendo son legitimos: la fisica real de la bola justo al salir de un freeze/knockback puede estar momentaneamente mas lenta que su tope, antes de que `BallFactory` la rampee de vuelta -- ya no es el bug, es variacion fisica normal). Sin errores en `get_runtime_logs`.

**Archivos tocados:** `ServerStorage.Classes.WeaponScaling.Unarmed` (`handleUnarmedFreeze`), `ServerStorage.Classes.BallSystem.WeaponSystem` (`applyRepulsion`).

#### 2. Selector de arma nativo → reescrito en React-Roblox

Diagnostico del sistema viejo (`StarterGui.BallsHud`, un `ScreenGui` a mano con 9 `ImageButton`, cada uno con su propio `LocalScript`): cada boton disparaba el remoto legado `"ChangeWeapon"` con el nombre del arma **sin comprobar nunca si el jugador la poseia de verdad** -- el servidor si validaba (`PlayerManager.equipBall` ya rechazaba armas no desbloqueadas, `if not data.Balls[ballId] then return false, "Player does not own this ball" end`), pero el resultado se descartaba sin avisar al cliente, y la UI no mostraba ningun candado/diferencia visual -- de ahi "sale que tengo todas las armas". El `CloseButton` tenia un handler conectado (`HudStarter.lua`) pero en la practica no cerraba nada util para el usuario.

**Reescritura completa** en `Front.Modules.WeaponsHud.WeaponsHud` (nuevo), registrada como ruta `overlay` en el Router (`hasBackdrop=true`, `dismissible=true` -- clic fuera cierra solo, sin logica propia) igual que `BallsConfig`/`BallInventory`:

* Datos reales via `Front.Shared.Hooks.useBalls` (la MISMA fuente que ya usa la customizacion) -- ya trae las 9 armas de `WeaponData.Weapons` con su `isOwned`/`isEquipped` real, incluidas las no poseidas.
* Cada tarjeta: icono (mismos `rbxassetid` recuperados del `ImageButton` nativo antes de deshabilitarlo) + nombre + estado. Poseida+equipada = borde resaltado + "EQUIPADA". Poseida = normal, clicable, equipa via `Balls/EquipBall` (remoto moderno, valida en servidor y reenvia snapshot fresco -- no el legado `ChangeWeapon`). **No poseida = icono oscurecido + overlay semitransparente + "🔒 BLOQUEADA", no interactiva** -- el candado real pedido.
* Cierre real: mismo patron que `BallsConfig.handleClose` (`router.closeView("WeaponsHud")`).
* Boton "Arma" del menu (`Front.Modules.HUD.HUD`) ahora abre esta ruta (`openView("WeaponsHud")`) en vez de `ballsHud.Enabled = true`.
* `StarterGui.BallsHud` (ScreenGui + 9 LocalScripts) y `StarterPlayer.StarterPlayerScripts.HudStarter` deshabilitados (`Enabled = false`), no borrados -- misma convencion ya establecida en el proyecto.

**Bug encontrado y arreglado DURANTE la propia construccion, antes de darlo por bueno** (confirmado con playtest, no solo revision): el overlay de "candado" (`LockOverlay`, tamaño 100% de la tarjeta) estaba como hermano de el icono/nombre/estado dentro del MISMO `UIListLayout` -- el layout lo trataba como un elemento normal de la lista, y al pedir el 100% del alto para si solo, empujaba el resto del contenido fuera de la tarjeta (etiquetas "BLOQUEADA" apareciendo cortadas o fuera de su tarjeta, solapando la fila de abajo). Arreglado moviendo icono/nombre/estado a su propio Frame interno con su propio `UIListLayout`, dejando `LockOverlay` como hermano de ESE frame (fuera del alcance del layout), pintado encima solo via `ZIndex`.

**Validado en playtest completo, con datos reales del perfil** (jugador con Sword equipada+desbloqueada, Unarmed desbloqueada sin equipar, y las 7 armas restantes sin desbloquear):

* Captura confirma la rejilla 3x3 correcta: "Sin arma"/"Espada" normales (Espada con borde verde + "EQUIPADA"), las 7 restantes con icono oscurecido + "🔒 BLOQUEADA", sin solapes.
* Click real (`simulate_mouse_input`) en "Sin arma" (poseida, no equipada) → `BallsController.getEquippedBall().id` cambio a `"Unarmed"` en el servidor real, UI se actualizo sola (Sin arma paso a verde+EQUIPADA, Espada volvio a normal).
* Click real en "Daga" (bloqueada) → sin cambio en el arma equipada (segui en `Unarmed`) -- confirmado que las tarjetas bloqueadas son realmente no interactivas.
* Click real en el boton "X" → el panel se cerro por completo, HUD normal de vuelta -- el cierre que antes no funcionaba.
* Arma equipada del jugador restaurada a `Sword` (su valor original) al terminar la prueba, para no dejar cambios de prueba en el perfil real (Studio usa el mismo DataStore que produccion, ya documentado en sesiones anteriores).
* Sin errores en `get_runtime_logs` en ningun punto.

**Archivos nuevos:** `ReplicatedStorage.Front.Modules.WeaponsHud.WeaponsHud`. **Archivos tocados:** `ReplicatedStorage.Front.Router.routes` (ruta `WeaponsHud`), `ReplicatedStorage.Front.Router.RouterView` (registro del componente), `ReplicatedStorage.Front.Modules.HUD.HUD` (boton "Arma" abre la ruta nueva, quitado el `require(Players)` que quedo sin uso). **Archivos deshabilitados (no borrados):** `StarterGui.BallsHud` (ScreenGui + 9 `LocalScript`), `StarterPlayer.StarterPlayerScripts.HudStarter`.

***

### 2026-08-02 (CombatHUD: la vida no se veía y las estadísticas de armas con 2 stats se solapaban por completo)

Nuevo `/goal` (confirmado que la cámara de la sesión anterior ya funciona bien): en el HUD de combate no se veía el texto de vida ("N/100") ni para rivales ni para uno mismo, y las estadísticas de "daño por golpe" se solapaban -- sospecha correcta del usuario: pasaba cuando una bola/arma tiene 2 estadísticas "levelables" (ej. Unarmed: Velocidad + Daño por golpe; Dagger: Velocidad de giro + Vueltas; Spear: Daño por golpe + Alcance).

#### 1. Estadísticas solapadas -- causa raíz confirmada

`Front.Modules.CombatHUD.CombatHUD`, `StatsList` (el `Frame` que contiene las filas `Stat1`, `Stat2`... generadas desde `WEAPON_STAT_PROVIDERS`) **no tenía ningún `UIListLayout`**. Cada `StatRow` traía su `LayoutOrder` puesto, pero sin un layout que lo consuma esa propiedad no hace nada -- todas las filas se quedaban en su `Position` por defecto `(0,0)`, exactamente superpuestas. Con 1 sola stat (ej. Sword) nunca se notaba (nada con qué solaparse); con 2 (Unarmed, Dagger, Spear) las dos filas quedaban una encima de la otra. Arreglado añadiendo el `UIListLayout` que faltaba (`SortOrder = LayoutOrder`, `Padding = 2px`).

#### 2. Vida invisible -- causa raíz distinta, mismo patrón de bug ya visto antes en este proyecto

Dentro de `HealthRow`, el `Frame` `Fill` (barra de color) y el `TextLabel` `Text` (el "N/100") no tenían `ZIndex` explícito. React-lua monta los hijos de un elemento iterando la tabla Lua de props con `pairs()` -- como las claves son texto (`"Fill"`, `"Text"`, `"UICorner"`), no un array, **el orden de esa iteración no respeta el orden en que están escritos en el código** (solo la parte array de una tabla Luau preserva orden, no la parte hash). Sin `ZIndex` para desempatar, el orden de apilado real dependía de ese orden de montaje no garantizado -- `Fill` podía acabar pintándose ENCIMA del texto, tapándolo casi por completo salvo con vida muy baja. Arreglado con `ZIndex = 1` en `Fill` y `ZIndex = 2` en `Text`, mismo patrón que ya usa `Front.Modules.HUD.HUD.MoneyDisplay` para evitar este problema (icono/monto con `ZIndex` explícito).

#### Validación en playtest (partida 1v1 local real, no solo revisión de código)

Unido a una partida 1v1 local real (jugador `ByBlackDead` con Sword vs bot `NoobSlayer677` con Unarmed -- el arma con 2 stats, elegida a propósito para forzar el caso del bug). Captura de pantalla confirma:

* Vida visible con claridad en ambas tarjetas: "71 / 100" (propia) y "64 / 100" (rival), texto blanco perfectamente legible sobre la barra verde.
* La tarjeta del rival (Unarmed) muestra "Velocidad: 100" y "Daño por golpe: 5.1" en dos líneas SEPARADAS, sin solaparse -- confirma el fix del `UIListLayout`.
* La tarjeta propia (Sword, 1 sola stat) sigue mostrando "Daño por golpe: 9.0" correctamente, sin regresión.
* Sin errores nuevos en `get_runtime_logs` durante toda la prueba.

**Archivo tocado:** `ReplicatedStorage.Front.Modules.CombatHUD.CombatHUD` (`UIListLayout` en `StatsList`, `ZIndex` en `Fill`/`Text` de `HealthRow`).

***

### 2026-08-02 (revertido el teletransporte de "Personalizar" -- solo debe mover la camara; carrera real entre la camara de la terminal y la de la partida que dejaba la camara "pegada" al estadio para siempre)

Nuevo `/goal`: el botón "Personalizar" (que la sesión anterior cambió para teletransportar al jugador) NO debe teletransportar, solo mover la cámara y activar la bola de preview + el menú; si se entra a una partida con la personalización abierta, su cámara no se cerraba bien y "se bugea con la de la partida"; y al terminar la partida la cámara se quedaba enfocando el estadio en vez de volver a seguir al jugador.

#### 1. Teletransporte revertido

`Front.Modules.HUD.HUD` (`PersonalizarButton`) ya no mueve `HumanoidRootPart` — solo dispara `Frontend/OpenCustomizationAt` como antes de la sesión pasada, pero ahora sin el paso de teleport.

Consecuencia directa: `CustomizationTerminalController.onTriggered` ya no puede fiarse del `startDistanceGuard` (mide la distancia FÍSICA real entre el personaje y el terminal — si el jugador no se ha movido, casi siempre está a más de `MAX_SESSION_DISTANCE`, así que cerraría la sesión en el primer `Heartbeat`). Nuevo parámetro `isRemote` en `onTriggered`, `true` cuando se invoca desde `Frontend/OpenCustomizationAt` (el menú) en vez de desde el `ProximityPrompt` físico (pulsar E de pie ahí) — en ese caso se omite el guard de distancia por completo.

#### 2 y 3. Carrera real entre `CameraController` (cámara de partida) y `CustomizationTerminalController` (cámara de la terminal) — causa raíz encontrada

Ambos escuchan el MISMO remoto `Arena/SetCamera` con conexiones independientes, sin ningún orden garantizado entre sí (dos `LocalScript`s distintos). Al empezar una partida con la terminal abierta:

* Si `CustomizationTerminalController` procesaba el evento DESPUÉS de `CameraController`, su `closeSession()` llamaba a `restoreCamera(previousCameraType, previousCameraCFrame)` — pisando la cámara de partida que `CameraController` acababa de fijar con la cámara "de antes de abrir la terminal". Resultado exacto del reporte: "se bugea con la de la partida".
* Peor aún, si esto ocurría la PRIMERA vez que el jugador entraba en combate en toda la sesión de juego (tras haber abierto antes la personalización), `CameraController.saveOriginalCamera()` (que solo se ejecutaba una vez, cacheando el `CameraType` vigente en ESE instante como "el original") podía capturar `Scriptable` en vez de `Custom` — dejando la cámara pegada al estadio en TODAS las partidas futuras de esa sesión, incluso después de terminarlas. Esto explica "al terminar la partida la cámara se queda enfocando el estadio".

**Arreglo (elimina la carrera de raíz, no solo un síntoma):**

* `CustomizationTerminalController.closeSession(skipCameraRestore)`: nuevo parámetro; el listener de `Arena/SetCamera` (inicio de partida real) ahora llama a `closeSession(true)` — cierra la sesión (bola de preview, `BallsHud`, visibilidad del personaje) pero NUNCA toca `camera.CameraType`/`CFrame`, dejando a `CameraController` como único dueño de esa transición, sin importar el orden de conexión.
* `CameraController.restoreCamera()`: ya no depende de un `originalCameraType` cacheado (variable y función `saveOriginalCamera` eliminadas) — vuelve siempre y de forma incondicional a `Enum.CameraType.Custom` (la cámara nativa que sigue al jugador), eliminando por construcción la posibilidad de que quede "envenenada" con `Scriptable`.

#### Validación en playtest (reproduciendo la condición de carrera de verdad, no solo revisión de código)

* Disparado `Frontend/OpenCustomizationAt` sobre el terminal real: posición del `HumanoidRootPart` del jugador **idéntica antes y después** (confirma que ya no hay teleport); `camera.CameraType` pasó a `Scriptable` mirando la bola; captura de pantalla confirma el panel `BallsConfig` abierto con la bola de preview centrada.
* Con la sesión de personalización TODAVÍA abierta, disparado el remoto real `Arena/SetCamera{enabled=true, focusPosition=(0,200,0)}` (el mismo que usa `ArenaManager.StartMatch`) desde el servidor: la cámara del cliente quedó exactamente en `(0,200,0)` con `CameraType=Scriptable` — NO fue pisada de vuelta a la cámara previa de la terminal. Confirmado también que la copia de la bola de preview en `Workspace.TempModels` fue destruida (sesión cerrada correctamente).
* Disparado `Arena/SetCamera{enabled=false}` (fin de partida): `camera.CameraType` volvió a `Custom`; captura de pantalla confirma la cámara siguiendo de nuevo al personaje en la Lobby, sin quedar enfocada a ningún punto fijo.
* Sin errores ni warnings nuevos en `get_runtime_logs` durante toda la prueba.

**Archivos tocados:** `ReplicatedStorage.Front.Modules.HUD.HUD` (quitado el teleport de `PersonalizarButton`), `StarterPlayer.StarterPlayerScripts.Controllers.CustomizationTerminalController` (`onTriggered`/`closeSession` con `isRemote`/`skipCameraRestore`), `StarterPlayer.StarterPlayerScripts.Controllers.CameraController` (`restoreCamera` sin cache de `originalCameraType`, siempre vuelve a `Custom`).

***

### 2026-08-02 (menú solo icono, botón de arma activa recuperado, "Personalizar" ahora teletransporta al terminal real, y se cierran los menús ajenos a la partida al empezar)

Cuatro pulidos más sobre el menú de 4 botones de la sesión anterior:

#### 1. Botones solo icono (sin texto)

`BigMenuButton` ya no dibuja ninguna etiqueta de texto — solo el icono de `Button03`, que ya se consideró suficientemente descriptivo. Necesarios dos iconos nuevos (Teleport, Ajustes) buscados en el Creator Store; el de "Armas" reutiliza el mismo `rbxassetid` que ya usa el botón "Sword" del HUD nativo de armas (`StarterGui.BallsHud`), para no subir nada nuevo.

**Gotcha real encontrado al probarlos**: el ID del propio `Decal` casi nunca sirve directo en un `ImageLabel.Image` — hace falta su `textureId` (un ID DISTINTO, obtenido con `get_asset_details`). Los dos iconos nuevos salían como un cuadrado en blanco liso hasta corregir esto; confirmado visualmente en playtest tras el cambio, los 4 botones muestran su icono correctamente.

#### 2. Botón de arma activa recuperado

La personalización (`BallsConfig`) solo cambia el ASPECTO de cada bola, nunca cuál está EQUIPADA — eso lo sigue haciendo el HUD nativo `StarterGui.BallsHud` (ya construido de antes, con las armas bloqueadas que aún no se tienen bien reflejado). Sustituye a "Más" (que abría `SubMenu`, una vista placeholder sin construir todavía) como 4º botón — se mantienen los 4 botones grandes pedidos en la sesión anterior, sin añadir un 5º.

#### 3. "Personalizar" ahora teletransporta al terminal real

Antes abría el panel de React directamente desde donde estuviera el jugador — sin bola de preview real detrás (esa depende de un `ProximityPrompt` físico en `Workspace.Lobby.Interactables.Customization.ZonaCustomizacion`). Ahora el botón teletransporta al jugador delante de ese terminal concreto y dispara un nuevo evento local, `Frontend/OpenCustomizationAt` (con la part del terminal como dato), que `CustomizationTerminalController` escucha para llamar a su misma función interna `onTriggered(part)` — exactamente el mismo camino que si el jugador hubiera caminado hasta ahí y pulsado E. Sin este teletransporte, además, el guardia de distancia del propio terminal (`startDistanceGuard`, 25 studs) habría cerrado la sesión solo un instante después de abrirla.

#### 4. Cierre de menús ajenos a la partida al empezar

Si un jugador en cola se pone a personalizar la bola (o tiene cualquier otro menú abierto) y la cola se resuelve mientras tanto, ahora se cierra todo lo que no sea parte de la partida: `RemoteRouteBridge` escucha el mismo remote `Arena/SetCamera` que ya usa `LobbyQueueController` para saber si hay partida activa, y al activarse llama a `router.closeAllOfType("main"/"overlay"/"modal")` (respeta las vistas `sticky` — HUD, CombatHUD, LobbyJoinPanel — que ya se ocultan solas por su cuenta). Por separado, `CustomizationTerminalController` también escucha el mismo remote para cerrar su PROPIA sesión (cámara bloqueada + bola de preview), que es un sistema aparte del router de React y no se cerraba solo con lo anterior.

#### Validación en playtest (con alguna caída de conexión del propio plugin de Studio esta sesión, no relacionada con el código)

* Captura de los 4 botones: los 4 iconos correctos, sin texto.
* Pulsado "Personalizar" (simulando la llamada real del botón): el jugador se teletransportó junto al terminal, la bola de preview salió centrada en pantalla con la espada correctamente orientada, el panel de React se abrió con normalidad.
* Con el panel de personalización abierto, simulado el inicio de una partida (`Arena/SetCamera enabled=true`) desde el servidor: el panel de React se cerró solo, el menú y el dinero desaparecieron, y confirmado en servidor que la copia de la bola de preview también se destruyó (limpieza completa, no solo visual).
* Sin errores nuevos en `get_runtime_logs` en ningún punto de la prueba.

**Archivos tocados:** `Front.Modules.HUD.HUD` (iconos, botón Armas, teletransporte de Personalizar), `Front.Router.RemoteRouteBridge` (cierre de vistas al empezar partida), `StarterPlayer.StarterPlayerScripts.Controllers.CustomizationTerminalController` (escucha `Frontend/OpenCustomizationAt` y `Arena/SetCamera` para auto-cerrar).

***

### 2026-08-02 (rediseño de la interfaz: HUD/menú/dinero se solapaban con el HUD de combate en partida, menú de 5 botones pequeños → 4 grandes, estilo visual unificado en base a un Theme real)

Pulido de la interfaz React-Roblox migrada en la sesión anterior: durante una partida el menú lateral izquierdo y el dinero se quedaban visibles solapando el HUD de combate; con muchos combatientes (3v3, BattleRoyale) todas las filas se apilaban en una sola columna a la derecha, saliendose de pantalla; el menú tenía un botón grande arriba (abría personalización, duplicado con uno de la rejilla) más una rejilla de 5 botones pequeños; y cada HUD (dinero, menú, combate, panel de unión) usaba colores literales distintos, sin unificar.

#### Descubrimiento de paso: `Theme.Colors` llevaba tiempo incompleta

`HUD.luau`/`MoneyDisplay.luau` ya referenciaban `Theme.Colors.PrimaryDark`/`.Surface`/`.Primary`/`.White` y `Shared.Components.Buttons` (el propio sistema de botones usado en TODA la interfaz) referenciaba `.Info`/`.Error`/`.TextPrimary` — ninguna de esas claves existía en `Theme.luau`. React-Lua ignora en silencio una prop en `nil` en vez de fallar, así que el bug nunca se notó (esos elementos simplemente perdían su color pretendido, degradando a lo que sea que Roblox use por defecto). Añadidas todas de verdad, con una paleta coherente basada en lo que el juego YA usaba de facto (el cian de los bordes del panel de unión, los fondos casi negros de las tarjetas oscuras, verde/amarillo/rojo para vida) — esto es lo que da "estilo propio" real: no colores nuevos inventados, sino los que ya definían la identidad visual del juego, ahora nombrados y reutilizables desde un solo sitio.

#### Descubrimiento de paso: `Button03` no renderiza sus children

Al intentar poner una etiqueta de texto dentro de cada botón grande del menú (igual que ya se intentaba, sin éxito, en el HUD anterior con "Tooltip"), resultó que `Shared.Components.Buttons.Button03` construye su propio árbol interno fijo y **ignora por completo** cualquier `children` que se le pase como tercer argumento — un bug preexistente, no algo nuevo de este cambio (los tooltips del menú de 5 botones nunca se vieron tampoco). Solucionado sin tocar `Buttons.lua` (compartido por más sitios): la etiqueta se dibuja como Frame **hermano** del botón, superpuesto encima (ZIndex alto), en vez de como hijo suyo.

#### Cambios

1. **HUD oculto durante partida**: `HUD.luau` ahora lee `inMatch` de `Front.Controllers.LobbyQueueController` (ya era la fuente de verdad de esto en cliente, vía `Arena/SetCamera` — reutilizado, no se inventó un listener nuevo) y no renderiza `LeftPanel` (menú) ni `RightPanel` (dinero) mientras dure.
2. **CombatHUD a dos columnas**: con más de 3 combatientes (3v3, BattleRoyale) se reparten en columna izquierda/derecha en vez de amontonarse en una sola lista a la derecha; con pocos (1v1) se queda en una sola columna como antes. El propio jugador (siempre primero en la lista) cae en la columna izquierda.
3. **Menú de 4 botones grandes**: eliminado el botón grande superior que abría personalización (`BallsConfig` ya era accesible desde "Personalizar" en la rejilla, no se pierde nada); rejilla 2x2 con celdas más grandes (0.47 en vez de 0.42); 5º botón (Arma, HUD nativo de selección de arma) desactivado por ahora, dejado comentado en el código para recuperarlo fácil si hace falta más adelante.
4. **Estilo unificado**: `CombatHUD` y `LobbyJoinPanel` (y de rebote todo lo que ya usaba `Shared.Components.Buttons`) pasan de colores `Color3.fromRGB(...)` sueltos a `Theme.Colors`/`Theme.CornerRadius`/`Theme.Stroke`, mismo lenguaje visual en las cuatro interfaces.

#### Validación en playtest (varias caídas de conexión del propio plugin de Studio esta sesión — desincronización de versión, ya solucionada tras un `Enable Studio Access` — no relacionadas con el código; una vez estable, todo funcionó a la primera)

* Partida real BattleRoyale con 4 bots: capturas de pantalla confirman `LeftPanel`/`RightPanel` completamente ausentes (ni menú ni dinero visibles) y `CombatHUD` repartido en dos columnas (2 combatientes a cada lado) sin salirse de pantalla.
* De vuelta en la Lobby: captura confirma los 4 botones grandes con sus etiquetas ("Personalizar", "Teleport", "Ajustes", "Más") correctamente visibles, sin 5º botón, y el panel de dinero con el nuevo borde cian consistente con el resto de la interfaz.
* Sin errores nuevos en `get_runtime_logs` en ningún punto.

**Archivos tocados:** `Front.Shared.Theme` (paleta completa + alias de claves que faltaban), `Front.Modules.HUD.HUD` (oculta Left/RightPanel en partida, menú de 4 botones, fix de etiquetas), `Front.Modules.CombatHUD.CombatHUD` (dos columnas + colores Theme), `Front.Modules.LobbyJoinPanel.LobbyJoinPanel` (colores Theme).


---

# Agent Instructions
This documentation is published with GitBook. GitBook is the documentation platform designed so that both humans and AI agents can read, navigate, and reason over technical content effectively. Learn more at gitbook.com.

## Querying This Documentation
If you need additional information that is not directly available in this page, you can query the documentation dynamically by asking a question.

Perform an HTTP GET request on the current page URL with the `ask` query parameter, and the optional `goal` query parameter:

```
GET https://claude.fractalgamestudios.es/bolas/game-info/pulido-visual-y-ui-parte-1.md?ask=<question>&goal=<endgoal>
```

`ask` is the immediate question: it should be specific, self-contained, and written in natural language.
`goal` is optional and describes the broader end goal you are ultimately trying to accomplish on behalf of the user. GitBook uses it to tailor the answer towards what is most useful for that goal.

The response will contain a direct answer to the question and relevant excerpts and sources from the documentation.

Use this mechanism when the answer is not explicitly present in the current page, you need clarification or additional context, or you want to retrieve related documentation sections.
