WENOPAL

L02 · Números, texto y booleanos

Aritmética con doubles, el % de Kahlua, ZombRand, textos y sus métodos (también los extra de Kahlua), string.format, getText y qué es verdadero en Lua.

Curso técnico · Build 42 · Actualizado el

Objetivo: manejar los tres tipos de valor que más vas a usar en un mod (números, textos y booleanos) sabiendo exactamente cómo se comportan en Kahlua, que en varios puntos no es igual al Lua estándar.

1. Números: todos son double

En Lua hay un solo tipo de número. En Kahlua cada número es un double de Java: no hay enteros separados.

lua
print(7 + 2)     -- 9
print(7 - 2)     -- 5
print(7 * 2)     -- 14
print(7 / 2)     -- 3.5  (la división NUNCA redondea)
print(2 ^ 10)    -- 1024 (potencia)
print(-7)        -- -7

Cómo se ven al imprimir (lo decide el tostring de Kahlua):

ValorSe vePor qué
5 o 10 / 25Un número entero menor que 1e14 se muestra sin decimales
3.53.5
0.1 + 0.20.30000000000000004Es un double: algunos decimales no son exactos
1e-51.0E-5Los no enteros se muestran con Double.toString de Java
1e151.0E15Es entero, pero no es menor que 1e14
1 / 0inf
0 / 0nan"no es un número"

Dos consecuencias prácticas:

  • No compares decimales con ==. 0.1 + 0.2 == 0.3 es false. Compara con un margen: math.abs(a - b) < 0.0001.
  • Los números muy grandes pierden precisión. Un double guarda enteros exactos solo hasta 2^53 (unos 9 mil billones). Para contadores y coordenadas del juego sobra; para IDs largos guardados como número, no.

En Kahlua no existe el operador // (división entera de Lua 5.3): es un error de sintaxis. Para dividir sin decimales usa math.floor:

lua
local minutos = 135
local horas = math.floor(minutos / 60)   -- 2
print(horas)

2. El % de Kahlua NO es el del Lua estándar

a % b es el resto de dividir a por b:

lua
print(135 % 60)   -- 15  (135 minutos = 2 horas y 15 minutos)
print(10 % 2)     -- 0   (par)
print(7 % 2)      -- 1   (impar)

Con números positivos funciona como esperas. Con negativos, Kahlua calcula a - trunc(a / b) * b, igual que Java, y el resultado lleva el signo de a:

ExpresiónKahlua (PZ)Lua estándar
7 % 311
-1 % 3-12
-7 % 3-12
5 % 0nannan

Si copias código de un tutorial de Lua genérico que usa % para "dar la vuelta" (por ejemplo, ir al elemento anterior de una lista circular), en PZ puede darte un índice negativo. Cuando necesites un resto siempre positivo:

lua
local function modPositivo(a, b)
    return ((a % b) + b) % b
end
print(modPositivo(-1, 3))   -- 2

(Las funciones las ves en L04. Por ahora basta con copiar la fórmula.)

3. La librería math

FunciónQué haceEjemplo
math.floor(x)Redondea hacia abajomath.floor(2.7) → 2, math.floor(-2.5) → -3
math.ceil(x)Redondea hacia arribamath.ceil(2.1) → 3
math.abs(x)Valor absolutomath.abs(-4) → 4
math.min(a, b, ...)El menormath.min(5, 2, 9) → 2
math.max(a, b, ...)El mayor
math.sqrt(x)Raíz cuadradamath.sqrt(16) → 4
math.pow(x, y)Potencia (igual que x ^ y)
math.fmod(a, b)Resto con signo de a
math.pi, math.hugeπ e infinito

Para redondear al entero más cercano: math.floor(x + 0.5). El juego trae una global round(num, decimales) en media/lua/shared/env.lua que hace justo eso (la viste en L01).

math.min y math.max sirven para limitar un valor, algo que en un mod haces todo el tiempo:

lua
local hambre = 1.3
hambre = math.min(math.max(hambre, 0), 1)   -- queda entre 0 y 1
print(hambre)                               -- 1

4. Azar: ZombRand

El juego usa ZombRand para todo lo aleatorio (481 usos en su Lua; math.random, ninguno). Úsalo tú también.

LlamadaDevuelve
ZombRand(n)Un entero de 0 a n - 1
ZombRand(a, b)Un entero de a a b - 1: b nunca sale
ZombRand(0)0
lua
local dado = ZombRand(1, 7)        -- 1 a 6 (el 7 no sale)
local otroDado = ZombRand(6) + 1   -- 1 a 6, la forma que más usa el juego

if ZombRand(100) < 25 then         -- 25 % de probabilidad (0..24 de 0..99)
    print("[CursoLab] tuviste suerte")
end

Elegir un elemento al azar de una lista (las listas empiezan en 1, lo ves en L05). Así lo hace el juego en media/lua/shared/Foraging/forageSystem.lua:

lua
local catName = categoryTable[ZombRand(#categoryTable) + 1];

Ojo: si la lista está vacía, ZombRand(0) da 0, el índice queda en 1 y obtienes nil. Revisa que la lista tenga algo antes.

5. Texto (strings)

Un string va entre comillas dobles o simples. Para textos de varias líneas, [[ ... ]]:

lua
local a = "Sopaipilla"
local b = 'con "pebre"'          -- comillas dobles dentro de simples
local c = "línea 1\nlínea 2"     -- \n salto de línea, \t tabulación, \" comilla, \\ barra
local d = [[
Texto largo
en varias líneas
]]

Concatenar: ..

lua
local item = "Sopaipilla"
local cantidad = 3
print("[CursoLab] tienes " .. cantidad .. " " .. item)   -- los números se convierten solos

Concatenar nil o un booleano da error (__concat not defined for operands). Es el error más común al armar mensajes:

lua
local nombre = nil
print("Hola " .. nombre)               -- error
print("Hola " .. tostring(nombre))     -- "Hola nil", sin error

Largo: #

#texto da el largo. En Kahlua los strings son String de Java, así que # cuenta caracteres, no bytes como en el Lua estándar: #"Ñandú" es 5.

Convertir: tostring y tonumber

lua
print(tostring(42))          -- "42"
print(tostring(true))        -- "true"
print(tostring(nil))         -- "nil"

print(tonumber("42"))        -- 42
print(tonumber("3.5"))       -- 3.5
print(tonumber("hola"))      -- nil: no es un número
print(tonumber(" 12 "))      -- 12: los espacios de los lados se ignoran

tonumber devuelve nil cuando no puede convertir: siempre revisa el resultado antes de usarlo. En Kahlua tiene una rareza heredada de Java: si el texto no es un número pero termina en nan o inf, devuelve nan o inf en vez de nil. tonumber("banana") da nan. Si lees números escritos por el usuario (por ejemplo desde un cuadro de texto), comprueba también que el resultado sea igual a sí mismo: n == n es false solo para nan.

Kahlua también convierte texto a número automáticamente en la aritmética ("10" + 1 da 11), pero no lo uses a propósito: conviértelo tú con tonumber y el código se entiende mejor.

6. Métodos de string

Las funciones de texto están en la tabla string, y se pueden llamar de dos formas equivalentes:

lua
local s = "Base.Sopaipilla"
print(string.upper(s))   -- BASE.SOPAIPILLA
print(s:upper())         -- lo mismo, con ":" (más corto, el juego lo usa mucho)
MétodoQué haceEjemplo
s:len()Largo (como #s)("hola"):len() → 4
s:lower(), s:upper()Minúsculas, mayúsculas (entienden tildes y ñ)
s:sub(i, j)Pedazo del carácter i al j (desde 1; negativos cuentan desde el final)("Base.Axe"):sub(1, 4) → "Base", ("Base.Axe"):sub(-3) → "Axe"
s:rep(n)Repite n veces("-"):rep(10)
s:reverse()Invierte
s:find(t, 1, true)Posición de t dentro de s, o nil("Base.Axe"):find(".", 1, true) → 5 5
s:format(...)Ver sección 7

Y los extra de Kahlua, que no existen en el Lua estándar pero el juego usa a cada rato:

MétodoQué haceEn el juego
s:trim()Quita espacios al principio y al finallocal steamID = line:trim() (client/OptionScreens/InviteFriends.lua)
s:contains(t)true si t aparece dentro de s (texto literal)newHairStyle:getName():contains("Bald") (shared/TimedActions/ISCutHair.lua)
s:split(sep)Corta s en una lista de pedazoskeyvalues.requireInstalled:split(";") (server/Vehicles/Vehicles.lua)
string.sort(a, b)true si a va después de b alfabéticamentenot string.sort(a:getDisplayName(), b:getDisplayName()) (shared/Camping/ISCampingMenu.lua)

Cuidado con split: por dentro usa String.split de Java, que interpreta el separador como expresión regular de Java. Con ;, , o : no hay problema, pero ., |, +, *, ?, ( o [ tienen significado especial. Mira cómo el juego tiene que escapar el punto en server/Vehicles/Vehicles.lua:

lua
local ss = functionName:split("\\.")

Con s:split(".") obtendrías una lista vacía. Otro detalle de Java: los pedazos vacíos del final se descartan (("a,b,,"):split(",") da solo a y b).

find, match, gsub y gmatch también aceptan patrones (una especie de expresiones regulares propias de Lua). Los ves a fondo en L10. Mientras tanto, si buscas un texto literal, usa :contains(t) o :find(t, 1, true): con true como cuarto argumento, find no interpreta el texto como patrón.

7. string.format

Arma un texto a partir de una plantilla con huecos que empiezan con %:

lua
local item, cantidad, peso = "Sopaipilla", 3, 0.4567
print(string.format("%s x%d (%.2f kg)", item, cantidad, peso))
-- Sopaipilla x3 (0.46 kg)
HuecoParaEjemploResultado
%sTexto (o número)("%s"):format("hola")hola
%dEntero("%d"):format(42)42
%.2fDecimal con 2 decimales("%.2f"):format(3.14159)3.14
%5dEntero de ancho 5 (relleno con espacios)("%5d"):format(42) 42
%02dEntero de ancho 2 (relleno con ceros)("%02d"):format(7)07
%+.2fCon signo siempre("%+.2f"):format(5)+5.00
%%Un % literal("%d%%"):format(50)50%

Dos detalles de Kahlua:

  • %d con un número con decimales trunca sin avisar: ("%d"):format(2.9) da 2. Si quieres redondear, hazlo antes con math.floor(x + 0.5).
  • %s solo acepta textos y números. Con nil, un booleano o una tabla da error. Pasa esos valores por tostring primero: ("%s"):format(tostring(valor)).

El juego usa string.format en más de 200 lugares. En media/lua/client/Foraging/ISZoneDisplay.lua:

lua
return string.format("%+.2f", _float * 100);

8. Textos para el jugador: getText

Los textos que ve el jugador no se escriben en el código: se ponen en archivos de traducción y se piden por su clave con getText. Así tu mod se puede traducir sin tocar el Lua (el módulo 13 lo explica completo).

lua
print(getText("IGUI_Gametime_hours"))   -- "horas" si juegas en español, "hours" en inglés

La traducción puede tener huecos %1, %2... que getText llena con los argumentos que le pases:

lua
-- en media/lua/shared/Translate/ES/ContextMenu.json del juego:
--     "ContextMenu_AllWithCount": "Todo (%1)",
print(getText("ContextMenu_AllWithCount", 5))   -- Todo (5)

Si la clave no existe, getText devuelve la clave misma (verás IGUI_CursoLab_Algo escrito en pantalla). Es la señal de que te falta la traducción o escribiste mal la clave. Si prefieres detectarlo en el código, getTextOrNull devuelve nil en ese caso, y el juego la combina con or para tener un texto de respaldo (client/OptionScreens/ISGameSounds.lua):

lua
tabName = getTextOrNull("GameSound_Category_"..tabName) or tabName

9. Booleanos y qué es "verdadero"

Un booleano es true o false. Lo producen las comparaciones (L03) y lo devuelven muchos métodos del juego (isClient(), jugador:isAsleep()...).

La regla más importante de Lua para escribir condiciones:

lua
if 0 then print("0 es verdadero") end             -- se imprime
if "" then print("el texto vacío es verdadero") end -- se imprime
if nil then print("esto nunca") end
if false then print("esto tampoco") end

Si vienes de otro lenguaje (JavaScript, Python, C), aquí te vas a equivocar al menos una vez. Para preguntar si un texto está vacío o si un número es cero, compáralo explícitamente:

lua
local texto = ""
if texto ~= "" then print("tiene algo") end
local cantidad = 0
if cantidad > 0 then print("hay") end

not invierte: not nil es true, not 0 es false.

lua
local tooltip = ""

if playerObj:getStats():get(CharacterStat.ENDURANCE) <= 0 then
    if string.len(tooltip) > 0 then tooltip = tooltip .. " <LINE> " end
    tooltip = tooltip ..getText("Tooltip_lightFireNoEndurance")
end

Y en el mismo archivo, ISCampingMenu.timeString junta casi todo este módulo en diez líneas: math.floor, %, la global round, tostring, concatenación y getText.

lua
function ISCampingMenu.timeString(timeInMinutes)
    if not timeInMinutes then timeInMinutes = 5 end
    local hours = round(math.floor(timeInMinutes / 60),2)
    local minutes = round(timeInMinutes % 60,2)
    local hourStr = tostring(hours) .. ' ' .. (hours == 1 and getText("IGUI_Gametime_hour") or getText("IGUI_Gametime_hours"))
    local minuteStr = tostring(minutes) .. ' ' .. (minutes <= 1 and getText("IGUI_Gametime_minute") or getText("IGUI_Gametime_minutes"))

if not timeInMinutes then usa la regla de arriba: si no le pasaron nada (nil), usa 5. El x and a or b lo ves en L03.


Ejercicios

Ejercicio L2.1 · Horas y minutos

En la consola, a partir de local minutos = 135, imprime 2 h 15 min usando math.floor, % y ... Después prueba con minutos = 59, 60 y 61.

Listo cuando: los cuatro casos dan el texto correcto.

Solución
lua
local minutos = 135
local h = math.floor(minutos / 60)
local m = minutos % 60
print(h .. " h " .. m .. " min")

Recuerda que en la consola cada ejecución es un trozo aparte: escribe las cuatro líneas juntas en una sola ejecución.

Ejercicio L2.2 · El % negativo

Predice, y después comprueba en la consola, el resultado de: -1 % 5, -6 % 5, 6 % 5, ((-1 % 5) + 5) % 5 y -6 % -5.

Listo cuando: acertaste o puedes explicar por qué no, usando la fórmula a - trunc(a / b) * b.

Solución
ExpresiónResultadoCuenta
-1 % 5-1trunc(-0.2) = 0, -1 - 0 = -1
-6 % 5-1trunc(-1.2) = -1, -6 - (-5) = -1
6 % 51
((-1 % 5) + 5) % 54(-1 + 5) % 5
-6 % -5-1trunc(1.2) = 1, -6 - (-5) = -1

En el Lua estándar -1 % 5 daría 4 y -6 % 5 también 4.

Ejercicio L2.3 · Limpiar una lista de ítems

Tienes este texto, como si lo hubiera escrito un jugador en una opción del mod:

lua
local entrada = "  Sopaipilla, Empanada ,Mote con Huesillo  "

Usando trim, split, lower y contains, imprime cada ítem limpio y en minúsculas, uno por línea, y después imprime si la entrada contiene "empanada" (sin importar mayúsculas).

Pista: split devuelve una lista. Para recorrerla, usa por ahora for i = 1, #lista do ... lista[i] ... end (lo explica L03).

Listo cuando: ves sopaipilla, empanada y mote con huesillo sin espacios sobrantes, y luego true.

Solución
lua
local entrada = "  Sopaipilla, Empanada ,Mote con Huesillo  "
local partes = entrada:split(",")
for i = 1, #partes do
    print("[CursoLab] '" .. partes[i]:trim():lower() .. "'")
end
print("[CursoLab]", entrada:lower():contains("empanada"))

Las comillas simples alrededor de cada ítem sirven para ver que no quedaron espacios. Se puede encadenar :trim():lower() porque cada método devuelve un string nuevo.

Ejercicio L2.4 · Una línea de estado

En una partida, imprime con un solo string.format una línea como esta:

[CursoLab] Ana Pérez | 12.5 h sobrevividas | 37 zombis

Usa getPlayer():getFullName(), getPlayer():getHoursSurvived() (con 1 decimal) y getPlayer():getZombieKills().

Listo cuando: la línea sale con el formato exacto, con un decimal en las horas.

Solución
lua
local p = getPlayer()
print(string.format("[CursoLab] %s | %.1f h sobrevividas | %d zombis",
    p:getFullName(), p:getHoursSurvived(), p:getZombieKills()))

%.1f redondea a un decimal. %d con un número entero (como el contador de zombis) lo muestra tal cual.

Ejercicio L2.5 · ¿Verdadero o falso?

Sin ejecutar, decide cuáles de estos if imprimen. Después compruébalo.

lua
if 0 then print("A") end
if "" then print("B") end
if "false" then print("C") end
if false then print("D") end
if nil then print("E") end
if not nil then print("F") end
if tonumber("abc") then print("G") end
if tonumber("0") then print("H") end

Listo cuando: acertaste las ocho, o entiendes cada una que fallaste.

Solución

Imprimen A, B, C, F, H. 0, "" y el texto "false" son valores distintos de nil y false, así que son verdaderos. tonumber("abc") es nil (falso). tonumber("0") es el número 0, que es verdadero.

Desafío L2

Escribe en la consola el reloj de la partida en el formato Día 3, 07:05, a partir de getGameTime():getWorldAgeHours() (las horas de juego desde que empezó el mundo, con decimales). El día 1 son las primeras 24 horas. Usa math.floor, % y string.format con %02d.

Después agrega una línea que diga, con getText, cuántas horas faltan para el siguiente día, usando getText("IGUI_Gametime_hours") para la palabra "horas".

Listo cuando: el reloj coincide con la hora que muestra el juego (puede diferir en la hora de inicio del mundo; ver la pista) y los minutos siempre tienen dos dígitos.

Una solución
lua
local total = getGameTime():getWorldAgeHours()
local dia = math.floor(total / 24) + 1
local horaDecimal = total % 24
local hora = math.floor(horaDecimal)
local minuto = math.floor((horaDecimal - hora) * 60)
print(string.format("[CursoLab] Día %d, %02d:%02d", dia, hora, minuto))

local faltan = 24 - horaDecimal
print(string.format("[CursoLab] Faltan %.1f %s para el día %d",
    faltan, getText("IGUI_Gametime_hours"), dia + 1))

Pista: getWorldAgeHours() cuenta desde el inicio del mundo, que no es necesariamente la medianoche. Si tu reloj no coincide con el del juego, compara con getGameTime():getTimeOfDay() (la hora del día con decimales) y piensa qué número tendrías que sumar. Como total es positivo, aquí el % de Kahlua da lo mismo que el estándar.


Anterior: L01 · Tu primer Lua en el juego · Siguiente: L03 · Decisiones y bucles