⛽ Ignitor Docs ← getignitor.com
🇺🇸 EN 🇪🇸 ES

Editor de GUIs#

El Editor de GUIs arma cada panel en pantalla que no es el cuarto en sí: la barra HUD siempre activa, las pantallas de Opciones y Guardar/Cargar, el popup de inventario, un verb-coin, o cualquier modal a medida que necesite tu proyecto (un teclado numérico, un teléfono, una lista de opciones de diálogo). Una GUI es un descriptor: datos puros que listan un id, un rect/backdrop opcional, y un arreglo ordenado de widgets. Aquí no hay UI hardcodeada: computeLayout() y hitTest() (core/guis/layout.js) convierten esos datos en geometría de pantalla y blancos de clic, ambas funciones puras sin acceso a canvas ni DOM; los píxeles reales los pinta drawGui() en shell/canvas_render.js. Guarda en el guis.json del proyecto (la barra HUD como mainBar/mainGui, todo lo demás bajo modals).

Llegas a él desde el Hub (con el dev server corriendo). El desplegable GUI de la barra de herramientas elige qué descriptor está cargado; el canvas del centro es un preview vivo y editable (no hay una pestaña de preview aparte, arrastras y redimensionas widgets directamente ahí) y el panel de la derecha muestra las propiedades de lo que esté seleccionado. Haz zoom en el canvas con los botones 🔍 / 🔍− de la barra o la rueda del mouse, ⊞ Fit encuadra toda la GUI en el área de preview, y puedes hacer pan para trabajar de cerca en un rincón cargado; el zoom es puramente visual, así que las coordenadas de los widgets nunca cambian al hacer zoom.

Al volver, el editor reabre la GUI que estabas editando en vez de la primera de la lista. Un enlace desde otro editor siempre le gana a esa memoria; un enlace a una GUI que ya no existe cae a esa memoria, y solo si tampoco existe, a la primera. Se lleva por proyecto.

La pantalla del Editor de GUI: un descriptor de panel con sus widgets distribuidos.
Editor de GUI: descriptor de panel con sus widgets.

Anatomía de un descriptor#

  • id: la clave de la GUI. La barra HUD se resuelve vía mainGuiId()/getMainGui() (core/guis/index.js): un descriptor puede optar explícitamente con role: 'main', o caer al id convencional mainBar/mainGui. Todo lo demás se abre con OPENGUI:<id> y se cierra con CLOSEGUI, condicionado por state.activeGui; mientras un modal está abierto, el input de sala deja de responder.
  • rect: los límites propios del panel, sobre todo para el backdrop/debug.
  • backdrop: un oscurecido opcional de pantalla completa pintado antes de los widgets (usado por los modales).
  • theme: opcional. 'ember' le da a la GUI el look ámbar de Ignitor: los botones de texto, las cajas de texto y los slots de guardado toman la paleta ámbar como color por defecto (un color que pongas en el widget sigue ganando). Sin él, queda el look clásico azul. Es el campo Estilo del GUI (Clásico (azul) / Ámbar Ignitor) del panel cuando no hay nada seleccionado. Las pantallas de Opciones y Guardar/Cargar del motor vienen en ámbar; un proyecto que ya personalizó una conserva su look hasta que lo cambies.
  • widgets: la lista ordenada descrita abajo. El orden importa tanto para pintar (los widgets posteriores se pintan encima) como para el hit-test (hitTest() recorre la lista al revés, así que gana el widget más "arriba" bajo el cursor).

Tipos de widget#

