-- OT-MMO: ventana de gremio (G), paso 1 «Base» de los gremios (vault OT-MMO/gremios/plan-gremios.md). Solo dibuja y -- pide: el servidor (server/scripts/gremios/gremios.lua) decide permisos, rangos y quién entra o sale. Opcode 142: -- servidor -> «G|gid^nombre^carta^fundado^miRango^misPermisos^lider|id^nombre^nivel^permisos~...|nombre^nivel^voc^rango^enLinea~...|invitado~...» -- «N|gid^nombre^miembros^lider~...|coste^nivel» (sin gremio) · «M|texto» · «S» (fuera: cerrar canal) · «K» (dentro: abrir canal) -- cliente -> «P» · «V|nombre» · «Q|nombre» · «X|nombre» · «R|nombre^rango» · «E|rango^nombre^permisos» · «C|texto» -- «A|gid» · «D|gid» · «L» -- El canal es el de gremio del motor (id 0): el servidor solo lo crea al pedir la lista de canales, así que «Canal del -- gremio» la pide, cierra la ventana de canales que abre la consola y entra en el canal. local OPCODE = 142 -- 30-oct: el 140 es del Admin Panel (game_admin) local INVITAR, EXPULSAR, RANGOS, CARTA = 1, 2, 4, 8 local CLASE = { [1] = 'Hechicero', [2] = 'Druida', [3] = 'Cazador', [4] = 'Guardián', [11] = 'Brujo', [12] = 'Bardo', [13] = 'Gunslinger' } local VERDE, GRIS = '#52A674', '#52606A' local MESES = { 'ene', 'feb', 'mar', 'abr', 'may', 'jun', 'jul', 'ago', 'sep', 'oct', 'nov', 'dic' } gremioController = Controller:new() local ventana = nil local estado = nil -- último «G» o «N» local pestana = 'M' local elegido = nil -- nombre del miembro elegido local confirmar = nil -- { clave, hasta } para los botones de dos pasos local avisoEvento = nil local pedirCanal = false 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 tiene(per, b) return math.floor((per or 0) / b) % 2 == 1 end local function claseDe(voc) voc = tonumber(voc) or 0 if CLASE[voc] then return CLASE[voc] end if voc >= 5 and voc <= 8 then return CLASE[voc - 4] end return 'Sin clase' end local function fecha(t) local d = os.date('*t', tonumber(t) or 0) return string.format('%d %s %d', d.day, MESES[d.month], d.year) 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 icono(w, nivel) w:setImageSource('/game_gremio/images/rango' .. math.max(0, math.min(3, tonumber(nivel) or 0))) end -- Fundido con salida exponencial (como game_pvp). 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) w:setOpacity(desde + (hasta - desde) * (1 - (1 - t) ^ 4)) if t < 1 then w.fundido = scheduleEvent(paso, 16) else w.fundido = nil if alTerminar then alTerminar() end end end paso() end local function avisar(texto) if not ventana then return end for _, w in ipairs({ ventana.conGremio.pie.aviso, ventana.sinGremio.avisoSin }) do w:setText(texto) fundir(w, 0, 1, 200) end if avisoEvento then removeEvent(avisoEvento) end avisoEvento = scheduleEvent(function() avisoEvento = nil if ventana then fundir(ventana.conGremio.pie.aviso, 1, 0, 400) fundir(ventana.sinGremio.avisoSin, 1, 0, 400) end end, 5000) end -- Botón de dos pasos: el primer clic pide confirmación durante 6 s. local function dosPasos(clave, boton, textoConfirmar) if confirmar and confirmar.clave == clave and g_clock.millis() < confirmar.hasta then confirmar = nil return true end confirmar = { clave = clave, hasta = g_clock.millis() + 6000, boton = boton, texto = boton:getText() } boton:setText(textoConfirmar) boton:setColor('#DC6B6B') scheduleEvent(function() if boton and not boton:isDestroyed() and confirmar and confirmar.boton == boton and g_clock.millis() >= confirmar.hasta then boton:setText(confirmar.texto) boton:setColor('#E2E5E6') confirmar = nil end end, 6100) return false end -- Lectura de lo que manda el servidor --------------------------------------------------------------------------------- local function leer(buffer) local p = partir(buffer, '|') if p[1] == 'N' then local e = { sin = true, invitaciones = {} } for _, l in ipairs(partir(p[2], '~')) do local c = partir(l, '^') if c[1] ~= '' then e.invitaciones[#e.invitaciones + 1] = { gid = tonumber(c[1]), nombre = c[2], miembros = tonumber(c[3]) or 0, lider = c[4] or '' } end end local r = partir(p[3], '^') e.coste, e.nivel = tonumber(r[1]) or 0, tonumber(r[2]) or 0 return e end local c = partir(p[2], '^') local e = { gid = tonumber(c[1]), nombre = c[2], carta = c[3] or '', fundado = tonumber(c[4]) or 0, miRango = tonumber(c[5]) or 0, permisos = tonumber(c[6]) or 0, lider = c[7] or '', rangos = {}, porId = {}, miembros = {}, invitados = {} } for _, l in ipairs(partir(p[3], '~')) do local r = partir(l, '^') if r[1] ~= '' then local x = { id = tonumber(r[1]), nombre = r[2], nivel = tonumber(r[3]) or 0, permisos = tonumber(r[4]) or 0 } e.rangos[#e.rangos + 1] = x e.porId[x.id] = x end end for _, l in ipairs(partir(p[4], '~')) do local m = partir(l, '^') if m[1] ~= '' then e.miembros[#e.miembros + 1] = { nombre = m[1], nivel = tonumber(m[2]) or 0, voc = tonumber(m[3]) or 0, rango = tonumber(m[4]) or 0, enLinea = m[5] == '1' } end end for _, n in ipairs(partir(p[5], '~')) do if n ~= '' then e.invitados[#e.invitados + 1] = n end end e.yo = e.porId[e.miRango] or { nivel = 0, nombre = '' } local function nivelDe(m) return (e.porId[m.rango] or { nivel = -1 }).nivel end table.sort(e.miembros, function(a, b) if a.enLinea ~= b.enLinea then return a.enLinea end if nivelDe(a) ~= nivelDe(b) then return nivelDe(a) > nivelDe(b) end if a.nivel ~= b.nivel then return a.nivel > b.nivel end return a.nombre < b.nombre end) return e end -- Pintar -------------------------------------------------------------------------------------------------------------- local pintarElegido local function pintarCabecera() local e = estado if e.sin then ventana.nombre:setText('Sin gremio') ventana.lema:setText('El registro de gremios está en la Ciudad Alta de Vigilia') ventana.miRango:setText('') ventana.miIcono:hide() return end local enLinea = 0 for _, m in ipairs(e.miembros) do if m.enLinea then enLinea = enLinea + 1 end end ventana.nombre:setText(e.nombre) ventana.lema:setText(string.format('Fundado el %s · %d %s, %d en línea', fecha(e.fundado), #e.miembros, #e.miembros == 1 and 'miembro' or 'miembros', enLinea)) ventana.miRango:setText(e.yo.nombre) ventana.miIcono:show() icono(ventana.miIcono, e.yo.nivel) end local function pintarMiembros() local m = ventana.conGremio.miembros local e = estado local puedeInvitar = tiene(e.permisos, INVITAR) m.invitarNombre:setVisible(puedeInvitar) m.invitar:setVisible(puedeInvitar) m.lista:destroyChildren() local existe = false for _, x in ipairs(e.miembros) do local r = e.porId[x.rango] or { nombre = '?', nivel = 0 } local w = g_ui.createWidget('GrMiembro', m.lista) w.miembro = x w.punto:setBackgroundColor(x.enLinea and VERDE or GRIS) w.punto:setTooltip(x.enLinea and 'En línea' or 'Desconectado') icono(w.icono, r.nivel) w.nombre:setText(x.nombre) w.nombre:setColor(x.enLinea and '#E2E5E6' or '#9AA7AE') w.nivel:setText(tostring(x.nivel)) w.clase:setText(claseDe(x.voc)) w.rango:setText(r.nombre) w:setChecked(x.nombre == elegido) w.onClick = function() elegido = x.nombre for _, o in ipairs(m.lista:getChildren()) do if o.miembro then o:setChecked(o.miembro.nombre == elegido) end end pintarElegido() end if x.nombre == elegido then existe = true end end for _, n in ipairs(e.invitados) do local w = g_ui.createWidget('GrInvitado', m.lista) w.nombre:setText(n) w.retirar:setVisible(puedeInvitar) w.retirar.onClick = function() enviar('Q|' .. n) end end if not existe then elegido = nil end -- carta local lado = m.lado local editando = lado.cartaEdit:isVisible() lado.editarCarta:setVisible(tiene(e.permisos, CARTA) and not editando) if not editando then if e.carta ~= '' then lado.carta:setText(e.carta) lado.carta:setColor('#E8D3A2') else lado.carta:setText(tiene(e.permisos, CARTA) and 'Todavía no hay carta. Escribe aquí lo que todo el gremio debe leer al abrir su canal.' or 'Todavía no hay carta.') lado.carta:setColor('#9AA7AE') end end pintarElegido() end pintarElegido = function() local lado = ventana.conGremio.miembros.lado local e = estado lado.acciones:destroyChildren() local x for _, m in ipairs(e.miembros) do if m.nombre == elegido then x = m end end if not x then lado.elegidoIcono:hide() lado.elegido:setText('') lado.elegidoMeta:setText('Elige a un miembro de la lista para ver qué puedes hacer con él.') return end local r = e.porId[x.rango] or { nombre = '?', nivel = 0 } lado.elegidoIcono:show() icono(lado.elegidoIcono, r.nivel) lado.elegido:setText(x.nombre) local soyYo = x.nombre == g_game.getCharacterName() local meta = string.format('%s · nivel %d · %s', r.nombre, x.nivel, x.enLinea and 'en línea' or 'desconectado') if soyYo then lado.elegidoMeta:setText(meta .. '\nEres tú.') return end local mio = e.yo.nivel local botones = 0 if tiene(e.permisos, RANGOS) and r.nivel < mio then for _, o in ipairs(e.rangos) do if o.nivel < mio and o.id ~= x.rango then local b = g_ui.createWidget('GrBotonRango', lado.acciones) b:setText('Pasar a ' .. o.nombre) b:setIcon('/game_gremio/images/rango' .. o.nivel) b.onClick = function() enviar('R|' .. x.nombre .. '^' .. o.id) end botones = botones + 1 end end end if mio == 3 then local lider = e.rangos[1] local b = g_ui.createWidget('GrBotonRango', lado.acciones) b:setText('Ceder el liderazgo') b:setIcon('/game_gremio/images/rango3') b.onClick = function() if dosPasos('ceder' .. x.nombre, b, '¿Seguro? Otra vez') then enviar('R|' .. x.nombre .. '^' .. lider.id) end end botones = botones + 1 end if tiene(e.permisos, EXPULSAR) and r.nivel < mio then local b = g_ui.createWidget('Button', lado.acciones) b:setHeight(24) b:setText('Expulsar del gremio') b:setColor('#DC6B6B') b.onClick = function() if dosPasos('expulsar' .. x.nombre, b, '¿Expulsar? Otra vez') then enviar('X|' .. x.nombre) end end botones = botones + 1 end if botones == 0 then meta = meta .. '\nTu rango no puede cambiar nada de ' .. x.nombre .. '.' end lado.elegidoMeta:setText(meta) end local function pintarRangos() local p = ventana.conGremio.rangos local e = estado local puede = tiene(e.permisos, RANGOS) p.rangosNota:setText(puede and 'Cambia el nombre y los permisos de los rangos por debajo del tuyo. El Líder siempre lo puede todo; quien entra al gremio empieza en el rango más bajo.' or 'Los rangos de tu gremio y lo que puede hacer cada uno. Solo quien tiene el permiso de Rangos puede cambiarlos.') p.rangosLista:destroyChildren() for _, r in ipairs(e.rangos) do local w = g_ui.createWidget('GrRango', p.rangosLista) icono(w.icono, r.nivel) w.nombre:setText(r.nombre) local editable = puede and r.nivel < e.yo.nivel w.nombre:setEnabled(editable) for _, b in ipairs({ 1, 2, 4, 8 }) do local c = w['p' .. b] c:setChecked(tiene(r.permisos, b)) -- nadie da un permiso que no tiene c:setEnabled(editable and tiene(e.permisos, b)) end w.guardar:setVisible(editable) if r.nivel >= 3 then w:setTooltip('El Líder siempre tiene todos los permisos.') end w.guardar.onClick = function() local per = 0 for _, b in ipairs({ 1, 2, 4, 8 }) do if w['p' .. b]:isChecked() then per = per + b end end enviar('E|' .. r.id .. '^' .. w.nombre:getText():gsub('[%^~|]', '') .. '^' .. per) end end end local function pintarSin() local s = ventana.sinGremio local e = estado s.comoFundar:setText(string.format('No perteneces a ningún gremio. Para fundar uno, habla con Maese Ivo, el Registrador de Gremios, en la plaza de la Ciudad Alta de Vigilia: ' .. 'hace falta un líder de nivel %d, %s de oro y dos compañeros de otras cuentas que firmen el acta. También puedes unirte a un gremio que te invite.', e.nivel, tostring(e.coste):reverse():gsub('(%d%d%d)', '%1 '):reverse():gsub('^ ', ''))) s.invitaciones:destroyChildren() if #e.invitaciones == 0 then local l = g_ui.createWidget('GameTextMuted', s.invitaciones) l:setText('Nadie te ha invitado todavía.') return end for _, i in ipairs(e.invitaciones) do local w = g_ui.createWidget('GrInvitacion', s.invitaciones) w.nombre:setText(i.nombre) w.meta:setText(string.format('%d %s · líder: %s', i.miembros, i.miembros == 1 and 'miembro' or 'miembros', i.lider)) w.aceptar.onClick = function() enviar('A|' .. i.gid) end w.rechazar.onClick = function() enviar('D|' .. i.gid) end end end local function pintar() if not ventana or not estado then return end pintarCabecera() ventana:setHeight(estado.sin and math.max(440, 330 + #estado.invitaciones * 62) or 640) ventana.conGremio:setVisible(not estado.sin) ventana.sinGremio:setVisible(estado.sin == true) if estado.sin then return pintarSin() end for _, t in ipairs(ventana.conGremio.pestanas:getChildren()) do t:setChecked(t.clave == pestana) end ventana.conGremio.miembros:setVisible(pestana == 'M') ventana.conGremio.rangos:setVisible(pestana == 'R') pintarMiembros() pintarRangos() ventana.conGremio.pie.salir:setText(estado.yo.nivel == 3 and #estado.miembros == 1 and 'Disolver el gremio' or 'Salir del gremio') end -- Canal de gremio ----------------------------------------------------------------------------------------------------- function abrirCanal() local consola = modules.game_console local tab = consola and consola.getChannelTab and consola.getChannelTab(0) if tab and estado and not estado.sin and tab:getText() == estado.nombre then consola.consoleTabBar:selectTab(tab) return end pedirCanal = true g_game.requestChannels() end local function alListaDeCanales(lista) if not pedirCanal then return end pedirCanal = false local hay = false for _, c in ipairs(lista) do if tonumber(c[1]) == 0 and c[2] ~= 'NPCs' then hay = true end end -- la consola abre su ventana de canales con la misma lista: se cierra en cuanto aparece scheduleEvent(function() local consola = modules.game_console if consola and consola.channelsWindow then consola.channelsWindow:destroy() consola.channelsWindow = nil end if hay then g_game.joinChannel(0) else avisar('El canal del gremio estará listo al volver a entrar al juego.') end end, 1) end local function cerrarCanal() local consola = modules.game_console local tab = consola and consola.getChannelTab and consola.getChannelTab(0) if tab then consola.removeTab(tab) else g_game.leaveChannel(0) end end -- Acciones ------------------------------------------------------------------------------------------------------------ function invitar() local campo = ventana.conGremio.miembros.invitarNombre local n = campo:getText():gsub('^%s+', ''):gsub('%s+$', '') if n == '' then campo:focus() return avisar('Escribe el nombre del personaje que quieres invitar.') end enviar('V|' .. n:gsub('[%^~|]', '')) campo:setText('') end function editarCarta() local lado = ventana.conGremio.miembros.lado lado.cartaEdit:setText(estado and estado.carta or '') lado.carta:hide() lado.cartaEdit:show() lado.cartaEdit:focus() lado.guardarCarta:show() lado.cancelarCarta:show() lado.editarCarta:hide() end function cancelarCarta() local lado = ventana.conGremio.miembros.lado lado.cartaEdit:hide() lado.carta:show() lado.guardarCarta:hide() lado.cancelarCarta:hide() pintar() end function guardarCarta() local lado = ventana.conGremio.miembros.lado enviar('C|' .. lado.cartaEdit:getText():gsub('[\r\n]+', ' ')) cancelarCarta() end function salir() local b = ventana.conGremio.pie.salir local disolver = estado and estado.yo.nivel == 3 and #estado.miembros == 1 if dosPasos('salir', b, disolver and '¿Disolver? Otra vez' or '¿Salir? Pulsa otra vez') then enviar('L') end end function toggle() if not ventana or not g_game.isOnline() then return end if ventana:isVisible() then ventana:hide() return end enviar('P') pintar() ventana:show() ventana:raise() ventana:setOpacity(1) ventana:focus() end local function onOpcode(protocol, opcode, buffer) local tipo = buffer:sub(1, 1) if tipo == 'M' then return avisar(buffer:sub(3)) elseif tipo == 'S' then return cerrarCanal() elseif tipo == 'K' then return scheduleEvent(abrirCanal, 400) end if tipo ~= 'G' and tipo ~= 'N' then return end local antes = estado estado = leer(buffer) if antes and antes.gid ~= estado.gid then elegido = nil pestana = 'M' end pintar() end function gremioController:onInit() g_ui.importStyle('/game_viaje/valdren') g_ui.importStyle('estilos') ventana = g_ui.displayUI('gremio') if OtmmoVentanas then OtmmoVentanas.recordar(ventana, 'gremio') end ventana:hide() for _, t in ipairs({ { 'M', 'Miembros' }, { 'R', 'Rangos' } }) do local tab = g_ui.createWidget('GameTab', ventana.conGremio.pestanas) tab.clave = t[1] tab:setText(t[2]) tab.onClick = function() pestana = t[1] pintar() end end -- lo que viene en los pasos siguientes del plan: visible, apagado for _, n in ipairs({ 'Renombre', 'Encargos', 'Mejoras' }) do local tab = g_ui.createWidget('GameTab', ventana.conGremio.pestanas) tab:setText(n) tab:setEnabled(false) tab:setTooltip('Próximamente') end ProtocolGame.registerExtendedOpcode(OPCODE, onOpcode) connect(g_game, { onChannelList = alListaDeCanales }) Keybind.new('Ventanas', 'Gremio', { [CHAT_MODE.ON] = '', [CHAT_MODE.OFF] = 'G' }, '') Keybind.bind('Ventanas', 'Gremio', { { type = KEY_DOWN, callback = function() if not escribiendo() then toggle() end end } }) if g_game.isOnline() then self:onGameStart() end end function gremioController:onGameStart() enviar('P') end function gremioController:onGameEnd() estado, elegido, pedirCanal = nil, nil, false if ventana then ventana:hide() end end function gremioController:onTerminate() self:onGameEnd() ProtocolGame.unregisterExtendedOpcode(OPCODE) disconnect(g_game, { onChannelList = alListaDeCanales }) Keybind.delete('Ventanas', 'Gremio') if ventana then ventana:destroy() end ventana = nil end