WENOPAL

13 · Loot, sandbox y traducciones

Cómo poner ítems en el mundo con una probabilidad calculada, hacerla ajustable con opciones de sandbox y traducir todo el texto del mod.

Curso técnico · Build 42 · Actualizado el

Objetivo: poner tus ítems en el mundo con una probabilidad que entiendes (no "a ojo"), dejar que el dueño del servidor la ajuste con opciones de sandbox, y que todo el texto del mod salga en el idioma del jugador.

1. Cómo llega el loot de Lua a Java

Las distribuciones son tablas de Lua (server/Items/Distributions.lua, ProceduralDistributions.lua: 53.418 líneas). Pero el que tira los dados es Java (ItemPickerJava, módulo 08). El paso de una cosa a la otra ocurre una sola vez, al cargar el mundo. En el bytecode de zombie.iso.IsoWorld el orden es:

triggerEvent OnPreDistributionMerge
triggerEvent OnDistributionMerge
triggerEvent OnPostDistributionMerge
SandboxOptions.load()
ItemPickerJava.Parse()               ← Java copia las tablas de Lua a sus propias estructuras
ItemPickerJava.InitSandboxLootSettings()

Consecuencias:

  • Modifica las tablas en uno de los tres eventos (o al cargar tu archivo). En nuestros mods usamos OnPreDistributionMerge (Tao Pai Pai's Pillar, Sable de Luz) u OnPostDistributionMerge (Tocomples Food, Adornos18): los dos sirven porque van antes de Parse().
  • Cambiar las tablas después no hace nada: Java ya tiene su copia. Si en mitad de la partida haces table.insert en ProceduralDistributions, el loot no cambia.

Agregar un ítem a un contenedor

Las distribuciones procedurales son listas planas nombre, peso, nombre, peso.... Tocomples Food lo hace en CompletoLoot.lua con el patrón defensivo que conviene copiar:

lua
require "Items/Distributions"
require "Items/ProceduralDistributions"

local applied = false
local function addLoot()
    if applied then return end                       -- una sola vez
    local ok, err = pcall(function()
        local function insertProcedural(name, item, weight)
            local d = ProceduralDistributions.list[name]
            if not d or not d.items then return end  -- el contenedor puede no existir en otra versión
            table.insert(d.items, item)
            table.insert(d.items, weight)
        end
        insertProcedural("FridgeGeneric", "CompleteItaliano.Vienesa", 50)
        applied = true
    end)
    if not ok then print("[CompletoItaliano] Distribution injection error: " .. tostring(err)) end
end
Events.OnPostDistributionMerge.Add(addLoot)

Qué significa el peso

Con la fórmula que sacaste del .jar en el módulo 08:

probabilidad por tirada = (peso × 100 × modificadorDeSandbox + densidadDeZombis) × modificadorPorTiempo / 10.000

Con loot "normal", el peso es un porcentaje por tirada: la vienesa de Tocomples Food con peso 50 tiene un 50 % de salir en cada tirada de cada refrigerador (FridgeGeneric tira varias veces por contenedor). Es mucho. Antes de elegir un peso, mira qué pesos tienen ítems parecidos del juego en ese mismo contenedor.

Dos detalles del código de Java que conviene recordar:

  • El modificador de sandbox depende del tipo de loot del ítem (getLootType: comida, enlatados, armas, herramientas, libros...), que se deduce de su script. Una comida tuya sigue la opción "Comida" del sandbox.
  • Si el contenedor se llena, tryAddItemToContainer devuelve nil y se cortan todas las tiradas que faltaban. Ítems pesados con peso alto pueden dejar contenedores "vacíos" de lo demás.

Después de llenar: OnFillContainer

Cuando Java termina de llenar un contenedor dispara OnFillContainer(roomName, containerType, container) (está en ItemPickerJava.fillContainerInternal). Ahí puedes agregar, quitar o modificar lo que salió, con lógica que las tablas no permiten (por ejemplo: "solo si es de noche", "solo en Muldraugh"). Corre en el servidor (o en un jugador), y lo que agregues hay que sincronizarlo (módulo 12).

2. Opciones de sandbox

Son las opciones que el dueño de la partida elige al crearla. Se declaran en media/sandbox-options.txt (este es el de Batalla de Aura, uno de nuestros mods):

VERSION = 1,

option BatallaAura.Exertion
{
    type = double,              ← boolean, integer, double o enum (los que usan nuestros mods)
    min = 0.1,
    max = 5.0,
    default = 1.0,
    page = BatallaAura,         ← pestaña en el menú de sandbox
    translation = BatallaAura_Exertion,
}

Y se leen en Lua desde la tabla global SandboxVars:

lua
local mult = SandboxVars.BatallaAura.Exertion      -- módulo.opción

Reglas:

  • Lee siempre con valor por defecto: (SandboxVars.CursoLab and SandboxVars.CursoLab.Peso) or 10. Una partida creada antes de instalar tu mod no tiene tus opciones.
  • Léelas cuando las uses, no al cargar el archivo. Tus archivos de Lua cargan antes de que exista la partida.
  • En multijugador las opciones vienen del servidor: el cliente las recibe, pero quien decide es el servidor.

3. Traducciones (Build 42: JSON)

Desde B42 las traducciones son archivos JSON en media/lua/shared/Translate/<IDIOMA>/. El juego trae 29 idiomas y 44 archivos por idioma. Nuestros mods usan:

ArchivoClavesEjemplo
ItemName.jsonModulo.Item"CompleteItaliano.Vienesa": "Vienesa"
IG_UI.jsonIGUI_..."IGUI_PumpkinZ_Cuts": "Cortes: %1"
Sandbox.jsonSandbox_<pagina>, Sandbox_<translation>, Sandbox_<translation>_tooltip"Sandbox_BatallaAura_Exertion": "Desgaste al bailar"
Tooltip.jsonTooltip_...el valor de Tooltip = del ítem
Recipes.jsonnombre del craftRecipe

En Lua: getText("IGUI_PumpkinZ_Cuts", self.cuts) reemplaza %1 por el primer argumento (%2 el segundo...).

Cómo busca, según Translator.getTextInternal en el .jar:

  • Elige la tabla por el prefijo de la clave: IGUI_ → textos de interfaz, Sandbox_ → sandbox, Tooltip_, ContextMenu_, Moodles_, Farming_, UI_, Fluid_... Una clave sin un prefijo conocido (CursoLab_Hola) nunca se encuentra, esté en el archivo que esté. Usa siempre un prefijo del juego: IGUI_CursoLab_Hola.
  • Si no la encuentra, devuelve la clave misma, y en modo debug escribe ERROR: Missing translation "..." en la consola. Un IGUI_CursoLab_Hola en pantalla significa "falta la traducción", no "error de código".
  • <br> en el texto se convierte en salto de línea.

Los idiomas: EN es la base y el respaldo. El juego distingue ES y variantes (ES_CL, ES_MX), PT y PTBR. Nuestros mods traen EN, ES, PT y PTBR.


Ejercicios

Ejercicio 13.1 · Leer las distribuciones

Con _juego/lua/server/Items/, responde:

  1. ¿En qué contenedores puede aparecer la lupa (Base.MagnifyingGlass) y con qué peso? (El sitio wenopal.work lo muestra en su ficha: compara.)
  2. ¿Cuántas tiradas (rolls) tiene FridgeGeneric?
  3. Con la fórmula, ¿cuál es la probabilidad de que un refrigerador genérico tenga al menos una vienesa de Tocomples Food?
Pista para la 3

Si la probabilidad por tirada es p y hay r tiradas, la de que salga al menos una es 1 - (1 - p)^r. Con p = 0,5 y unas pocas tiradas, es casi segura. ¿Es eso lo que queríamos para Tocomples Food?

Ejercicio 13.2 · Loot configurable

Agrega la sopaipilla a FridgeGeneric y a BakeryMisc con un peso que sale de una opción de sandbox CursoLab.SopaipillaLoot (entero de 0 a 20, por defecto 4, 0 = no aparece).

Listo cuando: con 0 no aparece nunca, con 20 aparece casi siempre, y la opción sale traducida en español e inglés.

Ejercicio 13.3 · ¿Cuándo existe SandboxVars?

En el ejercicio anterior leíste la opción de sandbox en OnPostDistributionMerge, pero SandboxOptions.load() corre después de ese evento (sección 1). Comprueba qué valor ves ahí:

  1. en una partida nueva con la opción en 20,
  2. al cargar esa misma partida.

Imprime el valor en el evento y en OnFillContainer. Si no es el que esperas, cambia el diseño (pista: OnFillContainer corre durante la partida).

Listo cuando: puedes explicar con tus logs cuál de los dos lugares es confiable para leer la opción.

Ejercicio 13.4 · Loot con reglas

Con OnFillContainer: en las cocinas (roomName == "kitchen") de noche, un 10 % de probabilidad de agregar una sopaipilla recién hecha (cocida y caliente). Sincroniza el ítem agregado para que funcione en el servidor.

Ejercicio 13.5 · Cómo carga el traductor

Decompila zombie.core.Translator (módulo 08) y responde:

  1. getText busca por prefijo (sección 3). Pero al cargar, ¿el nombre del archivo JSON decide en qué tabla queda cada clave? ¿Funciona una clave IGUI_... dentro de Sandbox.json?
  2. Si una clave existe en EN pero no en ES, ¿qué ve un jugador en español?
  3. ¿Qué pasa si dos mods definen la misma clave?

Comprueba cada respuesta en el juego.

Desafío 13

Escribe pztrad.py: recorre un mod y avisa de (a) cada getText("...") del Lua cuya clave no está en EN, (b) cada clave de EN que falta en ES, PT o PTBR, y (c) cada clave de traducción que nadie usa. Pásalo por tus propios mods. (Es parecido a lo que hace nuestro sitio para detectar traducciones al portugués que faltan.)


Anterior: 12 · Multijugador · Siguiente: 14 · Datos persistentes y rendimiento