Cualquier widget puede llevar un conjunto compartido de propiedades sin importar el tipo: visibilidad (isVisible), estado habilitado (isEnabled), relleno (bgColor/bgOpacity), color/fuente/alineación de texto, y una lista ordenada onClickAction de tokens de efecto. Un widget sin fuente propia usa la fuente predeterminada del proyecto, igual en la vista previa del editor que en el juego. isLocked es contabilidad solo-editor: el runtime puro la ignora. Los tipos:

  • panel: un rect o círculo de color, normalmente colocado primero (detrás de todo) como fondo o como oscurecido de un modal. Puede llevar un sprite overlay y una animación enlazada opcional.
  • label: texto que sale de hasta cuatro fuentes, resueltas cuadro a cuadro por orden de prioridad: un mensaje condicional (condMsgs, una lista de filas {condición, mensaje}; gana la primera fila cuya condición se cumple, con el mismo picker de condición que las reglas de puzzle y hotspot), luego un valor enlazado (bind a una ruta de estado como party.count o settings.musicVol), luego un ticker auto-rotante (una lista de mensajes que ciclan solos contra el reloj FX, secs por mensaje, gapSecs en blanco entre ellos, y una entrada en blanco es un beat de pausa válido; sin reglas ni timers), y finalmente el text estático, que también hace de placeholder cuando nada de lo anterior aplica. El ticker y los mensajes condicionales se autoran monolingües como todo contenido, cada uno respaldado por su propio lid para el Editor de Traducción.
  • sentence: la línea-frase estilo SCUMM ("Usar llave con puerta…"). Su contenido no se autora aquí para nada; lo calcula cuadro a cuadro core/guis/sentence.js a partir del blanco bajo el mouse y el ítem armado. Su visibilidad sigue para qué sirve la línea: con un modal abierto (el inventario, por ejemplo) se mantiene legible encima del atenuado del modal, porque ahí es el feedback del ítem que estás por elegir; mientras corre un diálogo se esconde junto con los botones de verbo, porque la lista de opciones toma esa franja y la sentence sólo estaría mostrando lo que hacías antes de que arrancara la conversación.
  • button: el control de propósito general, especializado según qué ref lleve (ver abajo).
  • sprite: una imagen, con animaciones de cuadros inline opcionales (anims) para que un build final nunca necesite listar una carpeta para animarla. Un sprite nuevo arranca sin imagen y muestra un recuadro magenta hasta que eliges una.
  • inventory: una cuadrícula cols × rows que se expande en una celda hoja por slot al momento del layout (autoFit deriva el tamaño del slot desde el rect del widget); las celdas reflejan state.inventory en vivo y arman/desarman/combinan ítems al clic. Dibujo de la celda elige cómo se pinta un slot: iconos (por defecto) o nombres en texto, el look SCUMM v5 que usa la barra classic9legacy. Cambia sólo el dibujo: las mismas celdas, los mismos objetivos de clic, el mismo estado armado. En el pintor de iconos, Mostrar nombres bajo el icono enciende o apaga el rótulo, así puedes tener una cuadrícula sólo de iconos; viene encendido salvo que digas lo contrario. El toggle no se ofrece en el pintor de texto, donde el nombre es la celda.
  • list: filas de texto + onClickAction, usado para cosas como una lista de opciones de diálogo reutilizada como GUI.
  • textbox: un campo de texto engine-bound (bind, placeholder, maxLength); usado para el nombre de guardado en la pantalla de Guardar. El foco y la escritura los enruta shell/main.js (state.guiFocus), no el widget en sí.
  • slider: un control min/max/step engine-bound atado a una ruta de estado (usado en los sliders de volumen/brillo/contraste de la pantalla de Opciones); el manejo de arrastre vive en shell/main.js. Estilo elige el look: clásico, o Fader Ignitor (style: 'fader'), una perilla de consola de mezcla que corre sobre un riel hundido con relleno ámbar sólido. Un fader también puede llevar relleno desde el centro (bipolar), para valores cuyo punto neutro es el medio, como brillo o contraste; dos marquitas señalan el centro. Un slider nuevo nace sin bind: elige en el inspector qué mueve (mientras tanto se queda en su mínimo y arrastrarlo no cambia nada).
  • saveSlots: se expande en N celdas de slot de guardado al momento del layout; el contenido mostrado viene de persistence.listSlots() al momento de dibujar, no del descriptor. La vista previa muestra los slots vacíos, como los ve alguien que todavía no guarda. Selecciona la columna para fijar la cantidad de slots, su ancho y alto, la separación entre ellos, el tamaño de fuente y la fuente. No hay campo de color: los slots toman sus colores del estilo del GUI.

