-- 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|id^nombre^tipo^nivelMin^nivelMax^motivo~…|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 -- El cliente envía «P» (estado), «B|roles|ids» (buscar), «Q» (salir de la cola), «S» / «N» (aceptar / rechazar). 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' } -- 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 buscadorController = Controller:new() local ventana, listo, pastilla = nil, nil, nil local estado = nil -- último «E»: { clase, permitidos, sugerido, acts = {…}, cola = {…} o nil } local elegidos = nil -- selección local antes de buscar: { roles = { P = true }, acts = { fortaleza = true } } 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 -- Ventana ---------------------------------------------------------------------------------------------------------------- local function enCola() return estado and estado.cola ~= nil 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 local function pintar() if not ventana or not estado then return end local c = estado.cola -- en la cola manda lo apuntado; si no, lo que el jugador va marcando local roles = c and {} or elegidos.roles local acts = c and {} or elegidos.acts if c then for r in c.roles:gmatch('.') do roles[r] = true end for id in c.ids:gmatch('[^,]+') do acts[id] = true end end 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 w.item:setItemId(r.item) w.nombre:setText(r.nombre) w.nota:setText(not puede and 'No es de tu clase' or (r.id == estado.sugerido and 'Sugerido' or '')) w.nota:setColor(r.id == estado.sugerido and puede and ORO or APAGADO) w:setChecked(roles[r.id] == true) -- en la cola lo apuntado se ve entero (sin clic) y el resto apagado w:setEnabled(puede and (not c or roles[r.id] == true)) w.onClick = function() if c then return end elegidos.roles[r.id] = not elegidos.roles[r.id] or nil pintar() end end ventana.actividades:destroyChildren() ventana:setHeight(313 + 46 * #estado.acts) for _, a in ipairs(estado.acts) do local w = g_ui.createWidget('BuActividad', ventana.actividades) local puede = a.motivo == '' local marcada = acts[a.id] == true and (puede or c ~= nil) w.nombre:setText(a.nombre) w.nombre:setColor(puede and BLANCO or OtmmoTheme.color.textDisabled) w.meta:setText(a.tipo .. ' · ' .. (a.max >= 99 and ('nivel ' .. a.min .. '+') or ('nivel ' .. a.min .. '-' .. a.max))) w.motivo:setText(a.motivo) w.casilla:setBackgroundColor(marcada and ORO or 'alpha') w.casilla:setBorderColor(marcada and ORO or OtmmoTheme.color.borderHover) 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 pintar() end end 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.falta:setColor(APAGADO) ventana.principal:setText('Salir de la cola') ventana.principal:setEnabled(true) else local hayRol, hayAct = next(elegidos.roles) ~= nil, false for _, a in ipairs(estado.acts) do if elegidos.acts[a.id] and a.motivo == '' then hayAct = true end end ventana.estado:setText('Sin buscar') ventana.falta:setText(not hayRol and 'Marca al menos un rol.' or (not hayAct and 'Elige al menos una mazmorra.' or 'Te avisamos cuando haya grupo.')) local aviso = mensaje and g_clock.seconds() < mensajeHasta if aviso then ventana.falta:setText(mensaje) end ventana.falta:setColor(aviso and OXIDO or APAGADO) ventana.principal:setText('Buscar grupo') ventana.principal:setEnabled(hayRol and hayAct) end 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) 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(('Mazmorra · %d de %d aceptaron'):format(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 -- Opcode ----------------------------------------------------------------------------------------------------------------- local function onOpcode(protocol, opcode, buffer) local tipo = buffer:sub(1, 1) if tipo == 'X' then return cerrarListo() 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 '', 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 '' } 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 y todas las mazmorras que puede if not elegidos or not estado or estado.permitidos ~= nuevo.permitidos then elegidos = { roles = { [nuevo.sugerido] = true }, acts = {} } for _, a in ipairs(nuevo.acts) do if a.motivo == '' then elegidos.acts[a.id] = true end end end estado = nuevo 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|fortaleza^La Fortaleza^Mazmorra^8^12^~cripta^La Cripta^Mazmorra^1^99^|') 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('estilos') ventana = g_ui.displayUI('buscador') OtmmoVentanas.recordar(ventana, 'buscador') -- posición entre sesiones (fase 4) ventana:hide() 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() 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') for _, w in ipairs({ ventana, listo, pastilla }) do if w then w:destroy() end end ventana, listo, pastilla = nil, nil, nil end