Objetivo: agregar opciones al clic derecho sin pelear con otros mods, y construir ventanas propias entendiendo cómo se conecta una tabla de Lua con un elemento de interfaz de Java.
1. El menú contextual: engancharse, no reemplazar
El juego arma el menú del clic derecho en dos archivos enormes (client/ISUI/ISInventoryPaneContextMenu.lua y ISWorldObjectContextMenu.lua), y al final dispara un evento para que los mods agreguen lo suyo:
| Clic derecho sobre… | Evento | Argumentos | Dónde se dispara |
|---|---|---|---|
| ítems del inventario | OnFillInventoryObjectContextMenu | playerNum, context, items | ISInventoryPaneContextMenu.lua:935 |
| el mundo | OnFillWorldObjectContextMenu | playerNum, context, worldobjects, test | ISWorldObjectContextMenu.lua:213 |
El comentario del propio juego junto al segundo lo dice: "use the event (as you would 'OnTick' etc) to add items to context menu without mod conflicts". No envuelvas createMenu para esto: usa el evento.
Ejemplo real: uno de nuestros mods
En Pumpkin Z DIY, uno de nuestros mods (PumpkinZ/.../client/PumpkinZ_CarveUI.lua):
local function onFillInventoryMenu(playerNum, context, items)
local player = getSpecificPlayer(playerNum)
if not player or player:isDead() then return end
local pumpkin
for _, v in ipairs(items) do
local item = v
if type(v) == "table" and v.items then item = v.items[1] end -- ← ojo con esto
if item and item.getFullType and item:getFullType() == PUMPKIN then
pumpkin = item
break
end
end
if not pumpkin then return end
local knife = sharpKnife(player)
local option = context:addOption(getText("IGUI_PumpkinZ_CarveMenu"), player, PumpkinZ.openCarveUI, pumpkin, knife)
if not knife then
option.notAvailable = true -- se ve en gris
local tip = ISInventoryPaneContextMenu.addToolTip()
tip.description = getText("IGUI_PumpkinZ_NeedKnife") -- y explica por qué
option.toolTip = tip
end
end
Events.OnFillInventoryObjectContextMenu.Add(onFillInventoryMenu)Tres cosas que este código hace bien y que vas a copiar siempre:
itemsmezcla dos formas. Si el jugador hace clic en una pila del inventario, el elemento es una tabla{ items = {...} }(la pila) y no el ítem. Si hace clic en un ítem suelto, es el objeto Java. Siempre normaliza.- Opción deshabilitada con explicación en vez de no mostrarla. El jugador aprende qué le falta. El juego usa
notAvailable = true153 veces. playerNumes un número, no el jugador:getSpecificPlayer(playerNum)(pantalla dividida).
addOption y sus parámetros
context:addOption(texto, target, funcion, arg1, arg2, ...) -- hasta 10 argumentosAl elegir la opción, el juego llama funcion(target, arg1, arg2, ...). Si funcion es un método (MiClase.metodo), target hace de self. Otras funciones útiles de ISContextMenu: addOptionOnTop, insertOptionAfter(nombreAnterior, ...), removeOptionByName, getNew(context) + addSubMenu(opcion, submenu) para submenús, y addDebugOption (solo en debug).
El parámetro test del menú del mundo
El menú del mundo a veces se arma solo para preguntar si habría opciones (por ejemplo para el mando), con test = true. El juego tiene 35 líneas del estilo if test then return true end. Si tu función abre ventanas o hace algo caro, respeta test.
2. Cómo funciona una ventana
Cada elemento de interfaz tiene dos mitades:
Lua: tabla PumpkinZCarveUI (derivada de ISPanel) Java: zombie.ui.UIElement
.x .y .width .height ◄──────► posición, tamaño, foco, dibujo
:prerender() :render() :onMouseDown() ... ◄──llama── por nombre, cada frame / evento
.javaObject ──────────────────────────────────────────────►ISUIElement:instantiate()crea la mitad Java:self.javaObject = UIElement.new(self). Le pasa la tabla de Lua.- Desde ahí, Java llama a tus funciones por nombre: en
zombie.ui.UIElementestán las cadenasprerender,render,update,onMouseDown,onMouseUp,onMouseMove,onMouseWheel,onRightMouseDown,onKeyPress,onResize... Si tu tabla tiene una función con ese nombre, se llama. Si no, no pasa nada. addToUIManager()llama ainstantiate()si hace falta y registra el elemento enUIManager.addChild(otro)hace lo mismo con un hijo.
El ciclo de vida
MiVentana = ISPanel:derive("MiVentana")
function MiVentana:new(x, y, w, h) -- 1. crear la tabla: solo datos, nada de Java todavía
local o = ISPanel.new(self, x, y, w, h)
o.backgroundColor = { r = 0, g = 0, b = 0, a = 0.8 }
o.moveWithMouse = true
return o
end
function MiVentana:initialise() -- 2. crear hijos (botones, listas)
ISPanel.initialise(self)
self.boton = ISButton:new(10, 10, 100, 25, "OK", self, MiVentana.onOK)
self.boton:initialise()
self:addChild(self.boton)
end
function MiVentana:prerender() -- 3. cada frame: el fondo
ISPanel.prerender(self)
end
function MiVentana:render() -- 4. cada frame: lo que va encima
self:drawText("Hola", 10, 50, 1, 1, 1, 1, UIFont.Small)
end
function MiVentana:close() -- 5. salir
self:setVisible(false)
self:removeFromUIManager()
end
-- abrirla:
local v = MiVentana:new(100, 100, 300, 200)
v:initialise()
v:addToUIManager()Pumpkin Z DIY sigue exactamente este patrón y agrega uno más: una sola instancia (if PumpkinZCarveUI.instance then PumpkinZCarveUI.instance:close() end). Sin eso, cada clic abre otra ventana encima.
Algunos elementos (las ventanas ISCollapsableWindow, por ejemplo) crean sus hijos en createChildren() en vez de initialise(): lo llama el juego al instanciar. Mira cuál usa la clase de la que derivas.
Los ladrillos
En client/ISUI/: ISPanel (base), ISCollapsableWindow (ventana con barra de título, se puede mover y cerrar), ISButton, ISLabel, ISTickBox, ISComboBox, ISTextEntryBox, ISScrollingListBox (listas largas), ISRichTextPanel (texto con formato), ISModalDialog (sí/no). Antes de construir algo, busca una ventana del juego que se parezca y lee cómo está hecha.
3. Tamaños: fuentes y pantallas
PZ deja que el jugador elija el tamaño de letra. Una interfaz con alturas fijas en píxeles se rompe con letra grande. El juego mide siempre:
local FONT_HGT_SMALL = getTextManager():getFontHeight(UIFont.Small) -- al inicio del archivo
local ancho = getTextManager():MeasureStringX(UIFont.Small, texto) -- 733 usos en el juegoReglas:
- Altura de botones y filas = altura de fuente + margen, no un número fijo.
- Ancho de botones = texto medido + margen (los textos traducidos al portugués suelen ser más largos que en inglés).
- Posición centrada:
getCore():getScreenWidth(),getCore():getScreenHeight()(como Pumpkin Z DIY).
4. Dibujar
Dentro de prerender/render, coordenadas relativas al elemento:
| Función | Para |
|---|---|
drawRect(x, y, w, h, a, r, g, b) / drawRectBorder(...) | Rectángulos (ojo: alfa primero) |
drawText(txt, x, y, r, g, b, a, fuente), drawTextCentre, drawTextRight | Texto (aquí alfa va al final) |
drawTexture(tex, x, y, a, r, g, b), drawTextureScaled(tex, x, y, w, h, a, r, g, b) | Imágenes (getTexture(...) del módulo 04) |
El orden distinto de los colores entre drawRect y drawText es un tropiezo clásico: si tu texto sale invisible, revisa ese orden.
Ejercicios
Ejercicio 10.1 · Opción con condiciones
Agrega al clic derecho de un ítem de comida la opción "Ver datos (CursoLab)", que solo aparece en comida. Si el ítem está podrido, la opción sale deshabilitada con un tooltip que dice por qué. Al elegirla, imprime en la consola hambre, días frescos y estado.
Listo cuando: funciona con un ítem suelto y con una pila de 5, y no aparece con herramientas.
Ejercicio 10.2 · Submenú en el mundo
En el clic derecho sobre el mundo, agrega un submenú "CursoLab" con tres opciones: "Coordenadas aquí", "Tipo de suelo" y "¿Hay zombis cerca?". Usa worldobjects para saber el cuadro (pista: cada objeto tiene getSquare()).
Listo cuando: el submenú funciona y respetas test.
Ejercicio 10.3 · Tu primera ventana
Haz CursoLab.VentanaBitacora, una ISCollapsableWindow que muestre las últimas 20 entradas de la bitácora (ejercicio 6.2) en una ISScrollingListBox, con un botón "Volcar a archivo" y un botón "Limpiar". Ábrela con una tecla (pista: Events.OnKeyPressed y Keyboard.KEY_..., módulo 07).
Listo cuando: se abre, se mueve, se cierra, no se abre dos veces y se ve bien con la letra más grande (opciones del juego → Pantalla → tamaño de letra).
Ejercicio 10.4 · Leer una ventana del juego
Elige una ventana del juego que te guste (busca en client/ISUI/), y dibuja en papel su árbol de elementos: qué hijos crea, en qué función, y qué funciones de render usa. Encuentra una técnica que no conocías.
Ejercicio 10.5 · Medir antes de dibujar
Cambia tu ventana del 10.3 para que ningún tamaño sea un número fijo: todo calculado desde getFontHeight y MeasureStringX. Pruébala en español, inglés y con letra grande.
Desafío 10
Una mini-HUD: un panel pequeño y transparente en una esquina que muestre en vivo la hora del juego, la temperatura y el peso cargado. Que se pueda mover con el mouse, que recuerde su posición entre partidas (pista: getFileWriter, módulo 06, o ModData, módulo 14) y que no consuma rendimiento (actualiza el texto cada segundo, no cada frame).
Anterior: 09 · Sobrescribir comportamiento · Siguiente: 11 · Timed actions