Tipos de botón#

El comportamiento de un widget button viene de qué campo de referencia lleve. El desplegable kind del editor cambia entre ellos y limpia los demás, así que solo aplica uno a la vez:

  • verbRef: resuelve label/sprite desde el registro VERBS; acción por defecto SELECTVERB:<id>. Así se arma la barra clásica de 9 verbos: nueve botones, cada uno con verbRef apuntando a un verbo.
  • charRef: resuelve name/sprite desde el registro CHARACTERS; acción por defecto SWITCHCHAR:<id>.
  • partyRef: un toggle del roster de grupo (agrega/quita un personaje del party activo); acción por defecto PARTYTOGGLE:<id>. Si el personaje que eliges no está en el pool del proyecto (Hub Config → Proyecto), el editor avisa ahí mismo: un botón que ofrece a alguien que el pool nunca retiene al arrancar significa que puede aparecer en pantalla antes de que nadie lo elija.
  • partySlotRef: un botón de slot de party committeado. No lleva ningún id de personaje fijo: en runtime se resuelve a chosenOrder(state)[N-1] (el N-ésimo personaje en el orden de selección actual) y dispara SWITCHPARTYSLOT:<N>. computeLayout() es puro y no puede resolver "quién está en el slot 2 ahora mismo", así que esa resolución pasa al momento de dibujar/clickear en el shell.
  • toggleRef: invierte un flag arbitrario del juego; acción por defecto TOGGLEFLAG:<flag>. El campo de flag es un input de texto libre con autocompletado (los flags son un namespace abierto que define el autor, así que no puede ser un picker cerrado), que lista cada flag ya usado en el proyecto más los flags reservados del motor: nombres que el motor mantiene solo, que ningún barrido del proyecto puede encontrar y que si no habría que saber de memoria.
  • scrollRef: paginado de inventario 'up'/'down'. Su acción está bloqueada (INVENTORY_UP/INVENTORY_DOWN): el editor no te deja sobrescribirla.
  • plain: sin ninguna ref: label libre más los tokens onClickAction que agregues con el widget compartido de tokens de efecto (el mismo que usan las reacciones de hotspot, las opciones de diálogo y las cinemáticas; cada argumento que porta un ID es un picker tipado, nunca texto libre).

Layout y posicionamiento#

Las coordenadas de un widget son píxeles lógicos absolutos a la resolución de canvas del proyecto (1920×1080 por defecto), no porcentajes; core/guis/constants.js guarda la geometría compartida (tope del panel, tamaño de celda de la cuadrícula de verbos, tamaño de slot de inventario…) que leen tanto las plantillas integradas como el renderer. Un proyecto en una resolución distinta a la por defecto la aplica una vez al boot con applyCanvasResolution(), que mueve esas constantes y reconstruye los cinco modales del motor (Opciones, Guardar/Cargar, confirmación de salida, inventario, menú de pausa) centrados sobre el canvas real; el editor los surface de la misma forma, así que materializar uno nunca escribe un widget fuera del borde de tu juego. La barra recibe el mismo trato por otro camino: cada barra que shippea el motor declara el canvas para el que fue dibujada (designResolution) y se escala al tuyo con un único factor uniforme, después se dockea abajo, así un solo descriptor classic9legacy autorado a 320×200 es también una barra de 1280 de ancho a 1280×1024. Un descriptor que autoraste tú queda exactamente como lo dibujaste, salvo que lleve su propio designResolution; ese campo es cómo una plantilla materializada sigue escalando después de volverse contenido tuyo. También sirve en tus modales, donde significa "re-céntrame en el canvas real" (y encógeme sólo si no entro): un diálogo centrado para un canvas de 1920 no es demasiado grande para uno de 1280, está en el lugar equivocado, y esa diferencia sólo la puede ver el canvas declarado. Todo lo demás lo reposicionas a mano aquí. computeLayout() toma un descriptor y devuelve una lista plana de nodos hoja (cada uno con un rect resuelto); los widgets contenedor como inventory y list se expanden en una hoja por celda/fila con un id compuesto (${parentId}.${index}), y saveSlots en una hoja por slot llamada saveSlot.${index} (el juego enruta el clic de un slot por su número, no por el id del widget). hitTest() recorre esa lista plana de atrás hacia adelante y devuelve el primer rect que contiene el punto de clic, saltando lo invisible o deshabilitado.

