-- OT-MMO: Admin Panel. Solo para admin: el servidor (server/scripts/admin.lua) dice por el opcode 140 «G|1» o «G|0» si el -- personaje lo es (grupo GM o marca del panel web); el cliente no puede saberlo solo (Canary no manda las acciones de GM -- y g_game.isGM() da falso). El botón vive al final del micromenú de game_hud. -- Tarjetas: Personaje abre la ventana de nivel y skills («N|», «S||»); Misiones, la de probar misiones -- («Q|||» -> «L|lista» / «D|detalle»); Artículos, el menú creativo de objetos («A||| -- » -> «I|página», «T|||» para dárselos); Hechizos, réplica del libro con Bloquear / -- Desbloquear («H|lista», «H|1|nombre», «H|0|nombre»); Talentos, el árbol sin reglas («R|estado», «R|rama|id», -- «R|rango|id|n», «R|vaciar»). El servidor revalida y aplica todo. local OPCODE = 140 local NIVEL_MAX = 100 -- tope del juego (server/canary/data/events/scripts/player.lua) -- id (también el ícono de iconos/grande/), texto local TARJETAS = { { 'personaje', 'Personaje' }, { 'misiones', 'Misiones' }, { 'articulos', 'Artículos' }, { 'hechizos', 'Hechizos' }, { 'talentos', 'Talentos' }, } adminController = Controller:new() local ventana, ventanaPj, ventanaMi, ventanaAr, ventanaHe, ventanaTa = nil, nil, nil, nil, nil, nil local esGM = false local sincronizando = false -- evita el eco barra <-> input local function enviar(texto) local protocol = g_game.getProtocolGame() if protocol then protocol:sendExtendedOpcode(OPCODE, texto) end end local function confirmar(titulo, texto, si) local caja local function cerrar() if caja then caja:destroy() caja = nil end end caja = displayGeneralBox(titulo, texto, { { text = 'Sí', callback = function() cerrar() si() end }, { text = 'No', callback = cerrar } }, function() cerrar() si() end, cerrar) end local function avisarHud() local hud = modules.game_hud if hud and hud.mostrarAdmin then hud.mostrarAdmin(esGM) end end function puede() return esGM end function abierta() return ventana ~= nil and ventana:isVisible() end function toggle() if not ventana or not esGM then return end if ventana:isVisible() then return ventana:hide() end ventana:show() ventana:raise() ventana:focus() end -- Personaje ---------------------------------------------------------------------------------------------------------- -- Cada fila (AdminStat): barrita + input sincronizados, recortados a [min, max]. Valores BASE (sin bonos de talentos ni -- equipo), que es lo que fija el servidor. id = lo que va en «S|||» (99 = nivel mágico; 0-6 = skills de -- Tibia). pct = avance hacia el siguiente (0-99 %), el mismo de las barras del HUD. local MAGICO = 99 local SKILLS = { -- por filas: dos por fila, una en cada columna { MAGICO, 'Nivel mágico', 0 }, { 5, 'Escudo', 10 }, { 2, 'Espada', 10 }, { 3, 'Hacha', 10 }, { 1, 'Maza', 10 }, { 4, 'Distancia', 10 }, { 0, 'Puños', 10 }, { 6, 'Pesca', 10 }, } local SKILL_MAX = 150 -- ponytail: tope del panel, no del juego; subirlo si algún día hace falta local filas = {} -- { w, id, min, max, actual = function(), avance = function() } -- Experiencia de inicio de un nivel (fórmula de Tibia; la misma de admin.lua y del tope en player.lua). local function expDeNivel(l) return math.floor(50 * l ^ 3 / 3 - 100 * l ^ 2 + 850 * l / 3 - 200) end local function miles(n) return (tostring(n):reverse():gsub('(%d%d%d)', '%1.'):reverse():gsub('^%.', '')) end local function nivelActual() local p = g_game.getLocalPlayer() return p and p:getLevel() or 1 end local function hayCambios() for _, f in ipairs(filas) do if f.leer() ~= f.actual() or f.leerPct() ~= f.avance() then return true end end return false end -- Línea bajo el nivel: experiencia exacta que quedará con lo tecleado. local function pintarExp() local f = filas[1] -- la del nivel if not (ventanaPj and f and f.id == 'nivel') then return end local v, p = f.leer(), f.leerPct() local base = expDeNivel(v) if v >= NIVEL_MAX then return ventanaPj.expNivel:setText('Experiencia: ' .. miles(base) .. ' \183 nivel máximo') end local sig = expDeNivel(v + 1) ventanaPj.expNivel:setText(('Experiencia: %s \183 siguiente nivel: %s'):format( miles(base + math.floor(p / 100 * (sig - base))), miles(sig))) end local function fijar(f, n, desde) n = math.max(f.min, math.min(f.max, math.floor(tonumber(n) or f.actual()))) sincronizando = true if desde ~= 'barra' then f.w.barra:setValue(n) end if desde ~= 'input' then f.w.valor:setText(tostring(n)) end sincronizando = false ventanaPj.aplicar:setEnabled(hayCambios()) pintarExp() return n end local function nuevaFila(w, id, nombre, min, max, actual, avance) local f = { w = w, id = id, min = min, max = max, actual = actual, avance = avance } f.leer = function() return math.max(min, math.min(max, tonumber(w.valor:getText()) or actual())) end f.leerPct = function() return math.min(99, tonumber(w.pct:getText()) or avance()) end w.pct.onTextChange = function(e, texto) if sincronizando then return end local limpio = texto:gsub('%D', '') if limpio ~= texto then return e:setText(limpio) end ventanaPj.aplicar:setEnabled(hayCambios()) pintarExp() end w.nombre:setText(nombre) w.barra:setMinimum(min) w.barra:setMaximum(max) w.barra.onValueChange = function(_, v) if not sincronizando then fijar(f, v, 'barra') end end w.valor.onTextChange = function(e, texto) if sincronizando then return end local limpio = texto:gsub('%D', '') if limpio ~= texto then return e:setText(limpio) end if limpio ~= '' then fijar(f, limpio, 'input') end end table.insert(filas, f) return f end -- Repinta los valores actuales; con «reiniciar», también barrita e input (al abrir). local function pintarPersonaje(reiniciar) if not ventanaPj then return end local p = g_game.getLocalPlayer() ventanaPj.nombre:setText(p and p:getName() or '') ventanaPj.nivel:setText('Nivel ' .. nivelActual()) for _, f in ipairs(filas) do f.w.actual:setText(('Nv. %d \183 %d %%'):format(f.actual(), f.avance())) if reiniciar == true then sincronizando = true f.w.pct:setText(tostring(f.avance())) sincronizando = false fijar(f, f.actual()) end end ventanaPj.aplicar:setEnabled(hayCambios()) pintarExp() end local function aplicar() for _, f in ipairs(filas) do local v, p = f.leer(), f.leerPct() if v ~= f.actual() or p ~= f.avance() then enviar(f.id == 'nivel' and ('N|' .. v .. '|' .. p) or ('S|' .. f.id .. '|' .. v .. '|' .. p)) end end end -- Clase: id de vocación base (server/xml/vocations.xml), nombre, ícono de la lista de personajes. El servidor dice la -- actual («C|») y hace el cambio (borra talentos y hechizos, recalcula vida/maná/capacidad y saca del juego). local CLASES_PJ = { { 4, 'Guardián', 'guardian' }, { 3, 'Cazador', 'cazador' }, { 1, 'Hechicero', 'hechicero' }, { 2, 'Druida', 'druida' }, { 11, 'Brujo', 'brujo' }, { 12, 'Bardo', 'bardo' }, { 13, 'Gunslinger', 'gunslinger' }, } local claseActual = nil local function pintarClases() for _, b in ipairs(ventanaPj.clases:getChildren()) do b:setOn(b:getId() == 'clase_' .. tostring(claseActual)) end end local function recibirClase(id) claseActual = tonumber(id) if ventanaPj then pintarClases() end end local function montarPersonaje() for _, c in ipairs(CLASES_PJ) do local b = g_ui.createWidget('AdmClase', ventanaPj.clases) b:setId('clase_' .. c[1]) b:setIcon('/images/login/clases/' .. c[3]) b:setTooltip(c[2]) b.onClick = function() if c[1] == claseActual then return end confirmar('Cambiar a ' .. c[2], 'Se borran tus talentos y hechizos aprendidos, se recalculan vida, maná y capacidad, y saldrás del juego ' .. 'para volver a entrar con la clase nueva.', function() enviar('C|' .. c[1]) end) end end local function jugador() return g_game.getLocalPlayer() end nuevaFila(ventanaPj.statNivel, 'nivel', 'Nivel', 1, NIVEL_MAX, nivelActual, function() local p = jugador() return p and p:getLevelPercent() or 0 end) for _, s in ipairs(SKILLS) do local id = s[1] local w = g_ui.createWidget('AdminStat', ventanaPj.skills) nuevaFila(w, id, s[2], s[3], SKILL_MAX, function() local p = jugador() if not p then return s[3] end return id == MAGICO and p:getBaseMagicLevel() or p:getSkillBaseLevel(id) end, function() local p = jugador() if not p then return 0 end return id == MAGICO and p:getMagicLevelPercent() or p:getSkillLevelPercent(id) end) end ventanaPj.aplicar.onClick = aplicar end function togglePersonaje() if not ventanaPj or not esGM then return end if ventanaPj:isVisible() then return ventanaPj:hide() end pintarPersonaje(true) enviar('C|estado') ventanaPj:show() ventanaPj:raise() ventanaPj:focus() end -- Misiones ----------------------------------------------------------------------------------------------------------- local misiones = {} -- [[id, nombre, zona, nivel, estado]] tal cual llega («L|») local elegida = nil -- id local ESTADOS = { [1] = { 'En curso', OtmmoTheme and OtmmoTheme.color.accent or '#C1A16B' }, [2] = { 'Hecha', '#9AA7AE' } } local function pintarFila(f, m) f.nombre:setText(m[2]) f.sub:setText(m[3] .. ' \183 nv ' .. m[4]) local e = ESTADOS[m[5]] f.estado:setText(e and e[1] or '') if e then f.estado:setColor(e[2]) end f:setBackgroundColor(m[1] == elegida and '#232C32' or '#00000000') end local function pintarLista() if not ventanaMi then return end local q = ventanaMi.buscar:getText():lower() local z = ventanaMi.zona:getCurrentOption() z = z and z.data or '' local mias = ventanaMi.soloMias:isChecked() local orden = ventanaMi.orden:getCurrentOption() orden = orden and orden.data or 'nivel' local vistas = {} for _, m in ipairs(misiones) do if (z == '' or m[3] == z) and (not mias or m[5] > 0) and (q == '' or m[1]:lower():find(q, 1, true) or m[2]:lower():find(q, 1, true)) then vistas[#vistas + 1] = m end end -- m = { id, nombre, zona, nivel, estado }; desempate siempre por nombre table.sort(vistas, function(a, b) if orden == 'zona' and a[3] ~= b[3] then return a[3] < b[3] end if orden ~= 'nombre' and a[4] ~= b[4] then return a[4] < b[4] end return a[2] < b[2] end) ventanaMi.lista:destroyChildren() local n = 0 for _, m in ipairs(vistas) do do local f = g_ui.createWidget('AdmMFila', ventanaMi.lista) f:setId('m_' .. m[1]) pintarFila(f, m) f.onClick = function() local antes = elegida and ventanaMi.lista:getChildById('m_' .. elegida) elegida = m[1] if antes then antes:setBackgroundColor('#00000000') end f:setBackgroundColor('#232C32') enviar('Q|ver|' .. m[1]) end n = n + 1 end end ventanaMi.total:setText(n .. ' de ' .. #misiones .. ' misiones') end local function recibirLista(d) misiones = d local zonas, vistas = {}, {} for _, m in ipairs(d) do if m[3] ~= '' and not vistas[m[3]] then vistas[m[3]] = true table.insert(zonas, m[3]) end end local actual = ventanaMi.zona:getCurrentOption() actual = actual and actual.data or '' ventanaMi.zona:clearOptions() ventanaMi.zona:addOption('Todas las zonas', '') for _, z in ipairs(zonas) do ventanaMi.zona:addOption(z, z) end ventanaMi.zona:setCurrentOptionByData(actual, true) pintarLista() end local function accion(nombre, i) if elegida then enviar('Q|' .. nombre .. '|' .. elegida .. '|' .. (i or '')) end end local function recibirDetalle(d) local det = ventanaMi.detalle det.vacio:hide() for _, w in ipairs({ det.nombre, det.sub, det.datos, det.tEtapas, det.etapas, det.acciones }) do w:show() end det.nombre:setText(d.nombre) det.sub:setText(table.concat({ d.id, d.zona, d.cadena ~= '' and d.cadena or nil }, ' \183 ')) local estado = ({ [0] = 'No la tienes', [1] = 'En curso', [2] = 'Entregada' })[d.estado] det.datos:setText(('Da: %s\nRecibe: %s\nNivel: %d%s%s\nEstado: %s'):format(d.da, d.recibe, d.nivel, d.previa ~= '' and ('\nPrevia: ' .. d.previa) or '', d.repetible ~= '' and (' \183 repetible ' .. d.repetible) or '', estado)) det.etapas:destroyChildren() for i, o in ipairs(d.objetivos) do local f = g_ui.createWidget('AdmMEtapa', det.etapas) f.texto:setText(o.texto) f.cuenta:setText(o.actual .. '/' .. o.total) local abierta = d.estado == 1 and o.actual < o.total f.mas:setEnabled(abierta) f.cumplir:setEnabled(abierta) f.mas.onClick = function() accion('avanzar', i) end f.cumplir.onClick = function() accion('cumplir', i) end end det.acciones.asignar:setEnabled(d.estado ~= 1) det.acciones.reiniciar:setEnabled(d.estado == 1) det.acciones.quitar:setEnabled(d.estado ~= 0) det.acciones.marcar:setEnabled(d.estado ~= 2) -- la lista refleja el estado nuevo sin pedirla entera otra vez for _, m in ipairs(misiones) do if m[1] == d.id then m[5] = d.estado local f = ventanaMi.lista:getChildById('m_' .. d.id) if f then pintarFila(f, m) end break end end end local function montarMisiones() local det = ventanaMi.detalle for _, w in ipairs({ det.nombre, det.sub, det.datos, det.tEtapas, det.etapas, det.acciones }) do w:hide() end ventanaMi.buscar.onTextChange = pintarLista ventanaMi.zona.onOptionChange = pintarLista for _, o in ipairs({ { 'Por nivel', 'nivel' }, { 'Por nombre', 'nombre' }, { 'Por zona', 'zona' } }) do ventanaMi.orden:addOption(o[1], o[2]) end ventanaMi.orden.onOptionChange = pintarLista ventanaMi.soloMias.onCheckChange = pintarLista det.acciones.asignar.onClick = function() accion('asignar') end det.acciones.marcar.onClick = function() accion('marcar') end det.acciones.entregar.onClick = function() accion('entregar') end det.acciones.reiniciar.onClick = function() confirmar('Reiniciar misión', 'Vuelve a empezar con todas las etapas a 0.', function() accion('reiniciar') end) end det.acciones.quitar.onClick = function() confirmar('Quitar misión', 'Se borra del todo, como si nunca la hubieras tenido.', function() accion('quitar') end) end end function toggleMisiones() if not ventanaMi then -- misiones.otui no cargó (error de OTUI en la terminal del cliente) return modules.game_textmessage.displayStatusMessage('Admin: la ventana de misiones no se pudo cargar.') end if not esGM then return end if ventanaMi:isVisible() then return ventanaMi:hide() end ventanaMi:show() ventanaMi:raise() ventanaMi:focus() enviar('Q|lista') if elegida then enviar('Q|ver|' .. elegida) end end -- Artículos ---------------------------------------------------------------------------------------------------------- -- Pestañas: id (lo entiende admin.lua), nombre, objeto que la representa (o ícono de línea). local PESTANAS = { { 'todo', 'Todo', 3043 }, { 'valdren', 'Equipo VALDREN', nil, '/images/game/hud/iconos/admin' }, { 'armas', 'Armas', 3264 }, { 'armaduras', 'Armaduras', 3357 }, { 'escudos', 'Escudos', 3409 }, { 'accesorios', 'Accesorios', 3048 }, { 'municion', 'Munición', 3447 }, { 'consumibles', 'Consumibles', 266 }, { 'contenedores', 'Contenedores', 2854 }, { 'otros', 'Otros', 3031 }, } local art = { pestana = 'todo', pagina = 1, paginas = 1, elegido = nil, buscando = nil, orden = 'nombre', dir = 'asc', sub = '', calidad = 0 } -- elegido = { id, nombre, valdren, nivel, ataque, defensa, armadura, vida, mana } (tal cual llega en «I|») -- Orden: id (lo entiende admin.lua), ícono de iconos/orden-*, nombre para el tooltip. local ORDENES = { { 'nombre', 'orden-nombre', 'nombre' }, { 'nivel', 'orden-nivel', 'nivel' }, { 'ataque', 'orden-ataque', 'ataque' }, { 'defensa', 'orden-defensa', 'defensa' }, { 'armadura', 'orden-armadura', 'armadura' }, { 'vida', 'orden-vida', 'vida' }, { 'mana', 'orden-mana', 'maná' }, } -- Subgrupos por pestaña: id (lo entiende admin.lua), nombre, objeto que lo representa. local PIEZAS = { { 'cabeza', 'Cabeza', 3351 }, { 'cuerpo', 'Cuerpo', 3357 }, { 'piernas', 'Piernas', 3557 }, { 'pies', 'Pies', 3552 } } local SUBGRUPOS = { armas = { { 'espada', 'Espadas', 3264 }, { 'maza', 'Mazas', 3286 }, { 'hacha', 'Hachas', 3276 }, { 'distancia', 'Distancia', 3350 }, { 'varita', 'Varitas y cetros', 3074 } }, armaduras = PIEZAS, valdren = { PIEZAS[1], PIEZAS[2], PIEZAS[3], PIEZAS[4], { 'mano', 'Mano (armas, escudos, libros)', 3264 }, { 'collar', 'Collares', 3054 }, { 'anillo', 'Anillos', 3048 }, { 'municion', 'Munición', 3447 } }, } -- Calidades (otmmo_calidades) y su color (ItemsDatabase.calidadColor); 0 = sin calidad. local CALIDADES = { { 0, 'Sin calidad', '#9AA7AE' }, { 1, 'Poco común', '#4cc94c' }, { 2, 'Rara', '#3d8fe0' }, { 3, 'Épica', '#b05ce6' } } -- «Nv 12 · Atq 30 · Def 20»: solo las cifras que tiene el objeto. local function cifras(it) local t = {} for i, r in ipairs({ 'Nv', 'Atq', 'Def', 'Arm', 'Vida', 'Maná' }) do local v = it[3 + i] or 0 if v > 0 then t[#t + 1] = r .. ' ' .. v end end return table.concat(t, ' \183 ') end local function pedirPagina() enviar(('A|%s|%d|%s|%s|%s|%s'):format(art.pestana, art.pagina, art.orden, art.dir, art.sub, ventanaAr.buscar:getText())) end local function elegir(it, slot) art.elegido = it for _, c in ipairs(ventanaAr.rejilla:getChildren()) do c:setBorderWidth(0) end if slot then slot:setBorderWidth(1) slot:setBorderColor('#C1A16B') end ventanaAr.elegido:setItemId(it and it[1] or 0) ventanaAr.nombre:setText(it and it[2] or 'Elige un objeto de la cuadrícula.') local c = it and cifras(it) or '' ventanaAr.detalle:setText(it and ('id ' .. it[1] .. (it[3] == 1 and ' \183 equipo VALDREN' or '') .. (c ~= '' and (' \183 ' .. c) or '')) or '') for _, b in ipairs(ventanaAr.calidad:getChildren()) do b:setEnabled(it ~= nil and it[3] == 1) end ventanaAr.dar:setEnabled(it ~= nil) ventanaAr.darUno:setEnabled(it ~= nil) end local function dar(cantidad) local it = art.elegido if not it then return end local calidad = it[3] == 1 and art.calidad or 0 enviar(('T|%d|%d|%d'):format(it[1], math.max(1, cantidad), calidad)) end local function recibirPagina(d) art.pagina, art.paginas = d.pagina, d.paginas ventanaAr.rejilla:destroyChildren() local seguia = art.elegido and art.elegido[1] art.elegido = nil for _, it in ipairs(d.items) do local slot = g_ui.createWidget('AdmASlot', ventanaAr.rejilla) slot:setItemId(it[1]) local c = cifras(it) slot:setTooltip(it[2] .. ' \183 id ' .. it[1] .. (c ~= '' and ('\n' .. c) or '')) slot.onClick = function() elegir(it, slot) end slot.onDoubleClick = function() elegir(it, slot) dar(1) end if it[1] == seguia then elegir(it, slot) end end if not art.elegido then elegir(nil) end ventanaAr.pagina:setText(('Página %d de %d \183 %d objetos'):format(d.pagina, d.paginas, d.total)) ventanaAr.anterior:setEnabled(d.pagina > 1) ventanaAr.siguiente:setEnabled(d.pagina < d.paginas) end -- Fila de subgrupos de la pestaña: «Todos» + una casilla por subgrupo; sin subgrupos, la fila queda a 0 de alto. local function pintarSubgrupos() local fila, lista = ventanaAr.subgrupos, SUBGRUPOS[art.pestana] fila:destroyChildren() fila:setHeight(lista and 34 or 0) fila:setMarginTop(lista and 6 or 0) if not lista then return end local todos = g_ui.createWidget('Button', fila) todos:setId('s_') todos:setText('Todos') todos:setSize({ width = 56, height = 34 }) todos:setOn(art.sub == '') todos.onClick = function() art.sub, art.pagina = '', 1 pintarSubgrupos() pedirPagina() end for _, g in ipairs(lista) do local b = g_ui.createWidget('AdmATab', fila) b:setId('s_' .. g[1]) b:setTooltip(g[2]) b.item:setItemId(g[3]) b:setOn(art.sub == g[1]) b.onClick = function() art.sub, art.pagina = g[1], 1 pintarSubgrupos() pedirPagina() end end end local function irPestana(id) art.pestana, art.pagina, art.sub = id, 1, '' for _, t in ipairs(ventanaAr.pestanas:getChildren()) do t:setOn(t:getId() == 'tab_' .. id) end pintarSubgrupos() pedirPagina() end local function pasar(delta) local p = math.max(1, math.min(art.paginas, art.pagina + delta)) if p ~= art.pagina then art.pagina = p pedirPagina() end end local function pintarOrden() for _, b in ipairs(ventanaAr.ordenes:getChildren()) do b:setOn(b:getId() == 'o_' .. art.orden) end local nombre = '' for _, o in ipairs(ORDENES) do if o[1] == art.orden then nombre = o[3]:gsub('^%l', string.upper) end end local asc = art.dir == 'asc' ventanaAr.sentido:setText(nombre) ventanaAr.flecha:setImageSource('/images/game/hud/iconos/' .. (asc and 'flecha-arriba' or 'flecha-abajo')) local explica = art.orden == 'nombre' and (asc and 'de la A a la Z' or 'de la Z a la A') or (asc and 'menor primero' or 'mayor primero') ventanaAr.flecha:setTooltip(nombre .. ': ' .. explica) ventanaAr.sentido:setTooltip(nombre .. ': ' .. explica) end local function pintarCalidad() for _, b in ipairs(ventanaAr.calidad:getChildren()) do b:setOn(b:getId() == 'c_' .. art.calidad) end end local function montarArticulos() for _, o in ipairs(ORDENES) do local b = g_ui.createWidget('GameIconButton', ventanaAr.ordenes) b:setId('o_' .. o[1]) b:setIcon('/images/game/hud/iconos/' .. o[2]) b:setTooltip('Ordenar por ' .. o[3] .. ' (otra vez: al revés)') b.onClick = function() if art.orden == o[1] then art.dir = art.dir == 'asc' and 'desc' or 'asc' else art.orden, art.dir = o[1], o[1] == 'nombre' and 'asc' or 'desc' -- cifras: lo más alto primero end art.pagina = 1 pintarOrden() pedirPagina() end end pintarOrden() for _, t in ipairs(PESTANAS) do local b = g_ui.createWidget('AdmATab', ventanaAr.pestanas) b:setId('tab_' .. t[1]) b:setTooltip(t[2]) if t[3] then b.item:setItemId(t[3]) else b.item:hide() b:setIcon(t[4]) end b.onClick = function() irPestana(t[1]) end end for _, c in ipairs(CALIDADES) do local b = g_ui.createWidget('AdmACalidad', ventanaAr.calidad) b:setId('c_' .. c[1]) b:setIcon('/images/game/hud/iconos/calidad') b:setIconColor(c[3]) b:setTooltip(c[2]) b.onClick = function() art.calidad = c[1] pintarCalidad() end end pintarCalidad() pintarSubgrupos() ventanaAr.buscar.onTextChange = function() removeEvent(art.buscando) art.buscando = scheduleEvent(function() art.pagina = 1 pedirPagina() end, 300) end ventanaAr.cantidad.onTextChange = function(e, texto) local limpio = texto:gsub('%D', '') if limpio ~= texto then e:setText(limpio) end end ventanaAr.anterior.onClick = function() pasar(-1) end ventanaAr.siguiente.onClick = function() pasar(1) end ventanaAr.rejilla.onMouseWheel = function(_, _, dir) pasar(dir == MouseWheelDown and 1 or -1) return true end ventanaAr.darUno.onClick = function() dar(1) end ventanaAr.dar.onClick = function() dar(tonumber(ventanaAr.cantidad:getText()) or 1) end elegir(nil) end function toggleArticulos() if not ventanaAr or not esGM then return end if ventanaAr:isVisible() then return ventanaAr:hide() end ventanaAr:show() ventanaAr:raise() ventanaAr:focus() irPestana(art.pestana) end -- Hechizos ----------------------------------------------------------------------------------------------------------- -- Presentación con SpellInfo (como game_libro.hechizos); el estado de los aprendibles lo manda el servidor («H|»). local TIPOS_H = { maestro = 'Maestro de clase', firma = 'Firma de rama', activa = 'Activa del árbol de talentos' } local hz = { pestana = 'clase', elegido = nil, aprendibles = {} } -- aprendibles[nombre] = { tipo, sabe } local function esFirmaH(s) -- mismo criterio que game_libro.esFirma + activas del árbol return s.rama or s.talento or (s.id >= 201 and s.id <= 210) or (s.id >= 213 and s.id <= 215) end local function hechizosDeClase(ramas) local yo = g_game.getLocalPlayer() local v = yo and translateVocation(yo:getVocation()) or 0 local raza = modules.game_talentos and modules.game_talentos.getRaza and modules.game_talentos.getRaza() or 'humano' local lista = {} for _, s in pairs(SpellInfo.Default) do if table.contains(s.vocations, v) and (esFirmaH(s) and true or false) == ramas and (not s.raza or s.raza == raza) then table.insert(lista, s) end end table.sort(lista, function(a, b) return a.level == b.level and a.id < b.id or a.level < b.level end) return lista end -- Estado para pintar: texto, ¿desbloqueado?, aprendible (tipo) o nil si es por nivel. local function estadoH(s) local a = hz.aprendibles[s.name] if a then return a.sabe and 'Desbloqueado' or 'Bloqueado', a.sabe, a.tipo end local yo = g_game.getLocalPlayer() local ok = (yo and yo:getLevel() or 1) >= s.level return (ok and 'Disponible' or 'Bloqueado') .. ' por nivel (' .. s.level .. ')', ok, nil end local function iconoH(widget, s) widget:setImageSource(SpelllistSettings.Default.iconFile) widget:setImageClip(Spells.getImageClip(s.clientId, 'Default')) end local pintarHechizos local function fichaH(s) local f = ventanaHe.ficha hz.elegido = s for _, w in ipairs({ f.marco, f.nombre, f.tipo, f.datos, f.estado }) do w:setVisible(s ~= nil) end f.vacia:setVisible(s == nil) f.desbloquear:hide() f.bloquear:hide() if not s then return end local texto, abierto, tipo = estadoH(s) iconoH(f.marco.icono, s) f.marco.icono:setOpacity(abierto and 1 or 0.35) f.nombre:setText(s.name) f.tipo:setText(tipo and TIPOS_H[tipo] or 'Hechizo de clase por nivel') f.datos:setText(('Palabras: %s\nNivel: %d\nId: %d'):format(s.words or '', s.level, s.id)) f.estado:setText(texto .. (tipo and '' or '. Para probarlo, cambia tu nivel en Personaje.')) f.estado:setColor(abierto and '#C1A16B' or '#9AA7AE') if tipo then if abierto then f.bloquear:show() else f.desbloquear:show() end end end pintarHechizos = function() if not ventanaHe then return end for _, p in ipairs(ventanaHe.pestanas:getChildren()) do p:setChecked(p:getId() == hz.pestana) end ventanaHe.rejilla:destroyChildren() local lista = hechizosDeClase(hz.pestana == 'ramas') local sigue = nil for _, s in ipairs(lista) do local w = g_ui.createWidget('AdmHCelda', ventanaHe.rejilla) local _, abierto = estadoH(s) iconoH(w.marco.icono, s) w.nombre:setText(s.name) if not abierto then w.marco.icono:setOpacity(0.35) w.nombre:setColor('#9AA7AE80') end w.marco:setBorderColor(s == hz.elegido and '#C1A16B' or '#39444C') w.onClick = function() fichaH(s) pintarHechizos() end if s == hz.elegido then sigue = s end end fichaH(sigue) end local function recibirHechizos(d) hz.aprendibles = {} for _, a in ipairs(d.aprendibles or {}) do hz.aprendibles[a[1]] = { tipo = a[2], sabe = a[3] == 1 } end pintarHechizos() end local function montarHechizos() for _, p in ipairs({ { 'clase', 'Clase' }, { 'ramas', 'Ramas y talentos' } }) do local w = g_ui.createWidget('GameTab', ventanaHe.pestanas) w:setId(p[1]) w:setText(p[2]) w:setTextAutoResize(true) w.onClick = function() hz.pestana, hz.elegido = p[1], nil pintarHechizos() end end local f = ventanaHe.ficha f.desbloquear.onClick = function() if hz.elegido then enviar('H|1|' .. hz.elegido.name) end end f.bloquear.onClick = function() if hz.elegido then enviar('H|0|' .. hz.elegido.name) end end end function toggleHechizos() if not ventanaHe or not esGM then return end if ventanaHe:isVisible() then return ventanaHe:hide() end ventanaHe:show() ventanaHe:raise() ventanaHe:focus() pintarHechizos() enviar('H|lista') end -- Talentos ----------------------------------------------------------------------------------------------------------- -- Presentación: modules.game_talentos.getRamas() (los datos que ya recibe cualquier jugador). Estado (rama activa y -- rangos) siempre del servidor («R|»). Decisiones 28-oct: sin reglas; rangos solo en la rama activa. local TIPOS_T = { atributo = 'Pasivo', mejora = 'Mejora', critico = 'Pasivo', disparador = 'Pasivo', amenaza = 'Pasivo', activa = 'Activa' } local ta = { vista = nil, elegido = nil, espec = nil, rangos = {}, puntos = 0, asignados = 0, reintento = nil } local function ramasT() local g = modules.game_talentos return g and g.getRamas and g.getRamas() end local function clipT(w, icono) w:setImageClip({ x = (icono or 0) * 32, y = 0, width = 32, height = 32 }) end local function conRangoT(texto, rango) return ((texto or ''):gsub('{(.-)}', function(s) local p = s:split('/') return p[math.max(1, math.min(#p, rango))] end)) end local pintarTalentos local function fichaT(r, t) local f = ventanaTa.ficha for _, w in ipairs({ f.marco, f.nombre, f.tipo, f.rango, f.efecto, f.requisito, f.controles }) do w:setVisible(t ~= nil) end f.vacia:setVisible(t == nil) if not t then return end local n = ta.rangos[t.id] or 0 local activa = r.id == ta.espec clipT(f.marco.icono, t.icono) f.marco.icono:setOpacity(n > 0 and 1 or 0.45) f.marco:setBorderColor(n > 0 and '#C1A16B' or '#39444C') f.nombre:setText(t.nombre) f.tipo:setText(('%s \183 fila %d'):format(TIPOS_T[t.tipo] or 'Pasivo', t.fila)) f.rango:setText('Rango ' .. n .. '/' .. t.rangos) f.efecto:setText(conRangoT(t.efecto, math.max(1, n))) local req = '' if t.requiere then for _, o in ipairs(r.talentos) do if o.id == t.requiere then req = 'En el juego necesita ' .. o.nombre .. ' al máximo (aquí no se exige).' end end end f.requisito:setText(activa and req or 'Rama no activa: usa esta rama para asignar rangos.') local c = f.controles c.menos:setEnabled(activa and n > 0) c.cero:setEnabled(activa and n > 0) c.mas:setEnabled(activa and n < t.rangos) c.maximo:setEnabled(activa and n < t.rangos) c.menos.onClick = function() enviar(('R|rango|%s|%d'):format(t.id, n - 1)) end c.mas.onClick = function() enviar(('R|rango|%s|%d'):format(t.id, n + 1)) end c.maximo.onClick = function() enviar(('R|rango|%s|%d'):format(t.id, t.rangos)) end c.cero.onClick = function() enviar(('R|rango|%s|0'):format(t.id)) end end pintarTalentos = function() if not ventanaTa then return end local lista = ramasT() ventanaTa.pestanas:destroyChildren() ventanaTa.arbol:destroyChildren() ventanaTa.puntos:setText(('Puntos del nivel: %d \183 asignados: %d'):format(ta.puntos, ta.asignados)) if not lista then ventanaTa.estadoRama:setText('Cargando los talentos de tu clase...') ventanaTa.usarRama:hide() fichaT(nil, nil) return end ta.vista = ta.vista or ta.espec or (lista[1] and lista[1].id) local r = nil for _, x in ipairs(lista) do local w = g_ui.createWidget('GameTab', ventanaTa.pestanas) w:setId(x.id) w:setText(x.nombre .. (x.id == ta.espec and ' \183 activa' or '')) w:setTextAutoResize(true) w:setChecked(x.id == ta.vista) w.onClick = function() ta.vista, ta.elegido = x.id, nil pintarTalentos() end if x.id == ta.vista then r = x end end if not r then return end local activa = r.id == ta.espec ventanaTa.estadoRama:setText(activa and ('Rama activa. Firma: ' .. (r.firma and r.firma.nombre or '')) or (ta.espec and 'No es tu rama activa.' or 'Aún no tienes rama.')) ventanaTa.usarRama:setVisible(not activa) ventanaTa.usarRama.onClick = function() confirmar('Cambiar de rama', 'Pasas a ' .. r.nombre .. ' sin coste y se borran tus rangos actuales.', function() enviar('R|rama|' .. r.id) end) end local filas, elegidoT = {}, nil for _, t in ipairs(r.talentos or {}) do filas[t.fila] = filas[t.fila] or {} table.insert(filas[t.fila], t) if t.id == ta.elegido then elegidoT = t end end for f = 1, #filas do local fila = g_ui.createWidget('AdmTFila', ventanaTa.arbol) fila.titulo:setText('Fila ' .. f) for _, t in ipairs(filas[f] or {}) do local n = ta.rangos[t.id] or 0 local c = g_ui.createWidget('AdmTCelda', fila.celdas) clipT(c.marco.icono, t.icono) c.marco.icono:setOpacity((activa and n > 0) and 1 or 0.4) c.marco:setBorderColor(t.id == ta.elegido and '#C1A16B' or '#39444C') c.rango:setText((activa and n or 0) .. '/' .. t.rangos) c:setTooltip(t.nombre) c.onClick = function() ta.elegido = t.id pintarTalentos() end end end fichaT(r, elegidoT) end local function recibirTalentos(d) ta.espec = type(d.espec) == 'string' and d.espec or nil ta.rangos = type(d.rangos) == 'table' and d.rangos or {} ta.puntos, ta.asignados = d.puntos or 0, d.asignados or 0 pintarTalentos() end local function montarTalentos() ventanaTa.pie.onClick = function() confirmar('Vaciar rangos', 'Todos los talentos de tu rama activa vuelven a 0.', function() enviar('R|vaciar') end) end end function toggleTalentos() if not ventanaTa or not esGM then return end if ventanaTa:isVisible() then return ventanaTa:hide() end ventanaTa:show() ventanaTa:raise() ventanaTa:focus() if not ramasT() and modules.game_talentos and modules.game_talentos.pedirDatos then modules.game_talentos.pedirDatos() -- los datos de presentación llegan a game_talentos; se repinta en un momento removeEvent(ta.reintento) ta.reintento = scheduleEvent(pintarTalentos, 1200) end enviar('R|estado') end local ACCIONES = { personaje = togglePersonaje, misiones = toggleMisiones, articulos = toggleArticulos, hechizos = toggleHechizos, talentos = toggleTalentos, } local function onOpcode(protocol, opcode, buffer) local tipo = buffer:sub(1, 2) if tipo == 'C|' then return recibirClase(buffer:sub(3)) end if (tipo == 'L|' or tipo == 'D|') and ventanaMi or tipo == 'I|' and ventanaAr or tipo == 'H|' and ventanaHe or tipo == 'R|' and ventanaTa then local ok, d = pcall(json.decode, buffer:sub(3)) if ok and d then if tipo == 'L|' then recibirLista(d) elseif tipo == 'D|' then recibirDetalle(d) elseif tipo == 'I|' then recibirPagina(d) elseif tipo == 'H|' then recibirHechizos(d) else recibirTalentos(d) end end return end if tipo ~= 'G|' then return end esGM = buffer:sub(3, 3) == '1' if not esGM then if ventana then ventana:hide() end if ventanaPj then ventanaPj:hide() end if ventanaMi then ventanaMi:hide() end if ventanaAr then ventanaAr:hide() end if ventanaHe then ventanaHe:hide() end if ventanaTa then ventanaTa:hide() end end avisarHud() end function adminController:onInit() g_ui.importStyle('/game_viaje/valdren') -- marco VALDREN (modules/game_viaje/valdren.otui) ventana = g_ui.displayUI('admin') ventanaPj = g_ui.displayUI('personaje') ventanaMi = g_ui.displayUI('misiones') ventanaAr = g_ui.displayUI('articulos') ventanaHe = g_ui.displayUI('hechizos') ventanaTa = g_ui.displayUI('talentos') OtmmoVentanas.recordar(ventana, 'admin') OtmmoVentanas.recordar(ventanaPj, 'adminPersonaje') OtmmoVentanas.recordar(ventanaMi, 'adminMisiones') OtmmoVentanas.recordar(ventanaAr, 'adminArticulos') OtmmoVentanas.recordar(ventanaHe, 'adminHechizos') OtmmoVentanas.recordar(ventanaTa, 'adminTalentos') ventana:hide() ventanaPj:hide() ventanaMi:hide() ventanaAr:hide() ventanaHe:hide() ventanaTa:hide() for _, t in ipairs(TARJETAS) do local w = g_ui.createWidget('AdminTarjeta', ventana.tarjetas) w:setId(t[1]) w:setIcon('/images/game/hud/iconos/grande/' .. t[1]) w:setText(t[2]) w.onClick = ACCIONES[t[1]] or function() modules.game_textmessage.displayStatusMessage(t[2] .. ': próximamente.') end end montarPersonaje() montarMisiones() montarArticulos() montarHechizos() montarTalentos() local ok, v = pcall(g_resources.readFileContents, '/data/version-cliente.txt') ventana.version:setText('Cliente: ' .. (ok and v and v:gsub('%s+$', '') or 'sin sello (compilado a mano)')) ProtocolGame.registerExtendedOpcode(OPCODE, onOpcode) if g_game.isOnline() then -- recargado a mitad de partida self:onGameStart() end end function adminController:onGameStart() esGM = false avisarHud() adminController:registerEvents(LocalPlayer, { onLevelChange = pintarPersonaje, onBaseSkillChange = pintarPersonaje, onBaseMagicLevelChange = pintarPersonaje, onSkillChange = pintarPersonaje, onMagicLevelChange = pintarPersonaje, -- también cambian solo con el % }) -- Se pide al entrar: lo que el servidor manda en el primer instante del login puede perderse. scheduleEvent(function() enviar('P') end, 1000) end function adminController:onGameEnd() esGM = false if ventana then ventana:hide() end if ventanaPj then ventanaPj:hide() end if ventanaMi then ventanaMi:hide() end if ventanaAr then ventanaAr:hide() end if ventanaHe then ventanaHe:hide() end if ventanaTa then ventanaTa:hide() end avisarHud() end function adminController:onTerminate() ProtocolGame.unregisterExtendedOpcode(OPCODE) filas, misiones, elegida = {}, {}, nil removeEvent(art.buscando) removeEvent(ta.reintento) for _, w in ipairs({ ventana, ventanaPj, ventanaMi, ventanaAr, ventanaHe, ventanaTa }) do if w then w:destroy() end end ventana, ventanaPj, ventanaMi, ventanaAr, ventanaHe, ventanaTa = nil, nil, nil, nil, nil, nil end