-- OT-MMO: buscador de grupo por rol (como el «Dungeon Finder» de WoW). Solo dibuja y pide: el servidor -- (server/scripts/buscador.lua) arma los grupos, pregunta a los cuatro y los mete en la mazmorra. Opcode 112: -- «E|clase^permitidos^sugerido^nivel^emblemas^porDescubrir|id^nombre^tipo^nivelMin^nivelMax^motivo^región~…|segundos^roles^ids^fP^fS^fD^enGrupo^esLider» -- «L|actividad^segundos^aceptado|nombre^vocación^nivel^rol^A/E~…» ¡Grupo listo! · «X» lo cierra · «M|texto» rechazo -- «D|nombre^región^id» mazmorra descubierta (cartel arriba al centro) -- El cliente envía «P» (estado), «B|roles|ids» (buscar), «Q» (salir de la cola), «S» / «N» (aceptar / rechazar). -- Solo llegan las mazmorras descubiertas (las demás, solo cuántas faltan). Pestañas: Mazmorras (por tramo de nivel) y -- Heroicas (nivel 100, bloqueo de 24 h, Emblemas del Faro). La selección es una sola para las dos pestañas. local OPCODE = 112 -- íconos = objetos de Tibia, como el micromenú: escudo de acero, ankh, espada local ROLES = { { id = 'P', nombre = 'Protector', item = 3409 }, { id = 'S', nombre = 'Sanador', item = 3077 }, { id = 'D', nombre = 'Daño', item = 3264 }, } local NOMBRE_ROL = { P = 'Protector', S = 'Sanador', D = 'Daño' } local CLASE = { [1] = 'Hechicero', [2] = 'Druida', [3] = 'Cazador', [4] = 'Guardián', [11] = 'Brujo', [12] = 'Bardo', [13] = 'Gunslinger' } -- getId: 5-8 = promoción local ORO, APAGADO, BLANCO, VERDE, OXIDO = OtmmoTheme.color.accent, OtmmoTheme.color.text2, OtmmoTheme.color.text, OtmmoTheme.color.ok, OtmmoTheme.color.warn -- tokens local NIVEL_HEROICA = 100 local HOLGURA = 5 -- «Para tu nivel»: de su nivel mínimo hasta 5 por encima del máximo local TRAMOS = { { 1, 30 }, { 31, 60 }, { 61, 100 } } local PESTANAS = { { 'M', 'Mazmorras' }, { 'H', 'Heroicas' } } buscadorController = Controller:new() local ventana, listo, pastilla, cartel = nil, nil, nil, nil local cartelEv = nil local estado = nil -- último «E»: { clase, permitidos, sugerido, nivel, emblemas, porDescubrir, acts = {…}, cola = {…} o nil } local elegidos = nil -- selección local antes de buscar: { roles = { P = true }, acts = { fortaleza = true } } local pestana = 'M' local colaDesde = nil -- g_clock.seconds() en que empezó la espera local listoFin, listoTotal = nil, 30 local ciclo = nil local mensaje, mensajeHasta = nil, 0 -- «M|texto»: por qué el servidor no lo hizo (6 s en la ventana) local function enviar(texto) local protocol = g_game.getProtocolGame() if protocol then protocol:sendExtendedOpcode(OPCODE, texto) end end -- Como string.split pero conservando los campos vacíos. local function partir(s, sep) local t = {} for x in (s .. sep):gmatch('(.-)' .. sep:gsub('%p', '%%%0')) do t[#t + 1] = x end return t end local function escribiendo() local w = rootWidget while w:getFocusedChild() do w = w:getFocusedChild() end return w:getClassName() == 'UITextEdit' and w:isVisible() end local function reloj(s) return string.format('%02d:%02d', math.floor(s / 60), s % 60) end local function plural(n, uno, varios) return n .. ' ' .. (n == 1 and uno or varios) end -- Ventana ---------------------------------------------------------------------------------------------------------------- local function enCola() return estado and estado.cola ~= nil end local function esHeroica(a) return a.tipo == 'Heroica' end local function deLaPestana(p) local l = {} for _, a in ipairs(estado.acts) do if esHeroica(a) == (p == 'H') then l[#l + 1] = a end end table.sort(l, function(x, y) return x.min < y.min end) return l end local function recomendada(a) return not esHeroica(a) and a.motivo == '' and estado.nivel >= a.min and estado.nivel <= a.max + HOLGURA end local function nivelTexto(a) if esHeroica(a) then return 'nivel ' .. a.min end return a.max >= 999 and ('nivel ' .. a.min .. '+') or ('nivel ' .. a.min .. '-' .. a.max) end local function faltaTexto(c) local partes = {} for _, r in ipairs({ { 'P', c.fP }, { 'S', c.fS }, { 'D', c.fD } }) do if r[2] > 0 then partes[#partes + 1] = (r[2] > 1 and (r[2] .. ' ') or '') .. NOMBRE_ROL[r[1]] end end return #partes > 0 and ('Faltan: ' .. table.concat(partes, ' · ')) or 'Grupo completo: avisando a todos...' end -- Selección efectiva (en la cola manda lo apuntado; si no, lo que el jugador va marcando). local function seleccion() local c = estado.cola if not c then return elegidos.roles, elegidos.acts end local roles, acts = {}, {} for r in c.roles:gmatch('.') do roles[r] = true end for id in c.ids:gmatch('[^,]+') do acts[id] = true end return roles, acts end local function cuentaMarcadas(acts, p) local n = 0 for _, a in ipairs(estado.acts) do if acts[a.id] and esHeroica(a) == (p == 'H') and (a.motivo == '' or enCola()) then n = n + 1 end end return n end local function nombresRoles(roles) local l = {} for _, r in ipairs(ROLES) do if roles[r.id] then l[#l + 1] = r.nombre end end if #l <= 1 then return l[1] or '' end return table.concat(l, ', ', 1, #l - 1) .. ' o ' .. l[#l] end local function vacio(titulo, texto) local w = g_ui.createWidget('BuVacio', ventana.actividades) w.titulo:setText(titulo) w.texto:setText(texto) w:setHeight(texto ~= '' and 72 or 40) return w end local function filaActividad(a, acts, c) local w = g_ui.createWidget('BuActividad', ventana.actividades) local puede = a.motivo == '' local marcada = acts[a.id] == true and (puede or c ~= nil) local base = a.id:gsub('_heroica$', '') w.nombre:setText(esHeroica(a) and a.nombre:gsub('%s*%(heroica%)$', '') or a.nombre) if modules.game_viaje and modules.game_viaje.ponerArte then modules.game_viaje.ponerArte(w.arte, base, 0, 180) w.arte:setOpacity(puede and 1 or 0.4) end w.nombre:setColor(puede and BLANCO or APAGADO) w.meta:setText((a.region ~= '' and (a.region .. ' · ') or '') .. nivelTexto(a)) if not puede then w.etiqueta:setText(a.motivo) w.etiqueta:setColor(OXIDO) elseif recomendada(a) then w.etiqueta:setText('Para tu nivel') w.etiqueta:setColor(ORO) else w.etiqueta:setText('') end w.casilla:setBackgroundColor(marcada and ORO or 'alpha') w.casilla:setBorderColor(marcada and ORO or (puede and OtmmoTheme.color.borderHover or OtmmoTheme.color.border)) w.casilla:setOpacity((puede or marcada) and 1 or 0.4) w:setChecked(marcada) w:setEnabled((puede and not c) or (c ~= nil and marcada)) w.onClick = function() if c then return end elegidos.acts[a.id] = not elegidos.acts[a.id] or nil mensaje = nil pintar() end end local function pintarLista(acts, c) local lista = ventana.actividades lista:destroyChildren() local filas = deLaPestana(pestana) if pestana == 'M' then if #filas == 0 then vacio(estado.porDescubrir > 0 and plural(estado.porDescubrir, 'mazmorra por descubrir', 'mazmorras por descubrir') or 'No hay mazmorras abiertas ahora.', 'Cada mazmorra tiene un portal en algún lugar de Valdren. Cuando llegues a uno, la mazmorra aparece aquí y puedes buscar grupo para ella.') else for _, t in ipairs(TRAMOS) do local cabecera = false for _, a in ipairs(filas) do if a.min >= t[1] and a.min <= t[2] then if not cabecera then local h = g_ui.createWidget('BuTramo', lista) h.texto:setText(('Nivel %d-%d'):format(t[1], t[2])) cabecera = true end filaActividad(a, acts, c) end end end if estado.porDescubrir > 0 then vacio(plural(estado.porDescubrir, 'mazmorra más por descubrir', 'mazmorras más por descubrir'), 'Explora Valdren: al llegar a su portal, se añade aquí.') end end else if estado.nivel < NIVEL_HEROICA then vacio(('Las heroicas se abren a nivel %d.'):format(NIVEL_HEROICA), ('Son las mismas mazmorras, más duras: equipo de nivel 110 y Emblemas del Faro. Te faltan %s.'):format( plural(NIVEL_HEROICA - estado.nivel, 'nivel', 'niveles'))) elseif #filas == 0 then vacio('Ninguna heroica abierta todavía.', 'La heroica de una mazmorra se abre cuando la descubres.') else for _, a in ipairs(filas) do filaActividad(a, acts, c) end end end end function pintar() if not ventana or not estado then return end local c = estado.cola local roles, acts = seleccion() ventana.roles:destroyChildren() for _, r in ipairs(ROLES) do local w = g_ui.createWidget('BuRol', ventana.roles) local puede = estado.permitidos:find(r.id) ~= nil local marcado = roles[r.id] == true w.item:setItemId(r.item) w.nombre:setText(r.nombre) w.nota:setText(not puede and 'Tu clase no puede' or (r.id == estado.sugerido and 'Sugerido para ti' or '')) w.nota:setColor(r.id == estado.sugerido and puede and ORO or APAGADO) w.casilla:setBackgroundColor(marcado and ORO or 'alpha') w.casilla:setBorderColor(marcado and ORO or OtmmoTheme.color.borderHover) w:setChecked(marcado) -- en la cola lo apuntado se ve entero (sin clic) y el resto apagado w:setEnabled(puede and (not c or marcado)) w.onClick = function() if c then return end elegidos.roles[r.id] = not elegidos.roles[r.id] or nil mensaje = nil pintar() end end -- pestañas: «Heroicas (2)» si hay marcadas fuera de la vista for _, tab in ipairs(ventana.pestanas:getChildren()) do local n = cuentaMarcadas(acts, tab.clave) tab:setText(tab.nombre .. (n > 0 and (' (' .. n .. ')') or '')) tab:setChecked(tab.clave == pestana) end local heroicaAbierta = pestana == 'H' and estado.nivel >= NIVEL_HEROICA ventana.emblemas:setText(pestana == 'H' and ('Emblemas del Faro ' .. estado.emblemas) or '') local hayFilas = #deLaPestana(pestana) > 0 if pestana == 'M' then ventana.nota:setText(hayFilas and 'Marca en qué mazmorras quieres entrar.' or '') else ventana.nota:setText(heroicaAbierta and 'Una vez cada 24 h por mazmorra, desde que cae su primer jefe.' or '') end local libres, todasMarcadas = 0, true for _, a in ipairs(deLaPestana(pestana)) do if a.motivo == '' then libres = libres + 1 if not elegidos.acts[a.id] then todasMarcadas = false end end end ventana.todas:setVisible(libres > 1 and not c) ventana.todas:setText(todasMarcadas and 'Quitar todas' or 'Marcar todas') pintarLista(acts, c) local aviso = mensaje and g_clock.seconds() < mensajeHasta if c then ventana.estado:setText('Buscando grupo · ' .. reloj(math.max(0, g_clock.seconds() - colaDesde))) ventana.falta:setText(faltaTexto(c) .. (c.enGrupo and (c.esLider and ' (con tu grupo)' or ' (te apuntó tu líder)') or '')) ventana.principal:setText('Salir de la cola') ventana.principal:setEnabled(true) else local nM, nH = cuentaMarcadas(elegidos.acts, 'M'), cuentaMarcadas(elegidos.acts, 'H') local hayRol = next(elegidos.roles) ~= nil if not hayRol then ventana.estado:setText('Elige tu rol') ventana.falta:setText('Marca al menos un rol en la columna de la izquierda.') elseif #estado.acts == 0 then ventana.estado:setText('Sin mazmorras todavía') ventana.falta:setText('Descubre el portal de una mazmorra para buscar grupo.') elseif nM + nH == 0 then ventana.estado:setText('Elige dónde entrar') ventana.falta:setText('Marca al menos una mazmorra de la lista.') else local donde = {} if nM > 0 then donde[#donde + 1] = plural(nM, 'mazmorra', 'mazmorras') end if nH > 0 then donde[#donde + 1] = plural(nH, 'heroica', 'heroicas') end ventana.estado:setText('Como ' .. nombresRoles(elegidos.roles) .. ' en ' .. table.concat(donde, ' y ')) ventana.falta:setText('Te avisamos cuando el grupo esté listo; puedes seguir jugando.') end ventana.principal:setText('Buscar grupo') ventana.principal:setEnabled(hayRol and nM + nH > 0) end if aviso then ventana.falta:setText(mensaje) end ventana.falta:setColor(aviso and OXIDO or APAGADO) end function cambiarPestana(p) pestana = p pintar() end -- «Marcar todas» / «Quitar todas» las que se pueden de la pestaña. function marcarTodas() if not estado or enCola() then return end local libres, todas = {}, true for _, a in ipairs(deLaPestana(pestana)) do if a.motivo == '' then libres[#libres + 1] = a.id if not elegidos.acts[a.id] then todas = false end end end for _, id in ipairs(libres) do elegidos.acts[id] = (not todas) or nil end pintar() end local function pintarPastilla() if not pastilla then return end if enCola() then pastilla:setText('Buscando grupo · ' .. reloj(math.max(0, g_clock.seconds() - colaDesde))) pastilla:show() else pastilla:hide() end end function toggle() if not ventana then return end if ventana:isVisible() then return ventana:hide() end enviar('P') pintar() ventana:show() ventana:raise() ventana:focus() end function principal() if enCola() then return enviar('Q') end local roles, ids = '', {} for _, r in ipairs(ROLES) do if elegidos.roles[r.id] then roles = roles .. r.id end end for _, a in ipairs(estado.acts) do if elegidos.acts[a.id] and a.motivo == '' then ids[#ids + 1] = a.id end end enviar('B|' .. roles .. '|' .. table.concat(ids, ',')) end -- ¡Grupo listo! --------------------------------------------------------------------------------------------------------- local function pintarListo(nombre, segundos, aceptado, miembros) listo.nombre:setText(nombre) if modules.game_viaje and modules.game_viaje.ponerArte then modules.game_viaje.ponerArte(listo.arte, nombre, 48, 78) end listo.miembros:destroyChildren() local orden = { P = 1, S = 2, D = 3 } table.sort(miembros, function(a, b) return (orden[a.rol] or 4) < (orden[b.rol] or 4) end) local nAcepta = 0 for _, m in ipairs(miembros) do local w = g_ui.createWidget('BuMiembro', listo.miembros) w.rol:setText(NOMBRE_ROL[m.rol] or '') w.nombre:setText(m.nombre) w.meta:setText((CLASE[m.voc] or CLASE[(m.voc - 1) % 4 + 1] or '') .. ' · nivel ' .. m.nivel) w.estado:setText(m.acepta and 'Aceptó' or 'Esperando') w.estado:setColor(m.acepta and VERDE or APAGADO) if m.acepta then nAcepta = nAcepta + 1 end end if not listo:isVisible() or not listoFin then listoTotal = math.max(segundos, 1) end listoFin = g_clock.seconds() + segundos listo.aceptar:setEnabled(not aceptado) listo.aceptar:setText(aceptado and 'Aceptado' or 'Aceptar') listo.rechazar:setEnabled(not aceptado) listo.meta:setText(('%s · %d de %d aceptaron'):format(nombre:find('heroica') and 'Heroica' or 'Mazmorra', nAcepta, #miembros)) if not listo:isVisible() then if ventana then ventana:hide() end listo:show() listo:raise() listo:focus() end end local function pintarTiempo() if listo and listo:isVisible() and listoFin then local quedan = math.max(0, listoFin - g_clock.seconds()) local aviso = mensaje and g_clock.seconds() < mensajeHasta listo.barra:setWidth(math.floor(listo.fondoBarra:getWidth() * quedan / listoTotal)) listo.tiempo:setText(aviso and mensaje or ('Quedan %d s para responder'):format(quedan)) listo.tiempo:setColor(aviso and OXIDO or APAGADO) end end function responder(si) enviar(si and 'S' or 'N') end local function cerrarListo() if listo then listo:hide() end listoFin = nil end -- Mazmorra descubierta: cartel 3 s arriba al centro (algo más tarde que el de logro o zona, para no pisarse). local function mostrarCartel(nombre, region, id) if not cartel then return end cartel.nombre:setText(nombre) cartel.sub:setText('Ya puedes buscar grupo para ella en el buscador (O)' .. (region ~= '' and (' · ' .. region) or '')) local k = modules.game_viaje and modules.game_viaje.ponerArte and modules.game_viaje.ponerArte(cartel.arte, id ~= '' and id or nombre, 0, 180) cartel.arte:setVisible(k ~= nil) if cartelEv then removeEvent(cartelEv) end cartelEv = scheduleEvent(function() cartel:setOpacity(0) cartel:show() cartel:raise() g_effects.fadeIn(cartel, 400) cartelEv = scheduleEvent(function() g_effects.fadeOut(cartel, 700) cartelEv = scheduleEvent(function() cartel:hide() cartelEv = nil end, 750) end, 3400) end, 900) end -- Opcode ----------------------------------------------------------------------------------------------------------------- local function onOpcode(protocol, opcode, buffer) local tipo = buffer:sub(1, 1) if tipo == 'X' then return cerrarListo() elseif tipo == 'D' then local d = partir(buffer:sub(3), '^') return mostrarCartel(d[1] or '', d[2] or '', d[3] or '') elseif tipo == 'M' then mensaje, mensajeHasta = buffer:sub(3), g_clock.seconds() + 6 return pintar() end local p = partir(buffer:sub(3), '|') if tipo == 'E' then local cab = partir(p[1] or '', '^') local nuevo = { clase = cab[1], permitidos = cab[2] or '', sugerido = cab[3] or '', nivel = tonumber(cab[4]) or 1, emblemas = tonumber(cab[5]) or 0, porDescubrir = tonumber(cab[6]) or 0, acts = {} } for _, linea in ipairs(partir(p[2] or '', '~')) do local a = partir(linea, '^') if a[1] ~= '' then nuevo.acts[#nuevo.acts + 1] = { id = a[1], nombre = a[2], tipo = a[3], min = tonumber(a[4]) or 1, max = tonumber(a[5]) or 99, motivo = a[6] or '', region = a[7] or '' } end end if p[3] and p[3] ~= '' then local c = partir(p[3], '^') nuevo.cola = { roles = c[2], ids = c[3], fP = tonumber(c[4]) or 0, fS = tonumber(c[5]) or 0, fD = tonumber(c[6]) or 0, enGrupo = c[7] == '1', esLider = c[8] == '1' } colaDesde = g_clock.seconds() - (tonumber(c[1]) or 0) end -- primera vez (o cambió la clase): el rol sugerido, las mazmorras para tu nivel (si no hay, todas las que puedes) -- y todas las heroicas libres. Lo que se descubre después llega sin marcar. local primera = not elegidos or not estado or estado.permitidos ~= nuevo.permitidos estado = nuevo if primera then elegidos = { roles = { [nuevo.sugerido] = true }, acts = {} } local alguna = false for _, a in ipairs(nuevo.acts) do if recomendada(a) or (esHeroica(a) and a.motivo == '') then elegidos.acts[a.id] = true alguna = alguna or not esHeroica(a) end end if not alguna then for _, a in ipairs(nuevo.acts) do if not esHeroica(a) and a.motivo == '' then elegidos.acts[a.id] = true end end end end pintar() pintarPastilla() elseif tipo == 'L' then local cab = partir(p[1] or '', '^') local miembros = {} for _, linea in ipairs(partir(p[2] or '', '~')) do local m = partir(linea, '^') if m[1] ~= '' then miembros[#miembros + 1] = { nombre = m[1], voc = tonumber(m[2]) or 0, nivel = tonumber(m[3]) or 0, rol = m[4], acepta = m[5] == 'A' } end end pintarListo(cab[1] or '', tonumber(cab[2]) or 30, cab[3] == '1', miembros) end end -- Para probar sin servidor: modules.game_buscador.probar('E|Guardián^PD^P^24^0^9|fortaleza^La Fortaleza^Mazmorra^8^12^^Bosque del Tuerto~cripta^La Cripta^Mazmorra^22^26^^Puerto de Vigilia|') function probar(texto) onOpcode(nil, OPCODE, texto) end local function tic() pintarTiempo() if mensaje and g_clock.seconds() >= mensajeHasta then mensaje = nil pintar() end if enCola() then pintarPastilla() if ventana and ventana:isVisible() then ventana.estado:setText('Buscando grupo · ' .. reloj(math.max(0, g_clock.seconds() - colaDesde))) end end end function buscadorController:onInit() g_ui.importStyle('/game_viaje/valdren') -- marco y estilos VALDREN (modules/game_viaje/valdren.otui) g_ui.importStyle('estilos') ventana = g_ui.displayUI('buscador') OtmmoVentanas.recordar(ventana, 'buscador') -- posición entre sesiones (fase 4) ventana:hide() for _, t in ipairs(PESTANAS) do local tab = g_ui.createWidget('GameTab', ventana.pestanas) tab.clave, tab.nombre = t[1], t[2] tab:setText(t[2]) tab.onClick = function() cambiarPestana(t[1]) end end listo = g_ui.displayUI('listo') listo:hide() -- arriba al centro, como el ojo del buscador de WoW: con clic abre la ventana pastilla = g_ui.createWidget('BuPastilla', modules.game_interface.getRootPanel()) pastilla:addAnchor(AnchorTop, 'parent', AnchorTop) pastilla:addAnchor(AnchorHorizontalCenter, 'parent', AnchorHorizontalCenter) pastilla:setMarginTop(8) pastilla.onClick = function() toggle() end pastilla:hide() cartel = g_ui.createWidget('BuCartel', modules.game_interface.getRootPanel()) cartel:addAnchor(AnchorTop, 'parent', AnchorTop) cartel:addAnchor(AnchorHorizontalCenter, 'parent', AnchorHorizontalCenter) cartel:setMarginTop(150) ProtocolGame.registerExtendedOpcode(OPCODE, onOpcode) Keybind.new('Ventanas', 'Buscador de grupo', { [CHAT_MODE.ON] = '', [CHAT_MODE.OFF] = 'O' }, '') Keybind.bind('Ventanas', 'Buscador de grupo', { { type = KEY_DOWN, callback = function() if not escribiendo() then toggle() end end } }) if g_game.isOnline() then self:onGameStart() end end function buscadorController:onGameStart() if ciclo then removeEvent(ciclo) end ciclo = cycleEvent(tic, 250) enviar('P') end function buscadorController:onGameEnd() if ciclo then removeEvent(ciclo) ciclo = nil end estado, elegidos, colaDesde = nil, nil, nil cerrarListo() if ventana then ventana:hide() end if pastilla then pastilla:hide() end end function buscadorController:onTerminate() self:onGameEnd() ProtocolGame.unregisterExtendedOpcode(OPCODE) Keybind.delete('Ventanas', 'Buscador de grupo') if cartelEv then removeEvent(cartelEv) cartelEv = nil end for _, w in ipairs({ ventana, listo, pastilla, cartel }) do if w then w:destroy() end end ventana, listo, pastilla, cartel = nil, nil, nil, nil end