Los labels de botones y verbos se autoajustan a su celda de la misma manera: el fontSize de un widget es un literal en el renderer, así que a diferencia de la geometría de arriba no escala solo; un label sin escalar en un canvas chico puede imprimirse más ancho que la celda que lo contiene (la barra clásica de 9 verbos a 640×400 solía leerse "DARGARRUSAR"). fitFontSize() encoge un label hasta que entra en su caja en ambos ejes, y elipsiza en vez de encoger más allá de un piso legible; un label que ya entra queda en su tamaño autorado, así que un proyecto que ya se ve bien no cambia. Las celdas de verbo se ajustan como grupo, no cada una por su cuenta: la barra toma el tamaño más chico que entra en todas las celdas que comparten un tamaño base, así que nunca terminas con un verbo legible a 22px al lado de otro apretado a 15px; una celda a la que un autor le puso un fontSize deliberadamente distinto queda afuera de ese grupo. La línea de sentence (lo que el jugador está por hacer) recibe el mismo ajuste de caja, medido contra la altura de línea de la propia fuente en vez de la tinta real, así que su tamaño no cambia cada vez que cambia el largo de la descripción del hotspot bajo el mouse.

Conectar widgets a la lógica del juego#

El comportamiento de cualquier widget clickeable es un arreglo ordenado onClickAction con los mismos tokens de efecto que se usan en todo el motor (SETFLAG, GIVEITEM:<item>|<char>, SAY…), ejecutados por applyEffects(). El runtime de GUIs suma algunos propios: OPENGUI:<id> / CLOSEGUI abren y cierran un modal (condicionados por state.activeGui), SELECTVERB/SWITCHCHAR/PARTYTOGGLE/SWITCHPARTYSLOT/TOGGLEFLAG son las acciones por defecto que derivan automáticamente los tipos-ref de arriba, y SETLANG:es/SETLANG:en (usado por la pantalla de Opciones) cambian el idioma activo. El onClickAction propio de un widget, si está seteado, siempre gana sobre el default derivado de un tipo; el editor solo re-sincroniza el default cuando la acción actual sigue siendo ese default, así que un override genuino nunca se pisa.

Texto y localización#

El texto de un widget de GUI se autora monolingüe, en el defaultLocale del proyecto, la misma regla de content-i18n que en todos lados. Aquí no hay un objeto inline {es, en} que llenar (algunos de los descriptores integrados del motor, como save.js y options.js, todavía llevan mapas de idioma inline heredados de antes de esta migración; el editor los lee bien vía L(), pero cualquier edición desde el campo de texto del editor los reescribe como un string plano). Lo que pasa realmente al guardar:

  • El literal que escribes se guarda en el descriptor como un string plano (la fuente de verdad del defaultLocale; a esto cae un locale si no existe traducción).
  • El editor también hace upsert de un lid derivado de la dirección en el strings.json del proyecto: guiTextLid(guiId, widgetId) para el text principal de un widget, y lids por propiedad equivalentes para placeholders, tooltips y texto de fila de lista. Los labels de botón-verbo son un caso especial: overlayean a través del propio lid del verbo (verb_<id>_label), así que traducir un verbo una vez cubre toda barra o coin que lo referencie.
  • En runtime, _guiText() de shell/canvas_render.js resuelve primero por ese lid (cayendo al literal, y luego a L()), así que el texto dibujado nunca es el string crudo del descriptor una vez que existe una traducción.
  • Esos lids aparecen bajo la categoría GUI en el Editor de Traducción, donde se completan los pares reales de español/inglés (o cualquier otro locale configurado), nunca inline aquí.

Los sprites de widget de GUI siguen la misma idea de overlay para el arte: se prefiere una variante de imagen por-locale (<base>.<lang>.<ext>) sobre el PNG base cuando existe, para imágenes que hornean texto en los píxeles.

