-- OT-MMO: viaje rápido de la Liga del Puerto (servidor: server/scripts/viaje.lua, opcode 132). Solo dibuja y pide: el -- servidor decide qué está descubierto, el coste y si se puede viajar ahora. -- servidor -> «A|agente^origen^oro^descubiertos^total|nombre^region^niveles^coste^estado^medio^x^y~...» abre la carta -- (estado A aquí / D descubierto / S sin descubrir; medio M mar / C camino / X camino y mar; x, y = llegada) -- «M|texto» · «T|destino^ms^medio^region» escena de viaje · «L|destino^region» llegada · «C» cancela -- «D|nombre^region» destino descubierto · «X» cierra la carta -- cliente -> «V|n» viajar al destino n · «P» pedir la lista -- Sin la ventana el viaje también va por palabras clave con el agente («destinos», el nombre del sitio, «sí»). -- Arte: images/ (PixelLab, ver CREDITOS.md). La carta es de 240 x 272 y se dibuja al doble; la escena de viaje es de -- 300 x 170 al doble. Las capas se mueven cambiando el recorte (image-clip) de tiras triples, nunca el layout. local OPCODE = 132 local ORO, CLARO, TEXTO, TEXTO2, APAGADO, ERROR = '#C7A86B', '#E8D3A2', '#E2E5E6', '#9AA7AE', '#9AA7AE80', '#DC6B6B' local MEDIO = { M = 'Por mar', C = 'Por camino', X = 'Camino y mar' } local IMG = '/modules/game_viaje/images/' -- se corrige en onInit con la ruta real de la carta -- Mundo -> carta: la carta cubre x 40-2620, y 200-3100 en 240 x 272 píxeles (tools: viaje/mascara2.py), al doble. local function aCarta(x, y) return math.floor((x - 40) / 10.75 * 2 + 0.5), math.floor((y - 200) / 10.6618 * 2 + 0.5) end -- Escenas: medidas en píxeles de pantalla (arte al doble). tira = ancho de un tramo de la tira triple (arte). local ESCENA = { mar = { fondo = 'mar-fondo', frente = 'olas', frenteAlto = 64, tira = 272, sprite = 'barco', x = 150, y = 120, nubesImg = 'nubes-tarde', nubesOp = 0.75, nubes = 10, vel = 54, pan = 20 }, camino = { fondo = 'camino-fondo', frente = 'camino', frenteAlto = 56, tira = 272, sprite = 'carro', x = 150, y = 126, nubesImg = 'nubes-dia', nubesOp = 0.5, nubes = 6, vel = 70, pan = 20 }, } local NUBES_TIRA, FOTOGRAMAS, FPS = 256, 9, 10 viajeController = Controller:new() local ventana, fundido, cartel = nil, nil, nil local datos = nil -- último «A» local elegido = nil -- índice del destino elegido local marcas, puntos, filas = {}, {}, {} local anillo, anilloAqui, rotulo = nil, nil, nil local mensaje, mensajeHasta = nil, 0 local seguro, animCartel, ciclo, escenaEv, rutaEv, portalEv = nil, nil, nil, nil, nil, nil local function enviar(texto) local protocol = g_game.getProtocolGame() if protocol then protocol:sendExtendedOpcode(OPCODE, 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 miles(n) local s = tostring(math.floor(tonumber(n) or 0)) return (s:reverse():gsub('(%d%d%d)', '%1 '):reverse():gsub('^ ', '')) end -- Fundido con salida exponencial (ease-out quart). 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() if w:isDestroyed() then return end 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 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 -- Carta --------------------------------------------------------------------------------------------------------------- local function borrarRuta() if rutaEv then removeEvent(rutaEv) rutaEv = nil end for _, p in ipairs(puntos) do p:destroy() end puntos = {} end -- Ruta en oro de origen a destino: curva suave (Bézier cuadrática) con puntos que aparecen uno tras otro. local function trazarRuta(a, b) borrarRuta() if not (a and b) then return end local ax, ay = aCarta(a.x, a.y) local bx, by = aCarta(b.x, b.y) local dx, dy = bx - ax, by - ay local largo = math.sqrt(dx * dx + dy * dy) if largo < 8 then return end local cx, cy = (ax + bx) / 2 - dy * 0.18, (ay + by) / 2 + dx * 0.18 local n = math.max(3, math.floor(largo / 13)) local i = 0 local function siguiente() rutaEv = nil i = i + 1 if i >= n or not ventana then return end local t = i / n local x = (1 - t) ^ 2 * ax + 2 * (1 - t) * t * cx + t ^ 2 * bx local y = (1 - t) ^ 2 * ay + 2 * (1 - t) * t * cy + t ^ 2 * by local p = g_ui.createWidget('ViajePunto', ventana.carta) p:addAnchor(AnchorTop, 'parent', AnchorTop) p:addAnchor(AnchorLeft, 'parent', AnchorLeft) p:setMarginLeft(math.floor(x) - 3) p:setMarginTop(math.floor(y) - 3) puntos[#puntos + 1] = p for _, m in pairs(marcas) do m:raise() end if anillo then anillo:raise() end rutaEv = scheduleEvent(siguiente, 14) end siguiente() end local function ponerEn(w, d, dx, dy) local x, y = aCarta(d.x, d.y) w:addAnchor(AnchorTop, 'parent', AnchorTop) w:addAnchor(AnchorLeft, 'parent', AnchorLeft) w:setMarginLeft(x + dx) w:setMarginTop(y + dy) end function pintarPlaca() if not ventana or not datos then return end local d = elegido and datos.destinos[elegido] local origen = datos.destinos[datos.origen] local aviso = mensaje and g_clock.seconds() < mensajeHasta ventana.aviso:setText(aviso and mensaje or '') if d and d.estado == 'D' then ventana.nombre:setText(d.nombre) ventana.region:setText(d.region) ventana.meta:setText(string.format('Niveles %s · %s', d.niveles, MEDIO[d.medio] or '')) ventana.coste:setText(miles(d.coste)) ventana.coste:setColor(d.coste > datos.oro and ERROR or TEXTO) ventana.monedas:setText('de oro, al llegar') if d.coste > datos.oro then ventana.llevas:setText(string.format('Llevas %s: te faltan %s.', miles(datos.oro), miles(d.coste - datos.oro))) ventana.llevas:setColor(ERROR) else ventana.llevas:setText('Llevas ' .. miles(datos.oro)) ventana.llevas:setColor(TEXTO2) end ventana.viajar:setEnabled(d.coste <= datos.oro) ventana.viajar:setText('Viajar a ' .. d.nombre) else ventana.nombre:setText(datos.descubiertos <= 1 and 'Aún no hay rutas' or 'Elige un faro') ventana.region:setText(datos.descubiertos <= 1 and 'Solo conoces ' .. (origen and origen.nombre or 'este puerto') .. '.' or 'Los encendidos son sitios que ya pisaste.') ventana.meta:setText(datos.descubiertos <= 1 and 'Llega a pie a otro poblado con posada y su faro se encenderá.' or 'Los que están en sombra, aún no.') ventana.coste:setText('') ventana.monedas:setText('') ventana.llevas:setText('Llevas ' .. miles(datos.oro) .. ' de oro') ventana.llevas:setColor(TEXTO2) ventana.viajar:setEnabled(false) ventana.viajar:setText('Viajar') end end local function elegirDestino(i) local d = datos and datos.destinos[i] if not d or d.estado ~= 'D' then return end elegido = i mensaje = nil if anillo then anillo:show() anillo:breakAnchors() ponerEn(anillo, d, -22, -26) anillo:raise() end if rotulo then rotulo:setText(d.nombre) rotulo:show() rotulo:breakAnchors() ponerEn(rotulo, d, 14, -40) rotulo:raise() end for j, f in pairs(filas) do local dj = datos.destinos[j] if dj and dj.estado == 'D' then f.nombre:setColor(j == i and CLARO or TEXTO) f.punto:setBackgroundColor(j == i and CLARO or ORO) f.punto:setSize({ width = j == i and 7 or 5, height = j == i and 7 or 5 }) f.coste:setColor(dj.coste > datos.oro and ERROR or (j == i and CLARO or TEXTO)) end end trazarRuta(datos.destinos[datos.origen], d) pintarPlaca() end local function pintar() if not ventana or not datos then return end local origen = datos.destinos[datos.origen] ventana.desde:setText('Desde ' .. (origen and origen.nombre or '?')) ventana.liga:setText('Liga del Puerto · ' .. datos.agente) ventana.cuenta:setText(string.format('%d / %d', datos.descubiertos, datos.total)) if elegido and not (datos.destinos[elegido] and datos.destinos[elegido].estado == 'D') then elegido = nil end borrarRuta() for _, m in pairs(marcas) do m:destroy() end marcas = {} for _, w in ipairs({ anillo, anilloAqui, rotulo }) do if w then w:destroy() end end anilloAqui = g_ui.createWidget('ViajeAnillo', ventana.carta) anilloAqui:setOpacity(0.45) anillo = g_ui.createWidget('ViajeAnillo', ventana.carta) anillo:hide() for i, d in ipairs(datos.destinos) do local m = g_ui.createWidget('ViajeMarca', ventana.carta) ponerEn(m, d, -16, -30) if d.estado == 'S' then m:setImageSource(IMG .. 'marca-sombra') m:setEnabled(false) m:setTooltip(d.nombre .. ' · sin descubrir\n' .. d.region .. ', niveles ' .. d.niveles) elseif d.estado == 'A' then m:setTooltip(d.nombre .. ' · estás aquí') ponerEn(anilloAqui, d, -22, -26) m.onClick = function() end else m:setTooltip(string.format('%s · %s de oro', d.nombre, miles(d.coste))) m.onClick = function() elegirDestino(i) end end marcas[i] = m end rotulo = g_ui.createWidget('ViajeRotulo', ventana.carta) rotulo:hide() ventana.lista:destroyChildren() filas = {} for i, d in ipairs(datos.destinos) do local f = g_ui.createWidget('ViajeDestino', ventana.lista) f.indice = i filas[i] = f f.nombre:setText(d.nombre) if d.estado == 'S' then f.nombre:setColor(APAGADO) f.punto:setBackgroundColor('#39444C') f.coste:setText('sin descubrir') pcall(function() f.coste:setTTFFont('geist', 11, 0, '#00000000') end) f.coste:setColor(APAGADO) f:setEnabled(false) elseif d.estado == 'A' then f.nombre:setColor(ORO) f.coste:setText('aquí') pcall(function() f.coste:setTTFFont('geist', 11, 0, '#00000000') end) f.coste:setColor(ORO) f:setPhantom(true) else f.coste:setText(miles(d.coste)) f.coste:setColor(d.coste > datos.oro and ERROR or TEXTO) f.onClick = function() elegirDestino(i) end f.onDoubleClick = function() elegirDestino(i) viajar() end end end if elegido then elegirDestino(elegido) else pintarPlaca() end end local function leer(buffer) local p = partir(buffer:sub(3), '|') local cab = partir(p[1], '^') local e = { agente = cab[1] or '', origen = tonumber(cab[2]) or 0, oro = tonumber(cab[3]) or 0, descubiertos = tonumber(cab[4]) or 0, total = tonumber(cab[5]) or 0, destinos = {} } for _, l in ipairs(partir(p[2], '~')) do local m = partir(l, '^') if m[1] ~= '' then e.destinos[#e.destinos + 1] = { nombre = m[1], region = m[2] or '', niveles = m[3] or '', coste = tonumber(m[4]) or 0, estado = m[5] or 'S', medio = m[6] or '', x = tonumber(m[7]) or 0, y = tonumber(m[8]) or 0 } end end return e end local function abrir() if not ventana:isVisible() then ventana:show() fundir(ventana, 0, 1, 220) end ventana:raise() ventana:focus() end function cerrar() ocultar(ventana) end function viajar() if not datos or not elegido then return end local d = datos.destinos[elegido] if d and d.estado == 'D' then enviar('V|' .. elegido) end end -- Escena de viaje ----------------------------------------------------------------------------------------------------- local function pararEscena() if escenaEv then removeEvent(escenaEv) escenaEv = nil end end local function quitarFundido(ms) if seguro then removeEvent(seguro) seguro = nil end ocultar(fundido, ms or 500) scheduleEvent(function() if fundido and not fundido:isVisible() then pararEscena() end end, (ms or 500) + 50) end local function cuadroEscena(e, t) local v = fundido.cuadro.vista local s = t / 1000 v.fondo:setImageClip({ x = math.min(e.pan, math.floor(s * e.pan / 3.2)), y = 5, width = 300, height = 170 }) v.nubes:setImageClip({ x = math.floor(s * e.nubes) % NUBES_TIRA, y = 0, width = 300, height = 64 }) v.frente:setImageClip({ x = math.floor(s * e.vel) % e.tira, y = 0, width = 300, height = e.frenteAlto }) local f = math.floor(s * FPS) % FOTOGRAMAS v.sprite:setImageClip({ x = f * 128, y = 0, width = 128, height = 96 }) end local function ponerEscena(destino, ms, medio, region) if not fundido then return end local e = ESCENA[medio == 'C' and 'camino' or 'mar'] local v = fundido.cuadro.vista v.fondo:setImageSource(IMG .. e.fondo) v.nubes:setImageSource(IMG .. e.nubesImg) v.nubes:setOpacity(e.nubesOp) v.frente:setImageSource(IMG .. e.frente) v.frente:setHeight(e.frenteAlto * 2) v.sprite:setImageSource(IMG .. e.sprite) v.sprite:setSize({ width = 256, height = 192 }) v.sprite:show() v.frente:show() if entradaEv then removeEvent(entradaEv) entradaEv = nil end v.sprite:setMarginLeft(e.x) v.sprite:setMarginTop(e.y) fundido.titulo:setText('Rumbo a ' .. destino) fundido.sub:setText((MEDIO[medio] or '') .. ((region and region ~= '') and (' · ' .. region) or '')) local t0 = g_clock.millis() pararEscena() local function paso() escenaEv = nil if not fundido:isVisible() then return end cuadroEscena(e, g_clock.millis() - t0) escenaEv = scheduleEvent(paso, 33) end fundido.saliendo = nil fundido:show() fundido:raise() paso() fundir(fundido, 0, 1, 350) if seguro then removeEvent(seguro) end seguro = scheduleEvent(function() seguro = nil quitarFundido() end, (tonumber(ms) or 3000) + 4000) end local function mostrarCartel(nombre, region) if not cartel then return end cartel.nombre:setText(nombre) cartel.sub:setText('Su faro ya está encendido en la carta de la Liga · ' .. region) if animCartel then removeEvent(animCartel) end -- después del cartel de zona (game_zonas, 3,2 s arriba): sale debajo y algo más tarde para no pisarse animCartel = scheduleEvent(function() cartel.saliendo = nil cartel:show() cartel:raise() fundir(cartel, 0, 1, 500, function() animCartel = scheduleEvent(function() animCartel = nil ocultar(cartel, 700) end, 2600) end) end, 700) end -- Arte de dungeons, arenas y raids (lo usan también game_dungeon, game_buscador, game_pvp y game_raid) --------------- -- Imágenes de 320 x 180 en images/dungeons/.png. claveDe() reconoce el nombre que manda el servidor. local ARTE = { fortaleza = 1, gorruk = 1, cripta = 1, abadia = 1, bastion = 1, fundicion = 1, santuario = 1, archivo = 1, bateria = 1, palacio = 1, camara = 1, arena_patio = 1, arena_cantera = 1, ciudadela = 1, raid_farero = 1 } local NOMBRES = { { 'fortaleza', 'fortaleza' }, { 'gorruk', 'gorruk' }, { 'cripta', 'cripta' }, { 'abadia', 'abad' }, { 'bastion', 'campanario' }, { 'fundicion', 'fundici' }, { 'santuario', 'santuario' }, { 'archivo', 'archivo' }, { 'bateria', 'bater' }, { 'palacio', 'palacio' }, { 'camara', 'noveno sello' }, { 'arena_patio', 'patio del faro' }, { 'arena_cantera', 'cantera' }, { 'ciudadela', 'ciudadela' }, { 'raid_farero', 'primer farero' } } function claveDe(texto) if ARTE[texto or ''] then return texto end local t = (texto or ''):lower() for _, c in ipairs(NOMBRES) do if t:find(c[2], 1, true) then return c[1] end end return nil end function rutaArte(clave) return IMG .. 'dungeons/' .. clave end -- Pone la ilustración en un widget: franja de y a y + alto (píxeles del arte, ancho completo de 320). Devuelve la clave. function ponerArte(w, nombre, y, alto) local k = claveDe(nombre) if not w then return k end if not k then w:hide() return nil end w:setImageSource(rutaArte(k)) w:setImageClip({ x = 0, y = y or 0, width = 320, height = alto or 180 }) w:show() return k end -- Escena al entrar o salir de una dungeon o arena («E»): la ilustración se acerca despacio, la niebla pasa y, al entrar -- a una dungeon, su portal gira en medio. Dura lo que dice el servidor y se va sola. local PORTAL_TONO = { fortaleza = 1, gorruk = 2, cripta = 3, abadia = 4, bastion = 5, fundicion = 6, santuario = 7, archivo = 8, bateria = 9, palacio = 10, camara = 11 } local entradaEv = nil local function ponerEntrada(titulo, ms, clave, verbo) if not fundido then return end local k = claveDe(clave) or claveDe(titulo) local v = fundido.cuadro.vista if k then v.fondo:setImageSource(rutaArte(k)) end v.nubes:setImageSource(IMG .. 'nubes') v.nubes:setOpacity(0.35) v.frente:hide() local conPortal = PORTAL_TONO[k] ~= nil and verbo ~= 'Saliendo de' v.sprite:setVisible(conPortal) if conPortal then v.sprite:setImageSource(IMG .. 'portales/' .. k .. '-hoja') v.sprite:setSize({ width = 128, height = 128 }) v.sprite:setMarginLeft(236) v.sprite:setMarginTop(150) end fundido.titulo:setText((verbo or 'Entrando en') .. ' ' .. titulo) fundido.sub:setText(k and k:find('^arena') and 'Arena' or ((k == 'ciudadela' or k == 'raid_farero') and 'Raid' or 'Mazmorra')) local dur = tonumber(ms) or 2600 local t0 = g_clock.millis() pararEscena() local function paso() escenaEv = nil if not fundido:isVisible() then return end local t = (g_clock.millis() - t0) / 1000 -- acercamiento lento: el recorte pasa de 320 x 180 a 296 x 166 hacia el centro local z = math.min(1, t / (dur / 1000)) local w, h = math.floor(320 - 24 * z), math.floor(180 - 14 * z) v.fondo:setImageClip({ x = math.floor((320 - w) / 2), y = math.floor((180 - h) / 2), width = w, height = h }) v.nubes:setImageClip({ x = math.floor(t * 14) % NUBES_TIRA, y = 0, width = 300, height = 64 }) if conPortal then v.sprite:setImageClip({ x = (math.floor(t * 9) % 8) * 64, y = 0, width = 64, height = 64 }) end escenaEv = scheduleEvent(paso, 33) end fundido.saliendo = nil fundido:show() fundido:raise() paso() fundir(fundido, 0, 1, 160) if entradaEv then removeEvent(entradaEv) end entradaEv = scheduleEvent(function() entradaEv = nil quitarFundido(500) end, dur) end -- Portales del mundo («Q»): un portal animado por dungeon (APNG en images/portales/), pegado a su casilla como efecto -- adjunto (game_attachedeffects / g_attachedEffects). Solo se dibuja; entrar sigue siendo cosa del servidor. local PORTAL_ID0 = 960 local portales = {} -- { clave, pos, id } local portalesListos = false local function registrarPortales() if portalesListos or not g_attachedEffects then return end portalesListos = true for k, i in pairs(PORTAL_TONO) do local ok, e = pcall(function() return g_attachedEffects.registerByImage(PORTAL_ID0 + i, 'Portal ' .. k, IMG .. 'portales/' .. k) end) if ok and e then e:setSize({ width = 64, height = 64 }) e:setOffset(16, 32) e:setPermanent(true) pcall(function() e:setLight({ color = 215, intensity = 2 }) end) end end end local function pegarPortales() local yo = g_game.isOnline() and g_game.getLocalPlayer() if not yo or #portales == 0 then return end local p = yo:getPosition() if not p then return end for _, q in ipairs(portales) do if q.pos.z == p.z and math.abs(q.pos.x - p.x) <= 10 and math.abs(q.pos.y - p.y) <= 8 then local tile = g_map.getTile(q.pos) if tile and not tile:getAttachedEffectById(q.id) then local e = g_attachedEffects.getById(q.id) if e then tile:attachEffect(e) end end end end end local function leerPortales(buffer) registrarPortales() portales = {} for _, l in ipairs(partir(buffer:sub(3), '~')) do local m = partir(l, '^') local i = PORTAL_TONO[m[1] or ''] if i and tonumber(m[4]) then portales[#portales + 1] = { clave = m[1], id = PORTAL_ID0 + i, pos = { x = tonumber(m[2]), y = tonumber(m[3]), z = tonumber(m[4]) } } end end pegarPortales() end local function onOpcode(protocol, opcode, buffer) local c = buffer:sub(1, 1) if c == 'A' then datos = leer(buffer) mensaje = nil pintar() abrir() elseif c == 'M' then mensaje, mensajeHasta = buffer:sub(3), g_clock.seconds() + 5 pintarPlaca() elseif c == 'X' then ocultar(ventana) elseif c == 'T' then local m = partir(buffer:sub(3), '^') ocultar(ventana, 120) ponerEscena(m[1] or '', m[2], m[3], m[4]) elseif c == 'L' then quitarFundido(500) elseif c == 'C' then quitarFundido(220) elseif c == 'D' then local m = partir(buffer:sub(3), '^') mostrarCartel(m[1] or '', m[2] or '') elseif c == 'E' then local m = partir(buffer:sub(3), '^') ocultar(ventana, 120) ponerEntrada(m[1] or '', m[2], m[3], m[4]) elseif c == 'Q' then leerPortales(buffer) end end -- Para probar sin servidor: modules.game_viaje.probar('A|...') function probar(texto) onOpcode(nil, OPCODE, texto) end function elegir(i) elegirDestino(i) end -- Latido: el anillo del destino elegido respira (estado elegido) y el aviso de error caduca. local function tic() if anillo and anillo:isVisible() then anillo:setOpacity(0.7 + 0.3 * math.sin(g_clock.millis() / 260)) end if mensaje and g_clock.seconds() >= mensajeHasta then mensaje = nil if ventana and ventana:isVisible() then pintarPlaca() end end end function viajeController:onInit() g_ui.importStyle('valdren') ventana = g_ui.displayUI('viaje') if OtmmoVentanas then OtmmoVentanas.recordar(ventana, 'viaje') end ventana:hide() local src = ventana.carta:getImageSource() or '' if src:find('carta') then IMG = src:gsub('carta[^/]*$', '') end -- velo y aviso sobre toda la pantalla (rootWidget), no solo sobre el panel del mapa fundido = g_ui.createWidget('ViajeFundido', rootWidget) fundido:fill('parent') cartel = g_ui.createWidget('CartelDestino', rootWidget) cartel:addAnchor(AnchorTop, 'parent', AnchorTop) cartel:addAnchor(AnchorHorizontalCenter, 'parent', AnchorHorizontalCenter) cartel:setMarginTop(220) ProtocolGame.registerExtendedOpcode(OPCODE, onOpcode) ciclo = cycleEvent(tic, 50) portalEv = cycleEvent(function() pcall(pegarPortales) end, 600) end function viajeController:onGameEnd() datos, elegido, mensaje = nil, nil, nil if seguro then removeEvent(seguro) seguro = nil end pararEscena() for _, w in ipairs({ ventana, fundido, cartel }) do if w then w:hide() w:setOpacity(1) end end end function viajeController:onTerminate() self:onGameEnd() borrarRuta() if ciclo then removeEvent(ciclo) ciclo = nil end if portalEv then removeEvent(portalEv) portalEv = nil end if animCartel then removeEvent(animCartel) animCartel = nil end ProtocolGame.unregisterExtendedOpcode(OPCODE) for _, w in ipairs({ ventana, fundido, cartel }) do if w then w:destroy() end end ventana, fundido, cartel, marcas = nil, nil, nil, {} end