-- OT-MMO: PvP instanciado (vault OT-MMO/pvp/plan-pvp.md). Solo dibuja y pide: el servidor (server/scripts/pvp/cola.lua y -- arena.lua) decide colas, equipos, rating y puntos. Cuatro piezas: -- · Ventana PvP (H): pestañas Arenas / Campos de batalla / Temporada; saldos con tope semanal; la cola. opcode 133 -- «E|cab|modos|cola|ratings» · «M|texto» · cliente «P» · «B|modo» · «Q» -- · «Tu partida está lista»: cuenta atrás, tu equipo, Entrar / Salir de la cola. opcode 134 -- «L|modo^segundos^total^yoEntro^listos^jugadores|nombre^voc^A/E~...» · «X» · cliente «S» / «N» -- · HUD de arena: reloj y marcos de los rivales. opcode 135 -- «H|mapa^modo^fase^segundos^ceniza|aliados|rivales» (nombre^voc^vida%^maná%^vivo^carga~...) · «S» -- · Marcador final. opcode 136 -- «F|res^modo^mapa^duración^antes^después^puntos|equipo^nombre^voc^daño^cura^derribos^caído^cambio~...» · «X» · cliente «S» local OP_VENTANA, OP_LISTA, OP_HUD, OP_FIN = 133, 134, 135, 136 local C = OtmmoTheme.color local ORO = '#C7A86B' local BRASA = '#D99A45' local CLASE = { [1] = 'Hechicero', [2] = 'Druida', [3] = 'Cazador', [4] = 'Guardián', [11] = 'Brujo', [12] = 'Bardo', [13] = 'Gunslinger' } local LETRA = { [1] = 'H', [2] = 'D', [3] = 'C', [4] = 'G', [11] = 'B', [12] = 'T', [13] = 'P' } -- T: trovador (la B ya es del Brujo); P: pistolero (la G es del Guardián) local PESTANAS = { { 'A', 'Arenas' }, { 'C', 'Campos de batalla' }, { 'T', 'Temporada' } } local ELITE = 1800 pvpController = Controller:new() local ventana, lista, hud, fin = nil, nil, nil, nil local estado = nil -- último «E» local pestana, elegido = 'A', nil local colaDesde = nil local mensaje, mensajeHasta = nil, 0 local listaFin, listaTotal = nil, 40 local finHasta = nil local ciclo = nil local function enviar(op, texto) local protocol = g_game.getProtocolGame() if protocol then protocol:sendExtendedOpcode(op, texto) end end local function partir(s, sep) local t = {} for x in ((s or '') .. sep):gmatch('(.-)' .. sep:gsub('%p', '%%%0')) do t[#t + 1] = x end return t end local function claseDe(voc) voc = tonumber(voc) or 0 if CLASE[voc] then return voc end if voc == 12 then return 11 end if voc >= 5 and voc <= 8 then return voc - 4 end return 0 end local function reloj(s) s = math.max(0, math.floor(s)) return string.format('%d:%02d', math.floor(s / 60), s % 60) end local function miles(n) local s = tostring(math.floor(tonumber(n) or 0)) return (s:reverse():gsub('(%d%d%d)', '%1 '):reverse():gsub('^ ', '')) end local function duracion(s) local d, h, m = math.floor(s / 86400), math.floor(s % 86400 / 3600), math.floor(s % 3600 / 60) if d > 0 then return string.format('%d d %d h', d, h) end if h > 0 then return string.format('%d h %d min', h, m) end return string.format('%d min', math.max(1, m)) end local function escribiendo() local w = rootWidget while w:getFocusedChild() do w = w:getFocusedChild() end return w:getClassName() == 'UITextEdit' and w:isVisible() end -- Fundido con salida exponencial (ease-out quart): rápido al principio y se posa suave. Solo opacidad. local function fundir(w, desde, hasta, ms, alTerminar) if not w then return end if w.fundido then removeEvent(w.fundido) end local t0 = g_clock.millis() w:setOpacity(desde) local function paso() local t = math.min(1, (g_clock.millis() - t0) / ms) local e = 1 - (1 - t) ^ 4 w:setOpacity(desde + (hasta - desde) * e) if t < 1 then w.fundido = scheduleEvent(paso, 16) else w.fundido = nil if alTerminar then alTerminar() end end end paso() end local function mostrar(w, ms) if not w:isVisible() then w:show() fundir(w, 0, 1, ms or 220) end w:raise() end local function ocultar(w, ms) if w and w:isVisible() and not w.saliendo then w.saliendo = true fundir(w, w:getOpacity(), 0, ms or 160, function() w.saliendo = nil w:hide() w:setOpacity(1) end) end end -- Ventana ------------------------------------------------------------------------------------------------------------- local function modosDe(tipo) local l = {} for _, m in ipairs(estado and estado.modos or {}) do if m.tipo == tipo then l[#l + 1] = m end end table.sort(l, function(a, b) return a.tam < b.tam end) return l end local function ratingDe(id) return estado and estado.ratings[id] or { rating = 1500, jugadas = 0, ganadas = 0, mejor = 1500 } end local function pintarSaldo(w, etiqueta, saldo, ganado, tope) w.etiqueta:setText(etiqueta) w.cifra:setText(miles(saldo)) w.tope:setText(string.format('semana %s / %s', miles(ganado), miles(tope))) w.tope:setColor(ganado >= tope and BRASA or C.text2) local ancho = w.raya:getWidth() w.raya.fill:setWidth(math.max(0, math.floor(ancho * math.min(1, ganado / math.max(1, tope))))) w:setTooltip(ganado >= tope and 'Llegaste al tope de esta semana: lo que ganes ya no suma hasta el lunes.' or string.format('Puedes ganar %s más esta semana.', miles(tope - ganado))) end local function filaModo(m) local w = g_ui.createWidget('PvpModo', ventana.lista) w.modo = m w.nombre:setText(m.nombre) local enCola = estado.cola and estado.cola.modo == m.id if m.tipo == 'A' then local r = ratingDe(m.id) w.meta:setText(string.format('%d contra %d · nivel 100 · solo o con tu grupo', m.tam, m.tam)) w.rating:setText(tostring(r.rating)) local perdidas = r.jugadas - r.ganadas w.registro:setText(r.jugadas > 0 and string.format('%d V %d D', r.ganadas, perdidas) or 'sin partidas') else w.meta:setText(string.format('%d contra %d · por tramos de 10 niveles', m.tam, m.tam)) w.rating:setText('') w.registro:setText('') end if m.motivo ~= '' then w.cola:setText(m.motivo) w.cola:setColor(m.tipo == 'C' and C.text2 or C.errorText) w:setEnabled(false) elseif enCola then w.cola:setText('En la cola') w.cola:setColor(ORO) else w.cola:setText(m.enCola > 0 and string.format('%d en cola', m.enCola) or '') w.cola:setColor(C.text2) end w:setChecked(enCola or (not estado.cola and elegido == m.id)) w.onClick = function() if estado.cola or m.motivo ~= '' then return end elegido = m.id for _, x in ipairs(ventana.lista:getChildren()) do x:setChecked(x.modo and x.modo.id == elegido) end modules.game_pvp.pintarPie() end return w end local function pintarTemporada() ventana.nota:setText(string.format('Temporada %d · ranking de cada modalidad en la web de VALDREN', estado.temporada)) local cab = g_ui.createWidget('PvpFilaTemporada', ventana.lista) for _, k in ipairs({ 'modo', 'rating', 'mejor', 'partidas', 'victorias' }) do cab[k]:setColor(C.text2) end cab.modo:setText('Modalidad') cab.rating:setText('Rating') cab.mejor:setText('Mejor') cab.partidas:setText('Partidas') cab.victorias:setText('Victorias') local mejorRating = 0 for _, m in ipairs(modosDe('A')) do local r = ratingDe(m.id) local f = g_ui.createWidget('PvpFilaTemporada', ventana.lista) f.modo:setText(m.nombre) f.rating:setText(tostring(r.rating)) f.rating:setColor(r.rating >= ELITE and ORO or C.text) f.mejor:setText(tostring(r.mejor)) f.partidas:setText(tostring(r.jugadas)) f.victorias:setText(r.jugadas > 0 and string.format('%d %%', math.floor(100 * r.ganadas / r.jugadas + 0.5)) or '-') mejorRating = math.max(mejorRating, r.rating) end local elite = g_ui.createWidget('GameParagraph', ventana.lista) elite:setHeight(64) elite:setMarginTop(10) local falta = ELITE - mejorRating elite:setText((falta > 0 and string.format('Equipo élite: rating %s en cualquier modalidad (te faltan %d).', miles(ELITE), falta) or 'Ya tienes rating para el equipo élite.') .. '\nAl cerrar la temporada, los mejores de cada modalidad reciben su título y la montura de temporada.' .. '\nEl equipo PvP solo es mejor contra jugadores: contra monstruos rinde como una pieza de 10 índices menos.') end function pintarPie() if not ventana or not estado then return end local c = estado.cola local aviso = mensaje and g_clock.seconds() < mensajeHasta ventana.principal:setVisible(c == nil) ventana.salirCola:setVisible(c ~= nil) if estado.desertor > 0 then ventana.estado:setText('Desertor: ' .. reloj(estado.desertor - (g_clock.seconds() - estado.recibido))) ventana.estado:setColor(C.errorText) ventana.detalle:setText('Abandonaste una partida. Hasta entonces no puedes volver a la cola.') ventana.principal:setEnabled(false) return end if c then local m for _, x in ipairs(estado.modos) do if x.id == c.modo then m = x end end ventana.estado:setText(string.format('Buscando %s · %s', m and m.nombre or c.modo, reloj(g_clock.seconds() - colaDesde))) ventana.estado:setColor(C.text) ventana.detalle:setText(aviso and mensaje or (c.enGrupo and (c.esLider and 'Con tu grupo. ' or 'Te apuntó tu líder. ') or '') .. string.format('Emparejando con rivales de ±%d de MMR; se abre con la espera.', c.ventana)) ventana.detalle:setColor(aviso and BRASA or C.text2) return end local m for _, x in ipairs(estado.modos) do if x.id == elegido then m = x end end local puede = m and m.motivo == '' and pestana ~= 'T' ventana.principal:setEnabled(puede == true) ventana.estado:setColor(C.text) if aviso then ventana.estado:setText(mensaje) ventana.estado:setColor(C.errorText) ventana.detalle:setText('') elseif pestana == 'T' then ventana.estado:setText('Fuera de la cola') ventana.detalle:setText('Elige una arena en su pestaña para buscar partida.') elseif puede then ventana.estado:setText(m.nombre) ventana.detalle:setText('Sin pérdida al morir: caes como espectador hasta el final de la ronda.') else ventana.estado:setText(pestana == 'C' and 'Campos de batalla en preparación' or 'Elige una modalidad') ventana.detalle:setText(pestana == 'C' and 'Se abrirán por tramos de nivel (20-29 ... 90-99 y 100).' or '') end ventana.detalle:setColor(C.text2) end local function pintar() if not ventana or not estado then return end ventana.temporada:setText(string.format('Temporada %d · semana %d de %d · el tope semanal se renueva en %s', estado.temporada, estado.semana, estado.semanas, duracion(estado.renueva))) pintarSaldo(ventana.saldoArena, 'Puntos de arena', estado.arena, estado.ganadoA, estado.topeA) pintarSaldo(ventana.saldoBatalla, 'Puntos de batalla', estado.batalla, estado.ganadoB, estado.topeB) ventana.atributos:setText(string.format('Templanza %d Ferocidad %d', estado.templanza, estado.ferocidad)) ventana.atributosNota:setText(string.format('-%d %% daño recibido · +%d %% daño hecho', estado.templanza, estado.ferocidad)) for _, tab in ipairs(ventana.pestanas:getChildren()) do tab:setChecked(tab.clave == pestana) end ventana.lista:destroyChildren() if pestana == 'T' then pintarTemporada() else local modos = modosDe(pestana) if pestana == 'A' then ventana.nota:setText('Gana el último equipo en pie · al minuto 8 la ceniza empieza a quemar a todos') else ventana.nota:setText('Bandos al azar, equilibrados por rol y equipo · estandartes del lore solo como estética') end if not elegido and pestana == 'A' then for _, m in ipairs(modos) do if m.motivo == '' and not elegido then elegido = m.id end end end for i, m in ipairs(modos) do local w = filaModo(m) w:setOpacity(0) scheduleEvent(function() if not w:isDestroyed() then fundir(w, 0, w:isEnabled() and 1 or 0.85, 200) end end, (i - 1) * 40) end end pintarPie() end local function cambiarPestana(clave) pestana = clave pintar() end function toggle() if not ventana then return end if ventana:isVisible() then return ocultar(ventana) end enviar(OP_VENTANA, 'P') pintar() mostrar(ventana) ventana:focus() end function principal() if elegido then enviar(OP_VENTANA, 'B|' .. elegido) end end function salirCola() enviar(OP_VENTANA, 'Q') end local function leerEstado(p) local cab = partir(p[1], '^') local n = function(i) return tonumber(cab[i]) or 0 end local e = { batalla = n(1), ganadoB = n(2), topeB = n(3), arena = n(4), ganadoA = n(5), topeA = n(6), desertor = n(7), templanza = n(8), ferocidad = n(9), temporada = n(10), semana = n(11), semanas = n(12), renueva = n(13), nivel = n(14), modos = {}, ratings = {}, recibido = g_clock.seconds() } for _, l in ipairs(partir(p[2], '~')) do local m = partir(l, '^') if m[1] ~= '' then e.modos[#e.modos + 1] = { id = m[1], nombre = m[2], tipo = m[3], tam = tonumber(m[4]) or 0, motivo = m[5] or '', enCola = tonumber(m[6]) or 0 } end end if p[3] and p[3] ~= '' then local c = partir(p[3], '^') e.cola = { modo = c[1], enGrupo = c[3] == '1', esLider = c[4] == '1', ventana = tonumber(c[5]) or 0 } colaDesde = g_clock.seconds() - (tonumber(c[2]) or 0) end for _, l in ipairs(partir(p[4], '~')) do local r = partir(l, '^') if r[1] ~= '' then e.ratings[r[1]] = { rating = tonumber(r[2]) or 0, jugadas = tonumber(r[3]) or 0, ganadas = tonumber(r[4]) or 0, mejor = tonumber(r[5]) or 0 } end end return e end -- Tu partida está lista --------------------------------------------------------------------------------------------- local function pintarLista(cab, miembros) lista.modo:setText(cab[1] or '') local seg, total = tonumber(cab[2]) or 40, tonumber(cab[3]) or 40 local yo, listos, jugadores = cab[4] == '1', tonumber(cab[5]) or 0, tonumber(cab[6]) or 0 listaTotal = total listaFin = g_clock.millis() + seg * 1000 lista.listos:setText(string.format('%d de %d dentro', listos, jugadores)) lista.miembros:destroyChildren() for _, l in ipairs(partir(miembros, '~')) do local m = partir(l, '^') if m[1] ~= '' then local w = g_ui.createWidget('PvpListo', lista.miembros) local entra = m[3] == 'A' w.nombre:setText(m[1]) w.estado:setText((CLASE[claseDe(m[2])] or '') .. (entra and ' · dentro' or ' · esperando')) w.estado:setColor(entra and ORO or C.text2) w.punto:setBackgroundColor(entra and ORO or C.borderHover) end end lista.aceptar:setEnabled(not yo) lista.aceptar:setText(yo and 'Dentro' or 'Entrar') lista.rechazar:setEnabled(not yo) lista.pie:setText(yo and 'Esperando al resto...' or 'Salir de la cola no tiene castigo.') if not lista:isVisible() then if ventana then ocultar(ventana) end mostrar(lista, 260) lista:focus() end end local function pintarCuenta() if not (lista and lista:isVisible() and listaFin) then return end local resta = math.max(0, listaFin - g_clock.millis()) lista.numero:setText(tostring(math.ceil(resta / 1000))) local urgente = resta <= 10000 lista.numero:setColor(urgente and C.errorText or ORO) lista.fondoBarra.fill:setBackgroundColor(urgente and C.hp or ORO) lista.fondoBarra.fill:setWidth(math.floor(lista.fondoBarra:getWidth() * resta / (listaTotal * 1000))) end function responder(si) enviar(OP_LISTA, si and 'S' or 'N') end -- HUD de arena ------------------------------------------------------------------------------------------------------- local function filas(texto) local l = {} for _, x in ipairs(partir(texto, '~')) do local m = partir(x, '^') if m[1] ~= '' then l[#l + 1] = { nombre = m[1], voc = claseDe(m[2]), vida = tonumber(m[3]) or 0, mana = tonumber(m[4]) or 0, vivo = m[5] == '1', carga = m[6] or '' } end end return l end local function pintarHud(p) local cab = partir(p[1], '^') local fase, seg, ceniza = cab[3], tonumber(cab[4]) or 0, cab[5] == '1' local mios, suyos = filas(p[2]), filas(p[3]) local function vivos(l) local n = 0 for _, x in ipairs(l) do if x.vivo then n = n + 1 end end return n end hud.reloj.aliados:setText(tostring(vivos(mios))) hud.reloj.rivales:setText(tostring(vivos(suyos))) if fase == 'P' then hud.reloj.fase:setText('Preparación') hud.reloj:setTooltip((cab[2] or '') .. ' · ' .. (cab[1] or '')) hud.reloj.fase:setColor(C.text2) hud.reloj.tiempo:setText(reloj(seg)) hud.reloj.tiempo:setColor(seg <= 5 and ORO or C.text) elseif fase == 'C' then hud.reloj.fase:setText(ceniza and 'Ceniza que asfixia' or 'Combate') hud.reloj:setTooltip((cab[2] or '') .. ' · ' .. (cab[1] or '')) hud.reloj.fase:setColor(ceniza and BRASA or C.text2) hud.reloj.tiempo:setText(reloj(seg)) hud.reloj.tiempo:setColor(ceniza and BRASA or C.text) else hud.reloj.fase:setText('Fin de la partida') hud.reloj.fase:setColor(C.text2) hud.reloj.tiempo:setColor(C.text2) end hud.marcos:setVisible(fase ~= 'F') hud.titulo:setVisible(fase ~= 'F') hud.reloj:setBorderColor(ceniza and fase == 'C' and '#D99A4580' or C.border) local marcos = hud.marcos:getChildren() for i, r in ipairs(suyos) do local w = marcos[i] or g_ui.createWidget('PvpMarco', hud.marcos) w.clase:setText(LETRA[r.voc] or '?') w.clase:setTooltip(CLASE[r.voc] or '') w.nombre:setText(r.nombre) local ancho = w.vida:getWidth() w.vida.fill:setWidth(math.floor(ancho * r.vida / 100)) w.mana.fill:setWidth(math.floor(ancho * r.mana / 100)) w.pct:setText(r.vivo and (r.vida .. ' %') or 'caído') w.carga:setText(r.carga ~= '' and ('Lanza: ' .. r.carga) or '') w:setOpacity(r.vivo and 1 or 0.4) w:setBorderColor(r.vivo and r.vida <= 30 and '#C9424299' or C.border) end for i = #suyos + 1, #marcos do marcos[i]:destroy() end hud.marcos:setHeight(#suyos * 52) if not hud:isVisible() then mostrar(hud, 300) end end -- Marcador final ---------------------------------------------------------------------------------------------------- local RESULTADO = { V = { 'Victoria', ORO }, D = { 'Derrota', C.errorText }, E = { 'Empate', C.text } } local function pintarFin(p) local cab = partir(p[1], '^') local r = RESULTADO[cab[1]] or RESULTADO.E fin.resultado:setText(r[1]) if modules.game_viaje and modules.game_viaje.ponerArte then modules.game_viaje.ponerArte(fin.arte, cab[3] or '', 54, 53) end fin.resultado:setColor(r[2]) fin.meta:setText(string.format('%s · %s · %s', cab[2] or '', cab[3] or '', reloj(tonumber(cab[4]) or 0))) local antes, despues, puntos = tonumber(cab[5]) or 0, tonumber(cab[6]) or 0, tonumber(cab[7]) or 0 local d = despues - antes fin.rating:setText(string.format('%d %s%d', despues, d >= 0 and '+' or '', d)) fin.rating:setColor(d > 0 and ORO or d < 0 and C.errorText or C.text) fin.puntos:setText(puntos > 0 and string.format('+%d Puntos de arena', puntos) or 'Tope semanal de puntos alcanzado') fin.filas:destroyChildren() local lados = { A = {}, B = {} } for _, x in ipairs(partir(p[2], '~')) do local m = partir(x, '^') if m[1] ~= '' then table.insert(lados[m[1]] or lados.B, m) end end local n, yo = 0, g_game.getCharacterName() for _, lado in ipairs({ 'A', 'B' }) do table.sort(lados[lado], function(a, b) return (tonumber(a[4]) or 0) > (tonumber(b[4]) or 0) end) local t = g_ui.createWidget('GameSection', fin.filas) t:setText(lado == 'A' and 'Tu equipo' or 'Rivales') t:setHeight(22) t:setMarginTop(lado == 'B' and 8 or 2) t:setColor(lado == 'A' and ORO or C.errorText) for _, m in ipairs(lados[lado]) do local w = g_ui.createWidget('PvpFilaFin', fin.filas) w.nombre:setText(m[2]) if m[2] == yo then w:setBackgroundColor('#C7A86B14') end w.clase:setText(CLASE[claseDe(m[3])] or '') w.danio:setText(miles(m[4])) w.cura:setText(miles(m[5])) w.derribos:setText(m[6]) local c = tonumber(m[8]) or 0 w.rating:setText(string.format('%s%d', c >= 0 and '+' or '', c)) w.rating:setColor(c > 0 and ORO or c < 0 and C.errorText or C.text2) if m[7] == '1' then w.nombre:setColor(C.text2) end n = n + 1 w:setOpacity(0) scheduleEvent(function() if not w:isDestroyed() then fundir(w, 0, 1, 240) end end, 200 + n * 60) end end local alto = n * 26 + 2 * 22 + 12 fin.filas:setHeight(alto) fin:setHeight(150 + alto + 60 + 108) -- cabecera + ilustración + tabla + pie: la ventana se ajusta a 2c2, 3c3 o 4c4 finHasta = g_clock.seconds() + 45 mostrar(fin, 320) end function salirArena() enviar(OP_FIN, 'S') ocultar(fin) end -- Opcodes ------------------------------------------------------------------------------------------------------------ local function onVentana(protocol, opcode, buffer) local tipo = buffer:sub(1, 1) if tipo == 'M' then mensaje, mensajeHasta = buffer:sub(3), g_clock.seconds() + 6 return pintarPie() elseif tipo == 'E' then local antes = estado and estado.cola estado = leerEstado(partir(buffer:sub(3), '|')) if not estado.cola then colaDesde = nil end if ventana and ventana:isVisible() then pintar() end end end local function onLista(protocol, opcode, buffer) if buffer:sub(1, 1) == 'X' then listaFin = nil return ocultar(lista) end local p = partir(buffer:sub(3), '|') pintarLista(partir(p[1], '^'), p[2]) pintarCuenta() end local function onHud(protocol, opcode, buffer) if buffer:sub(1, 1) == 'S' then return ocultar(hud, 300) end pintarHud(partir(buffer:sub(3), '|')) end local function onFin(protocol, opcode, buffer) if buffer:sub(1, 1) == 'X' then return ocultar(fin) end pintarFin(partir(buffer:sub(3), '|')) end -- Para probar sin servidor: modules.game_pvp.probar(133, 'E|...') function probar(op, texto) local f = ({ [OP_VENTANA] = onVentana, [OP_LISTA] = onLista, [OP_HUD] = onHud, [OP_FIN] = onFin })[op] if f then f(nil, op, texto) end end function verPestana(clave) cambiarPestana(clave) end local function tic() pintarCuenta() if mensaje and g_clock.seconds() >= mensajeHasta then mensaje = nil end if ventana and ventana:isVisible() and estado then pintarPie() end if fin and fin:isVisible() and finHasta then fin.pie:setText(string.format('Vuelves a donde estabas en %s', reloj(finHasta - g_clock.seconds()))) end end function pvpController:onInit() g_ui.importStyle('/game_viaje/valdren') -- marco y estilos VALDREN (modules/game_viaje/valdren.otui) g_ui.importStyle('estilos') ventana = g_ui.displayUI('pvp') if OtmmoVentanas then OtmmoVentanas.recordar(ventana, 'pvp') end ventana:hide() for _, t in ipairs(PESTANAS) do local tab = g_ui.createWidget('GameTab', ventana.pestanas) tab.clave = t[1] tab:setText(t[2]) tab.onClick = function() cambiarPestana(t[1]) end end lista = g_ui.displayUI('lista') lista:hide() local raiz = modules.game_interface.getRootPanel() hud = g_ui.loadUI('hud', raiz) hud:hide() fin = g_ui.displayUI('fin') fin:hide() ProtocolGame.registerExtendedOpcode(OP_VENTANA, onVentana) ProtocolGame.registerExtendedOpcode(OP_LISTA, onLista) ProtocolGame.registerExtendedOpcode(OP_HUD, onHud) ProtocolGame.registerExtendedOpcode(OP_FIN, onFin) Keybind.new('Ventanas', 'Jugador contra jugador', { [CHAT_MODE.ON] = '', [CHAT_MODE.OFF] = 'H' }, '') Keybind.bind('Ventanas', 'Jugador contra jugador', { { type = KEY_DOWN, callback = function() if not escribiendo() then toggle() end end } }) ciclo = cycleEvent(tic, 250) if g_game.isOnline() then self:onGameStart() end end function pvpController:onGameStart() if ciclo then removeEvent(ciclo) end ciclo = cycleEvent(tic, 250) enviar(OP_VENTANA, 'P') end function pvpController:onGameEnd() if ciclo then removeEvent(ciclo) ciclo = nil end estado, colaDesde, listaFin = nil, nil, nil for _, w in ipairs({ ventana, lista, hud, fin }) do if w then w:hide() end end end function pvpController:onTerminate() self:onGameEnd() for _, op in ipairs({ OP_VENTANA, OP_LISTA, OP_HUD, OP_FIN }) do ProtocolGame.unregisterExtendedOpcode(op) end Keybind.delete('Ventanas', 'Jugador contra jugador') for _, w in ipairs({ ventana, lista, hud, fin }) do if w then w:destroy() end end ventana, lista, hud, fin = nil, nil, nil, nil end