Estados de un botón#

Un botón puede verse distinto según qué esté haciendo el jugador con él: en reposo, con el cursor encima, apretado, o seleccionado (el verbo elegido, el personaje que estás manejando, un miembro del grupo, un toggle encendido). Hay dos cosas que puedes cambiar por estado, y se suman:

  • Arte: un PNG por estado. Completa solo los que necesites: un estado que dejes vacío cae a la imagen por defecto, así que un botón con nada más que su sprite base es perfectamente válido.
  • Color: un relleno de fondo y un color de texto por estado, en colores por estado dentro del panel de propiedades. Misma regla: vacío significa "usa el color base".

El botón auto al lado de un color de hover o de clicked deriva ese tono del color base (más claro para hover, más oscuro para apretado) y lo escribe en el campo, donde después lo puedes ajustar a mano. Necesita que el color base sea un valor hex (#rrggbb) para tener de dónde derivarlo.

El canvas del editor es para autorar, así que siempre dibuja los botones en reposo. El 👁 que está junto a cada estado dibuja el widget seleccionado en ese estado (arte y colores juntos) para que puedas revisar cómo queda un hover sin lanzar el juego. Clic de nuevo y vuelve a la normalidad.

El estado selected solo existe en los botones que realmente pueden estar seleccionados: verbo, personaje, grupo y toggle de flag, y además un botón plain con Se ve activo cuando puesto. Ese campo toma una ruta de estado y un valor (lang = es, saveGuiMode = load), y el botón se ve seleccionado mientras la ruta tenga ese valor: así la pantalla de Opciones marca el idioma actual. Un botón plain sin ruta nunca entra en él, así que esos campos no aparecen en uno.

Un widget de inventario toma los mismos colores, autorados una sola vez para toda la cuadrícula. Después cada celda elige su propio estado según por dónde ande el jugador: con el cursor encima es la celda que está debajo, apretada es la que está sosteniendo, y selected es la que tiene el ítem que levantó y todavía no usó. En el pintor de celda nombres de texto los colores son toda la historia, porque ese modo no tiene caja que teñir: el nombre mismo cambia de color en cada estado.

Animaciones#

Un widget (sprite, button o panel) puede enlazar una animación autorada en el Editor de Animaciones bajo su propia categoría de GUI (assets/guis/<guiId>/animations/<animName>/, registrada en animations.json). El cuadro actual de la animación enlazada sobrescribe el PNG estático del widget al dibujar; quitar el enlace cae de vuelta al sprite estático. El toggle Animate de la barra de herramientas reproduce en vivo cada animación enlazada en el canvas del editor para que puedas revisar el timing sin salir de la herramienta.

Flujo de trabajo#

  1. New GUI… y elige un id, un kind (modal / panel / coin), y una plantilla de partida, vacía o un clon de una integrada (classic9 la barra de 9 verbos, modern4, coinHud, o classic9legacy, los mismos nueve verbos sin nada de cromo: puro texto, dibujada para 320×200 y escalada desde ahí). Un kind panel se convierte en la barra HUD del proyecto; un kind modal recibe un backdrop para OPENGUI; un kind coin materializa el verb-coin radial como contenido editable.
  2. + Add widget… y arrástralo/redimensiónalo directamente en el canvas; Snap alinea los arrastres a una cuadrícula de 4px.
  3. Fija los campos específicos del tipo en el panel derecho: un verbRef/charRef/ partySlotRef/toggleRef para un botón, bind para un label/textbox/slider (el campo sugiere las rutas de estado que le quedan a ese widget: un slider ofrece los ajustes de volumen y calibración; un label o un textbox, los valores de texto), cols/rows/autoFit para una cuadrícula de inventario, cantidad y tamaño de slots para saveSlots.
  4. Conecta los clics con la lista compartida de tokens onClickAction, el mismo widget de picker tipado que usan las reacciones de hotspot y las opciones de diálogo.
  5. Autora el text del widget una sola vez, en el idioma por defecto de tu proyecto; las traducciones se completan después en el Editor de Traducción.
  6. Enlaza una animación desde la categoría GUI del Editor de Animaciones si el widget necesita reproducir cuadros.
  7. Save para escribir el guis.json del proyecto.

Borrar una GUI#

Un solo botón en la barra de herramientas maneja quitar una GUI, y lee el descriptor cargado en ese momento para decidir cuál de dos cosas muy distintas hace:

  • Sobre una de las cinco ranuras por defecto del motor (la barra principal, el verb-coin, y los modales de inventario/guardar/opciones) dice Revertir al default: sacarla del guis.json no la saca del juego; esas cinco se reconstruyen solas desde las plantillas propias del motor en cuanto la clave no está, así que la barra o el modal nunca desaparece, solo se pierden tus cambios guardados sobre esa versión (y esa pérdida no se puede deshacer).
  • Sobre un modal propio tuyo dice Borrar GUI, en rojo, porque no hay ningún default detrás: borrarla es permanente, y cualquier OPENGUI:<id> que la siga apuntando queda colgando (el token es null-safe, no hace nada en silencio, pero el diálogo de confirmación lista exactamente qué reacciones, vigías o botones de GUI todavía la nombran, así los puedes arreglar antes en vez de descubrirlo jugando).
  • Sobre una entrada virtual (un default que se ofrece para editar, o una GUI nueva que todavía no guardaste), el botón está deshabilitado: no hay nada en disco que sacar todavía. Guárdala una vez y se habilita.

Una barra principal que se esconde en el borde#

Una barra principal puede quedarse fuera del camino hasta que hace falta. La propiedad Barra que se asoma al borde tiene dos valores: Siempre visible, el de fábrica y lo que hacían todas las barras hasta ahora, y Se asoma al borde. Con el segundo, la barra vive escondida justo del otro lado del borde donde está (debajo de la imagen si está abajo, encima si está arriba) y se asoma cuando el cursor llega a ese borde; se vuelve a esconder un rato después de que el cursor se va. La sección aparece sólo en la GUI principal: ningún otro panel se desliza.

La ajustan tres deslizadores, y un campo vacío vuelve a su valor de fábrica:

  • Franja del borde (px): qué tan gruesa es la banda junto al borde de la barra que dispara el asomo. 1 a 64, de fábrica 8. Es la banda que el cursor tiene que alcanzar, no el alto de la barra. Pasarse del borde también cuenta, así que las barras negras alrededor de la imagen funcionan como parte de ella.
  • Espera antes de esconderse (s): cuánto se queda afuera la barra después de que el cursor se va. 0 a 5, de fábrica 1.5.
  • Duración del deslizamiento (ms): cuánto tarda el deslizamiento, en cada dirección. 0 a 1000, de fábrica 250; en 0 la barra aparece de golpe en vez de deslizarse.

El mouse y el stick de un control llegan al borde, y cambiar de verbo con los botones laterales asoma la barra un momento para que el jugador vea lo que eligió. Tres cosas la mantienen afuera sin importar el cursor:

  • Pantallas táctiles: no hay hover que la dispare, así que la barra queda siempre visible; una barra que se esconde sería inalcanzable.
  • Un ítem en la mano: la frase "Usar … con" vive en la barra, así que se queda arriba hasta que el ítem se usa o se suelta.
  • Una conversación: las opciones se dibujan sobre la barra, así que se abre de inmediato.

La barra nunca está escondida en un momento en que el jugador la necesita. Esconder todos los GUIs (la tecla M, o HIDEGUI) sigue ganándole a todo esto. Mientras la barra no está, la franja que tapaba vuelve a ser de la sala (un clic ahí camina e interactúa como en cualquier otro lado), siempre que el área jugable llegue hasta ahí. Para que esa franja muestre algo, pinta los fondos de tus salas a todo el alto de la pantalla.

Una barra que se esconde cambia cuál debería ser el área jugable. Una barra siempre visible es dueña de la banda de abajo de la pantalla, así que el área de sala termina arriba de ella. Una que se esconde, no. Con la barra fuera del camino esa banda queda vacía salvo que la sala llegue hasta ahí. Por eso, guardar una barra principal que se esconde hace que el chequeo de abajo corra al revés: en vez de ofrecer subir el área de sala hasta la barra, ofrece extenderla a todo el alto, nombrando los píxeles que si no quedarían en blanco.

Una barra principal arriba#

La barra principal no tiene que vivir abajo. Arrastra su marco a la parte de arriba del lienzo y el editor deduce el borde de donde la pusiste: una barra cuyo centro queda en la mitad de arriba de la pantalla es una barra de arriba. No hay ningún ajuste que cambiar, y si la vuelves a bajar vuelve a ser una barra de abajo.

La sala se sigue dibujando desde arriba de la pantalla y la barra simplemente tapa esa franja, así que nada de tus salas se mueve; deja lo importante debajo de la barra, o pinta esa franja para que quede detrás. Todo lo que antes se quedaba arriba de una barra de abajo ahora se queda debajo de una de arriba:

  • Clics: la franja de la barra es de la barra y todo lo que está debajo es sala, incluida la franja de abajo que antes era de la barra.
  • Conversaciones: las opciones se listan dentro de la barra, en su parte de arriba.
  • Globos de diálogo y el texto del narrador arriba se quedan debajo de la barra en vez de esconderse detrás, y la moneda de verbos se abre debajo de ella.
  • Barra que se asoma al borde: una barra de arriba se esconde hacia arriba y se asoma cuando el cursor llega al borde de arriba; el borde de abajo no la abre.

Guardar una barra principal puede ofrecer mover el área jugable#

roomViewport en project.json es dónde termina el área de sala: la franja arriba de la barra en la que se dibujan las salas. El wizard la deja en el borde superior de la barra al crear el proyecto, pero después nadie las mantenía en paso: mueves o materializas una barra principal y el área jugable del proyecto sigue describiendo la anterior, mientras el runtime le cree al archivo del proyecto. O la sala se dibuja debajo de la barra y pierde su banda inferior, o queda una franja que no es de nadie: la sala no la puede usar y la barra no la cubre.

Así que al guardar un panel (una barra principal) se compara el borde superior de la barra con el área jugable, y si difieren por más de un par de píxeles se ofrece alinearlas, diciendo los números: esta barra empieza en y=740, pero el área de sala llega hasta 1024: la barra tapa los últimos 284 px de cada sala. Es una oferta, nunca automático: dejarla como está es lo que pasa por defecto, y el chequeo corre después de que el GUI ya se guardó, así que rechazarla no cuesta nada. Aceptar parchea sólo el alto de roomViewport (un x/y/ancho autorado queda intacto) y una vez que coinciden, la oferta deja de aparecer.

Una barra que se asoma desde el borde invierte el objetivo. Tapa la sala a propósito y la destapa al esconderse, así que el área de sala debería llegar a todo el alto de la pantalla; cortarla donde empieza la barra dejaría una franja vacía cada vez que la barra se va. Para esas barras la oferta es la otra (extender el área jugable a todo el alto) y el aviso de "la barra tapa la sala" no aparece. Una barra arriba recibe la misma oferta por la misma razón: se dibuja sobre la parte de arriba de la sala, así que el área de sala debería llegar hasta abajo.

Los ids de widget son load-bearing para partySlotRef y cualquier otro campo ref, pero el id en sí es solo una etiqueta que el widget porta, no una clave de contenido estable como un id de hotspot o de ítem. Los valores que de verdad se referencian en otros lados son los campos ref: el número de slot de partySlotRef se resuelve en runtime contra chosenOrder(state), no contra ningún personaje fijo; renombra el roster de personajes y el botón sigue funcionando. El único id que sí es load-bearing es el id propio de la GUI, ya que tanto OPENGUI:<id> como la resolución de la barra principal dependen directamente de él; renombrar una GUI después de que otro contenido ya la abre con OPENGUI:<idViejo> rompe ese enlace en silencio (el token es null-safe, simplemente no hace nada, así que la falla es silenciosa, no un crash).