-- OT-MMO: consejos del tutorial (vault OT-MMO/diseno/tutorial.md). Ventanitas que salen la PRIMERA vez que pasa algo, -- estilo WoW: icono, título, una a tres frases, la tecla dibujada y una flecha sobre el botón de la interfaz. -- - Cola: nunca dos a la vez; entre uno y otro, una pausa. Lo urgente (abatido, vida baja) pasa delante. -- - No toman el foco: nada es focusable, se sigue caminando y escribiendo con la ventanita abierta. -- - Qué consejos vio cada PERSONAJE y si están apagados (por CUENTA) lo guarda el servidor (server/scripts/consejos.lua, -- opcode 171). Sin respuesta del servidor no sale ninguno. -- - Libro de la Vigía (K, y en el menú «Más»): todos los consejos ya vistos, para repasarlos. -- - Los de clase (Bardo, Gunslinger) salen en el primer combate de un personaje de esa clase. -- - Opciones > Interfaz: «Consejos del tutorial» y «Volver a ver los consejos». -- Opcode 171: cliente «H» hola · «V|id» visto · «A|1/0» encender/apagar · «R» volver a verlos; -- servidor «E|1^id,id» estado · «T|id» consejo que pide el servidor (botín, viaje, dungeon, abatido...). -- El catálogo (textos, teclas, a qué apunta) está en catalogo.lua. Este archivo está en cp1252. local OPCODE = 171 local IMG = '/game_consejos/images/' local ESPERA_INICIO = 3000 -- tras entrar, antes del primero (deja pasar el cartel de zona) local ENTRE = 3000 -- pausa entre un consejo y el siguiente local AUTO_CIERRE = 60000 -- si nadie lo cierra, se va solo (queda en el libro) -- Prioridad en la cola: 1 urgente, 2 de lo que tienes delante ahora (caduca si te alejas), 3 el resto. local PRIORIDAD = { abatido = 1, vida = 1, botin = 1, atacar = 2, misiones = 2, entregar = 2, dungeon = 2, viaje = 2, habilidades = 2, crafteo = 2, tablon = 2, mercado = 2, recoleccion = 2 } local CADUCA = { atacar = true, misiones = true, entregar = true, dungeon = true } -- se vuelven a pedir cada segundo local VIGENCIA = 5000 local COMBO = { Ctrl = true, Alt = true, ['Mayús'] = true } local CLASE_CONSEJO = { [7] = 'bardo', [8] = 'gunslinger' } -- clientid de vocations.xml (Bardo, Gunslinger) consejosController = Controller:new() local POR_ID = {} for i, c in ipairs(ConsejosCatalogo.lista) do POR_ID[c.id] = c c.orden = i end local estado = { conocido = false, activos = true, vistos = {} } local sesion = { inicio = 0, nivel0 = nil } local cola, actual = {}, nil local ventana, flecha, aro, libro = nil, nil, nil, nil local ciclo, anim, cierreEv, siguienteEv = nil, nil, nil, nil local libertad = 0 -- g_clock.millis() a partir del que puede salir el siguiente local pintando = false local elegido = nil -- consejo abierto en el libro local ultimoError = nil local function enviar(texto) local protocol = g_game.getProtocolGame() if protocol then protocol:sendExtendedOpcode(OPCODE, texto) end end local function activo(c) return c and c.activo ~= false end local function cuantosVistos() local n, total = 0, 0 for _, c in ipairs(ConsejosCatalogo.lista) do if activo(c) then total = total + 1 if estado.vistos[c.id] then n = n + 1 end end end return n, total end local function aviso(texto) if modules.game_textmessage and modules.game_textmessage.displayStatusMessage then modules.game_textmessage.displayStatusMessage(texto) end end -- Teclas dibujadas: «Ctrl B» se une con «+»; «W A S D» van sueltas. local function pintarTeclas(panel, c) panel:destroyChildren() local t = c.teclas or {} for i, k in ipairs(t) do if i > 1 and COMBO[t[1]] then g_ui.createWidget('ConsejoMas', panel):setText('+') end local w = g_ui.createWidget('ConsejoTecla', panel) w:setText(k) if w:getWidth() < 28 then w:setWidth(28) end end if #t > 0 and c.teclasTexto then g_ui.createWidget('ConsejoTeclaPie', panel):setText(c.teclasTexto) end panel:setHeight(#t > 0 and 26 or 0) panel:setMarginTop(#t > 0 and 10 or 0) end -- Flecha y aro sobre el elemento de la interfaz ---------------------------------------------------------------------- local function objetivoDe(c) if not c or not c.senalar then return nil end local w = rootWidget:recursiveGetChildById(c.senalar) if w and w:isVisible() and w:getWidth() > 0 then local p = w:getParent() while p do if not p:isVisible() then return nil end p = p:getParent() end return w end end local function colocarFlecha() local w = actual and objetivoDe(actual) if not w then flecha:hide() aro:hide() return end local r = w:getRect() local vaiven = math.floor(math.sin(g_clock.millis() / 160) * 4) aro:setRect({ x = r.x - 3, y = r.y - 3, width = r.width + 6, height = r.height + 6 }) aro:setOpacity(0.55 + 0.45 * math.abs(math.sin(g_clock.millis() / 420))) local ancho, alto = rootWidget:getWidth(), rootWidget:getHeight() local cx, cy = r.x + math.floor(r.width / 2), r.y + math.floor(r.height / 2) if cy < alto * 0.3 and cx > ancho * 0.7 then -- columna de botones junto al minimapa: la flecha llega de lado para no tapar el botón de abajo flecha:setImageSource(IMG .. 'flecha-derecha') flecha:resize(44, 36) flecha:setPosition({ x = r.x - 52 + vaiven, y = cy - 18 }) elseif cy > alto / 2 then flecha:setImageSource(IMG .. 'flecha-abajo') flecha:resize(36, 44) flecha:setPosition({ x = cx - 18, y = r.y - 50 + vaiven }) else flecha:setImageSource(IMG .. 'flecha-arriba') flecha:resize(36, 44) flecha:setPosition({ x = cx - 18, y = r.y + r.height + 6 - vaiven }) end flecha:show() aro:show() flecha:raise() aro:raise() end -- La ventanita ------------------------------------------------------------------------------------------------------- local function ajustarAlto() if not ventana or not actual then return end ventana:setHeight(ventana.entendido:getY() + ventana.entendido:getHeight() - ventana:getY() + 16) end local function pararAnim() if anim then removeEvent(anim) anim = nil end end local function entrar() pararAnim() local t0 = g_clock.millis() ventana:setOpacity(0) ventana:setMarginRight(-4) anim = cycleEvent(function() local k = math.min(1, (g_clock.millis() - t0) / 220) local e = 1 - (1 - k) ^ 4 -- ease-out-quart ventana:setOpacity(e) ventana:setMarginRight(math.floor(20 + 24 * (1 - e))) ajustarAlto() if actual then colocarFlecha() end if k >= 1 and not actual then pararAnim() end end, 16) end local function ocupado() if libro and libro:isVisible() then return true end for _, id in ipairs({ 'conteoDungeon', 'ventanaDungeon', 'grupoListo' }) do local w = rootWidget:recursiveGetChildById(id) if w and w:isVisible() then return true end end return false end local programar local function mostrar(c) actual = c estado.vistos[c.id] = true enviar('V|' .. c.id) ventana.icono:setImageSource(IMG .. 'iconos/' .. c.icono) ventana.titulo:setText(c.titulo) ventana.texto:setText(c.texto) pintarTeclas(ventana.teclas, c) ventana:show() ventana:raise() entrar() if cierreEv then removeEvent(cierreEv) end cierreEv = scheduleEvent(function() cierreEv = nil modules.game_consejos.cerrar() end, AUTO_CIERRE) if c.id ~= 'guia' and cuantosVistos() >= 3 and not estado.vistos.guia then modules.game_consejos.pedir('guia') end if libro and libro:isVisible() then modules.game_consejos.pintarLibro() end end local function siguiente() siguienteEv = nil if actual or not estado.activos or not g_game.isOnline() then return end if ocupado() then siguienteEv = scheduleEvent(siguiente, 1000) return end local ahora = g_clock.millis() while #cola > 0 do local e = table.remove(cola, 1) local c = POR_ID[e.id] if activo(c) and not estado.vistos[e.id] and (not e.hasta or e.hasta >= ahora) then return mostrar(c) end end end programar = function() if actual or siguienteEv or #cola == 0 then return end siguienteEv = scheduleEvent(siguiente, math.max(50, libertad - g_clock.millis())) end function pedir(id) local c = POR_ID[id] if not activo(c) or not estado.conocido or not estado.activos or estado.vistos[id] then return end if actual and actual.id == id then return end local hasta = CADUCA[id] and (g_clock.millis() + VIGENCIA) or nil for _, e in ipairs(cola) do if e.id == id then e.hasta = hasta return end end local pr = PRIORIDAD[id] or 3 local pos = #cola + 1 for i, e in ipairs(cola) do if (PRIORIDAD[e.id] or 3) > pr then pos = i break end end table.insert(cola, pos, { id = id, hasta = hasta }) programar() end local function ocultar() pararAnim() if cierreEv then removeEvent(cierreEv) cierreEv = nil end actual = nil if ventana then ventana:hide() end if flecha then flecha:hide() end if aro then aro:hide() end end function cerrar() if not actual then return end ocultar() libertad = g_clock.millis() + ENTRE programar() end -- Encender, apagar, volver a ver --------------------------------------------------------------------------------------- local function sincronizarCasillas() pintando = true if modules.client_options and modules.client_options.getOption and modules.client_options.getOption('consejosTutorial') ~= estado.activos then modules.client_options.setOption('consejosTutorial', estado.activos) end if libro then libro.pie.activos:setChecked(estado.activos) end pintando = false end -- Desde Opciones, el libro o la ventanita. Sin conexión no se toca nada: la preferencia vive en la cuenta. function setActivos(v) v = v and true or false if pintando or v == estado.activos or not g_game.isOnline() then return end estado.activos = v enviar('A|' .. (v and '1' or '0')) if not v then cola = {} ocultar() end sincronizarCasillas() end function apagarDesdeVentana() setActivos(false) aviso('Consejos apagados. Puedes encenderlos en Opciones o en el Libro de la Vigía (K).') end function desdeLibro(v) if not pintando then setActivos(v) end end function reiniciar() if not g_game.isOnline() then return aviso('Entra con tu personaje para volver a ver los consejos.') end if not estado.activos then setActivos(true) end enviar('R') estado.vistos, cola = {}, {} ocultar() sesion.nivel0 = nil aviso('Los consejos volverán a salir mientras juegas.') if libro and libro:isVisible() then pintarLibro() end end -- Lo que detecta el cliente (cada segundo) ------------------------------------------------------------------------------ local function visible(id) local w = rootWidget:recursiveGetChildById(id) return w and w:isVisible() end local function revisar() if not estado.conocido or not estado.activos or not g_game.isOnline() then return end local yo = g_game.getLocalPlayer() if not yo then return end local v = estado.vistos local jugado = g_clock.millis() - sesion.inicio local nivel = yo:getLevel() if not sesion.nivel0 then sesion.nivel0 = nivel end if nivel > sesion.nivel0 then pedir('nivel') end for _, n in ipairs({ { 3, 'mapa' }, { 4, 'hogar' }, { 5, 'maestro' }, { 8, 'buscador' }, { 10, 'talentos' }, { 10, 'monturas' }, { 15, 'pvp' }, { 20, 'gremio' }, { 100, 'heroicas' } }) do if nivel >= n[1] then pedir(n[2]) end end if jugado > 60000 then pedir('chat') end if jugado > 120000 then pedir('camara') end if yo:getMaxHealth() > 0 and yo:getHealth() / yo:getMaxHealth() < 0.5 then pedir('vida') end if g_game.isAttacking() then pedir('habilidades') local deClase = CLASE_CONSEJO[yo:getVocation()] if deClase then pedir(deClase) end if not v.objetivo then -- con dos enemigos vivos cerca, cambiar de objetivo con Tab local p, n = yo:getPosition(), 0 for _, c in ipairs(g_map.getSpectators(p, false)) do local q = c:getPosition() if c:isMonster() and q.z == p.z and c:getHealthPercent() > 0 and math.max(math.abs(q.x - p.x), math.abs(q.y - p.y)) <= 7 then n = n + 1 end end if n >= 2 then pedir('objetivo') end end end if yo.isPartyMember and yo:isPartyMember() then pedir('grupo') end if not (v.atacar and v.chat and v.misiones and v.entregar) then local p = yo:getPosition() local marcas = modules.game_misiones and modules.game_misiones.getMarcas and modules.game_misiones.getMarcas() or {} for _, c in ipairs(g_map.getSpectators(p, false)) do local q = c:getPosition() local d = math.max(math.abs(q.x - p.x), math.abs(q.y - p.y)) if q.z == p.z then if c:isMonster() and d <= 6 and c:getHealthPercent() > 0 then pedir('atacar') end if c:isNpc() then if d <= 3 then pedir('chat') end local m = marcas[c:getName()] if m == '!' and d <= 8 then pedir('misiones') end if m == '?' and d <= 8 then pedir('entregar') end end end end end if not (v.diario and v.brillo) and modules.game_misiones and modules.game_misiones.getActivas then local activas = modules.game_misiones.getActivas() or {} if #activas > 0 then pedir('diario') end for _, m in ipairs(activas) do for _, d in ipairs(m.donde or {}) do if d.tipo == 'recoger' or d.tipo == 'caza' then pedir('brillo') end end end end if not v.crafteo and visible('crafteo') then pedir('crafteo') end if not v.tablon and visible('tablon') then pedir('tablon') end if not v.mercado and visible('mercado') then pedir('mercado') end if not v.intercambio and visible('ventanaIntercambio') then pedir('intercambio') end if not v.logros and visible('avisoLogro') then pedir('logros') end end -- Libro de la Vigía --------------------------------------------------------------------------------------------------- local function detalle(c) local d = libro.detalle elegido = c if not c then d.icono:setImageSource(IMG .. 'iconos/guia') d.titulo:setText('Todavía no hay consejos') d.cuando:setText('') d.texto:setText('Irán saliendo mientras juegas, la primera vez que pase cada cosa, y quedarán guardados aquí.') pintarTeclas(d.teclas, {}) return end d.icono:setImageSource(IMG .. 'iconos/' .. c.icono) d.titulo:setText(c.titulo) d.cuando:setText(c.cuando) d.texto:setText(c.texto) pintarTeclas(d.teclas, c) for _, f in ipairs(libro.lista:getChildren()) do if f.consejo then f:setChecked(f.consejo == c) end end end function pintarLibro() if not libro then return end local lista = libro.lista lista:destroyChildren() local primero, sigue = nil, false for _, g in ipairs(ConsejosCatalogo.grupos) do local cabecera = nil for _, c in ipairs(ConsejosCatalogo.lista) do if c.grupo == g and activo(c) and estado.vistos[c.id] then if not cabecera then cabecera = g_ui.createWidget('LibroGrupo', lista) cabecera:setText(g) end local f = g_ui.createWidget('LibroFila', lista) f.consejo = c f.icono:setImageSource(IMG .. 'iconos/' .. c.icono) f.nombre:setText(c.titulo) f.onClick = function() detalle(c) end primero = primero or c if elegido == c then sigue = true end end end end local n, total = cuantosVistos() libro.resumen:setText(n .. ' de ' .. total .. ' consejos vistos') if n < total then local falta = g_ui.createWidget('LibroFalta', lista) falta:setText(total - n == 1 and 'Falta uno: saldrá cuando llegue su momento.' or ('Faltan ' .. (total - n) .. ': saldrán cuando llegue su momento.')) end detalle(sigue and elegido or primero) sincronizarCasillas() end function toggleLibro() if not libro then return end if libro:isVisible() then libro:hide() else pintarLibro() libro:show() libro:raise() libro:focus() end end -- Opcode ------------------------------------------------------------------------------------------------------------- local function onOpcode(protocol, opcode, buffer) local tipo, resto = buffer:sub(1, 1), buffer:sub(3) if tipo == 'E' then local on, ids = resto:match('^(%d)%^(.*)$') estado.conocido = true estado.activos = on ~= '0' estado.vistos = {} for id in (ids or ''):gmatch('[%w_]+') do estado.vistos[id] = true end if not estado.activos then cola = {} ocultar() end sincronizarCasillas() pedir('bienvenida') if libro and libro:isVisible() then pintarLibro() end elseif tipo == 'T' then pedir(resto) end end local function escribiendo() local w = rootWidget while w:getFocusedChild() do w = w:getFocusedChild() end return w:getClassName() == 'UITextEdit' and w:isVisible() end function consejosController:onInit() g_ui.importStyle('/game_viaje/valdren') g_ui.importStyle('consejos') local raiz = modules.game_interface.getRootPanel() ventana = g_ui.createWidget('ConsejoVentana', raiz) ventana:addAnchor(AnchorRight, 'parent', AnchorRight) ventana:addAnchor(AnchorVerticalCenter, 'parent', AnchorVerticalCenter) ventana:setMarginRight(20) ventana:setMarginTop(90) ventana:hide() flecha = g_ui.createWidget('ConsejoFlecha', rootWidget) flecha:hide() aro = g_ui.createWidget('ConsejoAro', rootWidget) aro:hide() libro = g_ui.displayUI('libro') if OtmmoVentanas then OtmmoVentanas.recordar(libro, 'libroVigia') end libro:hide() ProtocolGame.registerExtendedOpcode(OPCODE, onOpcode) Keybind.new('Ventanas', 'Libro de la Vigía', { [CHAT_MODE.ON] = '', [CHAT_MODE.OFF] = 'K' }, '') Keybind.bind('Ventanas', 'Libro de la Vigía', { { type = KEY_DOWN, callback = function() if not escribiendo() then toggleLibro() end end } }) ciclo = cycleEvent(function() local ok, e = pcall(revisar) if not ok and e ~= ultimoError then ultimoError = e g_logger.warning('[consejos] ' .. tostring(e)) end end, 1000) if g_game.isOnline() then self:onGameStart() end end function consejosController:onGameStart() estado.conocido = false sesion.inicio, sesion.nivel0 = g_clock.millis(), nil cola = {} libertad = g_clock.millis() + ESPERA_INICIO enviar('H') end function consejosController:onGameEnd() ocultar() cola = {} estado.conocido = false if siguienteEv then removeEvent(siguienteEv) siguienteEv = nil end if libro then libro:hide() end end function consejosController:onTerminate() self:onGameEnd() if ciclo then removeEvent(ciclo) ciclo = nil end ProtocolGame.unregisterExtendedOpcode(OPCODE) Keybind.delete('Ventanas', 'Libro de la Vigía') for _, w in ipairs({ ventana, flecha, aro, libro }) do if w then w:destroy() end end ventana, flecha, aro, libro = nil, nil, nil, nil end -- Estado para las pruebas: modules.game_consejos.depurar() function depurar() local v = {} for id in pairs(estado.vistos) do v[#v + 1] = id end table.sort(v) local c = {} for _, e in ipairs(cola) do c[#c + 1] = e.id end return string.format('conocido=%s activos=%s actual=%s cola=%s vistos=%s error=%s', tostring(estado.conocido), tostring(estado.activos), actual and actual.id or '-', table.concat(c, ','), table.concat(v, ','), tostring(ultimoError)) end -- Para probar sin servidor: modules.game_consejos.probar('atacar') function probar(id) estado.conocido, estado.activos = true, true estado.vistos[id] = nil pedir(id) end