Objetivo: escribir acciones con barra de progreso (comer, tallar, reparar) que se animen bien, se puedan cancelar, se encadenen en la cola y funcionen en multijugador de B42.
1. Qué es una timed action
Todo lo que el personaje "hace durante un rato" es una timed action: una tabla de Lua derivada de ISBaseTimedAction (shared/TimedActions/ISBaseTimedAction.lua), con su mitad Java (zombie.characters.CharacterTimedActions.LuaTimedActionNew), igual que las ventanas del módulo 10. Las del juego están en shared/TimedActions/ (143 archivos) y client/TimedActions/ (21).
Se encolan, no se ejecutan:
ISTimedActionQueue.add(MiAccion:new(player, item))La cola (client/TimedActions/ISTimedActionQueue.lua) las corre una tras otra. Otras funciones útiles: addAfter(anterior, accion), clear(character), hasActionType(character, "ISEatFoodAction"), isPlayerDoingAction(player).
2. El ciclo de vida
Java llama a tus funciones por nombre en este orden:
new() ← tú, al crearla: guarda datos y calcula maxTime
│
isValidStart() ← ¿se puede empezar?
waitToStart() ← devuelve true mientras haya que esperar (por ejemplo, girarse hacia algo)
start() ← una vez: animación, sonido, modelos en las manos
update() ← cada tick: progreso, comprobaciones
isValid() ← cada tick: si devuelve false, se cancela
│
├── si se cancela (te mueves, te atacan, isValid falla) → stop()
│
└── si termina → perform() (cliente: limpieza visible)
complete() (el efecto real; ver sección 4)Campos de ISBaseTimedAction:new que puedes cambiar: stopOnWalk, stopOnRun, stopOnAim (todos true por defecto), caloriesModifier y maxTime. Java lee además, si existen, useProgressBar, forceProgressBar, loopedAction y allowedWhileDraggingCorpses (lo ves en el constructor de LuaTimedActionNew). Comer, por ejemplo, pone stopOnWalk = false: se puede comer caminando.
Pumpkin Z DIY completo, como referencia
En Pumpkin Z DIY, uno de nuestros mods, .../shared/TimedActions/PumpkinZCarveAction.lua es una acción B42 correcta y corta. Las piezas:
PumpkinZCarveAction = ISBaseTimedAction:derive("PumpkinZCarveAction")
function PumpkinZCarveAction:isValid() -- ¿todavía tengo la calabaza y el cuchillo?
local inv = self.character:getInventory()
return self.pumpkin and self.knife and inv:contains(self.pumpkin) and inv:contains(self.knife)
and PumpkinZ.isValidCarve(self.carve)
end
function PumpkinZCarveAction:update()
self.pumpkin:setJobDelta(self:getJobDelta()) -- la barrita sobre el ícono del ítem
end
function PumpkinZCarveAction:start()
self.pumpkin:setJobType(getText("IGUI_PumpkinZ_Carving"))
self:setActionAnim("Craft") -- animación del personaje
self:setOverrideHandModels(self.knife, self.pumpkin) -- qué tiene en cada mano
self.sound = self.character:playSound("SliceFood")
end
function PumpkinZCarveAction:stop() -- cancelada
self:stopSound()
self.pumpkin:setJobDelta(0.0)
ISBaseTimedAction.stop(self) -- ¡siempre llamar al padre!
end
function PumpkinZCarveAction:perform() -- terminó (lado visible)
self:stopSound()
self.pumpkin:setJobDelta(0.0)
ISBaseTimedAction.perform(self) -- ¡siempre! avisa a la cola que siga
end
function PumpkinZCarveAction:complete() -- terminó (el efecto)
... quita la calabaza, agrega la tallada, sincroniza ...
return true
end
function PumpkinZCarveAction:getDuration()
if self.character:isTimedActionInstant() then return 1 end -- modo debug "acciones instantáneas"
return math.min(1800, 90 + PumpkinZ.countCuts(self.carve) * 8)
end
function PumpkinZCarveAction:new(character, pumpkin, knife, carve)
local o = ISBaseTimedAction.new(self, character)
o.pumpkin, o.knife, o.carve = pumpkin, knife, carve
o.maxTime = o:getDuration()
return o
endOlvidar ISBaseTimedAction.perform(self) es el bug más común: la acción termina pero la cola queda trabada y el personaje no hace nada más. ISBaseTimedAction.perform es lo que llama a onCompleted en la cola.
3. La duración
maxTime no es en segundos: son unidades de tiempo de acción. Además, ISBaseTimedAction:create() la pasa por adjustMaxTime, que la alarga según el estado del personaje:
maxTime = maxTime * (1 + infeliz / 4) -- nivel del moodle de infelicidad
maxTime = maxTime * (1 + borracho / 4)
maxTime = maxTime * (1 + dolorDeManos / 300) -- salvo que pongas ignoreHandsWounds = true
maxTime = maxTime * character:getTimedActionTimeModifier() -- temperatura del cuerpoSi tu acción debe durar exactamente lo mismo siempre (por ejemplo, sincronizada con una animación), sobrescribe adjustMaxTime para que devuelva maxTime sin cambios. En Batalla de Aura, uno de nuestros mods, lo hacemos en DanceAction para los bailes.
4. Multijugador: perform vs complete
Este es el cambio grande de B42, y está a la vista en LuaTimedActionNew (decompílala como en el módulo 08):
public void complete() {
super.complete();
if (!GameClient.client) { // ← solo si NO soy un cliente
LuaManager.caller.pcall(LuaManager.thread, this.table.rawget("complete"), this.table);
}
}y en el constructor:
if (table.getMetatable().rawget("complete") == null) {
this.useCustomRemoteTimedActionSync = true; // ← sin complete(): modo antiguo
}En palabras:
| Dónde corre | perform() | complete() |
|---|---|---|
| Un jugador | sí | sí |
| Cliente de un servidor | sí | no |
| Servidor | — | sí |
- Si tu acción tiene
complete(), el cliente le pide la acción al servidor (Java maneja la transacción conActionManager) y el servidor ejecutacomplete(). El efecto va encomplete(): así lo decide el servidor y no se puede hacer trampa desde el cliente. - Para eso el servidor tiene que poder crear tu acción por nombre: la clase tiene que estar en
shared/. El comentario de Pumpkin Z DIY lo dice: "En shared: en multijugador B42 el servidor crea la accion por nombre y ejecuta complete()." - Si tu acción no tiene
complete(), funciona a la antigua (todo en el cliente, sin sincronizar). Sirve para acciones puramente locales; para cualquier cosa que cambie el mundo o el inventario, no. - Lo que hace
complete()tiene que avisarse a los clientes: Pumpkin Z DIY llamasendRemoveItemFromContainerysendAddItemToContainerdespués de tocar el inventario. El módulo 12 explica la sincronización. isValid()corre en los dos lados. Pumpkin Z DIY revalida el dibujo (isValidCarve) porque viene del cliente y no se confía en él.
5. Moverse antes de actuar
Para acciones sobre algo del mundo, primero hay que caminar hasta ahí. El juego encola primero un ISWalkToTimedAction (52 usos) o usa luautils.walkAdj(player, square) (281 usos), que camina a un cuadro adyacente libre, y después encola la acción:
if luautils.walkAdj(player, objeto:getSquare()) then
ISTimedActionQueue.add(MiAccion:new(player, objeto))
endEjercicios
Ejercicio 11.1 · Freír sopaipillas
Haz CursoLab.FreirAction: con una sartén y una sopaipilla cruda en el inventario, la fríes (resultado: sopaipilla cocida). Requisitos:
- animación y sonido mientras dura,
- la sartén se ve en la mano,
- se cancela si sueltas la sartén,
- la duración baja con tu nivel de cocina.
Listo cuando: funciona, se cancela bien, y después de cancelar puedes encolar otra cosa sin que el personaje quede trabado.
Ejercicio 11.2 · El bug de la cola
Quita ISBaseTimedAction.perform(self) de tu acción y encola dos frituras seguidas. Describe qué pasa. Después busca en ISTimedActionQueue.lua la función que no se llamó y explica la causa.
Ejercicio 11.3 · Sobre el mundo
Haz una acción "Golpear la puerta" sobre una puerta: el personaje camina hasta ella (luautils.walkAdj), golpea durante unos segundos y al final se emite un ruido que atrae zombis (busca cómo hace ruido el juego: addSound).
Ejercicio 11.4 · Mídelo
Con print en start y perform y getTimestampMs(), mide cuántos segundos reales dura tu acción con maxTime = 100, a velocidad normal y estando muy infeliz. Compáralo con la fórmula de adjustMaxTime.
Ejercicio 11.5 · Prepararla para multijugador
Revisa tu acción del 11.1 contra esta lista y corrígela:
- ¿Está en
shared/? - ¿El efecto está en
complete()y devuelvetrue? - ¿
isValid()comprueba todo lo que viene del cliente? - ¿Avisas los cambios de inventario (
sendAddItemToContainer,sendRemoveItemFromContainer)?
La prueba real la haces en el módulo 12 con un servidor local.
Desafío 11
Una acción en bucle: "Pescar hasta que pare" o "Picar leña hasta llenar el inventario" que se repite sola hasta que se cumpla una condición o el jugador la cancele. Busca cómo lo hace el juego (pista: en el constructor de LuaTimedActionNew se lee un campo loopedAction).
Anterior: 10 · Menús e interfaz · Siguiente: 12 · Multijugador