-- OT-MMO: libro de hechizos (P), estilo WoW con la piel del juego. Pestañas Clase y Rama de talentos, cuadrícula de -- habilidades y una ficha que explica la que señalas (datos.lua). Se arrastran a la barra de atajos o, con clic derecho, -- van al primer hueco libre (mismo camino que ponerEnBarra de game_talentos: ApiJson.createOrUpdateText + updateButton). -- La barra de OTClient solo acepta soltar objetos, así que el arrastre es propio: al soltar se busca la casilla bajo -- el ratón (id «barra.botón») y se le asignan las palabras del hechizo. local CLASES = { [8] = 'Guardián', [7] = 'Cazador', [5] = 'Hechicero', [6] = 'Druida', [11] = 'Brujo' } -- translateVocation() -- Tokens (corelib/ui/theme.lua): acento = elegida o en la barra; borde = reposo; borde claro = hover. local ORO, CENIZA, APAGADO = OtmmoTheme.color.accent, OtmmoTheme.color.border, OtmmoTheme.color.text2 local HOVER, BLOQUEADA = OtmmoTheme.color.borderHover, OtmmoTheme.color.textDisabled libroController = Controller:new() local ventana = nil local pestana = 'clase' local elegido = nil -- hechizo fijado con clic (la ficha vuelve a él al quitar el ratón) local flotante = nil -- ícono que sigue al ratón mientras se arrastra local function escribiendo() local w = rootWidget while w:getFocusedChild() do w = w:getFocusedChild() end return w:getClassName() == 'UITextEdit' and w:isVisible() end local function vocacion() local yo = g_game.getLocalPlayer() return yo and translateVocation(yo:getVocation()) or 0 end local function nivel() local yo = g_game.getLocalPlayer() return yo and yo:getLevel() or 1 end local function esFirma(s) -- habilidades de las ramas: 201-210, las del Brujo 213-215 y las marcadas rama = true (23-oct) return s.rama or (s.id >= 201 and s.id <= 210) or (s.id >= 213 and s.id <= 215) end local function raza() local t = modules.game_talentos return t and t.getRaza and t.getRaza() or 'humano' end local function aprendida(nombre) local t = modules.game_talentos and modules.game_talentos.getAprendidos and modules.game_talentos.getAprendidos() return t and t[nombre] or false end -- Ramas de talentos: { [nombre de la firma] = nombre de la rama } y la firma de la rama elegida. local function ramas() local lista, espec = nil, nil if modules.game_talentos and modules.game_talentos.getRamas then lista, espec = modules.game_talentos.getRamas() end local porFirma, mia, nombreRama, activas = {}, nil, nil, {} for _, r in ipairs(lista or {}) do if r.firma then porFirma[r.firma.nombre] = r.nombre if r.id == espec then mia, nombreRama = r.firma.nombre, r.nombre end end for _, t in ipairs(r.talentos or {}) do -- activas del árbol: [nombre] = { rama, fila, mía } if t.tipo == 'activa' then activas[t.nombre] = { rama = r.nombre, fila = t.fila, mia = r.id == espec } end end end return porFirma, mia, nombreRama, lista ~= nil, activas end local function hechizos(firmas) local v, lista = vocacion(), {} local ocultas = {} -- las viejas de una versión «Gran» ya aprendida no salen (la Gran ocupa su sitio) for _, s in pairs(SpellInfo.Default) do if s.reemplaza and aprendida(s.name) then ocultas[s.reemplaza] = true end end for _, s in pairs(SpellInfo.Default) do if table.contains(s.vocations, v) and esFirma(s) == firmas and not ocultas[s.name] and (not s.raza or s.raza == raza()) then -- las raciales, solo las de tu raza 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 -- ¿Se puede usar ya? Devuelve nil o el motivo para pintarla en gris. local function bloqueo(s) if s.talento then -- activa del árbol: se gana poniendo el punto local _, _, _, _, activas = ramas() local a = activas[s.name] if aprendida(s.name) then return nil end if not a then return 'Habilidad de otra rama' end return (a.mia and '' or ('Rama ' .. a.rama .. ': ')) .. 'talento de la fila ' .. a.fila .. ' del árbol (N)' end if esFirma(s) then local porFirma, mia = ramas() if mia ~= s.name then local rama = porFirma[s.name] and ('Habilidad de la rama ' .. porFirma[s.name]) or 'Habilidad de otra rama' return mia and rama or (rama .. ': elige tu rama de talentos (N)') end end if s.level > nivel() then return 'Nivel ' .. s.level end if s.maestro and not aprendida(s.name) then -- se compra con el maestro (server/scripts/clases/maestro_precios.lua) return 'Apréndela con Corvo, el maestro de clase del templo' end end local function segundos(ms) local s = ms / 1000 return (s == math.floor(s) and tostring(math.floor(s)) or (string.format('%.1f', s):gsub('%.', ','))) .. ' s' end -- Botón de la barra que ya tiene el hechizo (para «en tu barra: tecla 3»). local function enBarra(palabras) local api = modules.game_actionbar and modules.game_actionbar.ApiJson if not api then return nil end for b = 1, 10 do local m = api.getMapping(1, b) local a = m and m.actionsetting if a and a.chatText == palabras then local tecla = tostring(b % 10) for _, e in ipairs(api.getHotkeyEntries('chatOff') or {}) do if e.actionsetting and e.actionsetting.action == 'TriggerActionButton_1.' .. b and e.keysequence then tecla = e.keysequence end end return tecla end end end local function icono(widget, s) widget:setImageSource(SpelllistSettings.Default.iconFile) widget:setImageClip(Spells.getImageClip(s.clientId, 'Default')) end local function dato(clave, valor) local w = g_ui.createWidget('LibroDato', ventana.ficha.datos) w.clave:setText(clave) w.valor:setText(valor) end local function mostrarFicha(s) local f = ventana.ficha f:setVisible(s ~= nil) if not s then return end local d = LIBRO_DATOS[s.name] or {} local motivo = bloqueo(s) icono(f.marco.icono, s) f.marco.icono:setOpacity(motivo and 0.4 or 1) f.marco:setBorderColor(motivo and CENIZA or ORO) f.nombre:setText(s.name) f.tipo:setText((d.tipo or 'Habilidad') .. ' \183 ' .. (d.carga and ('carga de ' .. segundos(d.carga)) or 'instantánea')) f.datos:destroyChildren() dato('Maná', tostring(s.mana)) dato('Recarga', segundos(s.exhaustion)) dato('Alcance', s.range and s.range > 0 and (s.range == 1 and 'cuerpo a cuerpo' or (s.range .. ' casillas')) or (d.area and 'a tu alrededor' or 'en ti')) dato('Nivel', tostring(s.level)) dato('Palabras', s.words) f.texto:setText(d.texto or '') local c = d.cifra if c then local l = nivel() local a, b = math.floor(c[1] + c[2] * l), math.floor(c[3] + c[4] * l) f.cifra:setText('A tu nivel: ' .. (a == b and a or (a .. ' a ' .. b)) .. ' de ' .. c[5] .. ' (los talentos lo suben).') else f.cifra:setText('') end local tecla = enBarra(s.words) if motivo then f.estado:setText(motivo .. '.') f.estado:setColor(APAGADO) elseif tecla then f.estado:setText('En tu barra: tecla ' .. tecla .. '.') f.estado:setColor(OtmmoTheme.color.text) else f.estado:setText('No está en tu barra: arrástrala o usa clic derecho.') f.estado:setColor(APAGADO) end end -- Asignar a la barra --------------------------------------------------------------------------------------------------- local function asignar(barra, boton, palabras, widget) local ab = modules.game_actionbar local api = ab and ab.ApiJson if not api then return false end api.createOrUpdateText(barra, boton, palabras, true) api.saveData() -- el hueco vacío es un UIWidget; updateButton lo cambia por un botón de verdad (como al arrastrar un objeto) local w = widget if not w then local b = ab.actionBars and ab.actionBars[barra] w = b and b.tabBar:getChildById(barra .. '.' .. boton) end if w and ab.updateButton then ab.updateButton(w) end return true end local function primerHueco(s) local api = modules.game_actionbar and modules.game_actionbar.ApiJson if not api then return end if enBarra(s.words) then return modules.game_textmessage.displayStatusMessage(s.name .. ' ya está en tu barra.') end for b = 1, 10 do local m = api.getMapping(1, b) local a = m and m.actionsetting if not (a and next(a)) then asignar(1, b, s.words) modules.game_textmessage.displayStatusMessage(s.name .. ': tecla ' .. (b % 10) .. '.') return mostrarFicha(s) end end modules.game_textmessage.displayFailureMessage('Tu barra está llena: arrastra la habilidad sobre una casilla.') end local function casillaBajo(pos) local w = rootWidget:recursiveGetChildByPos(pos, false) while w do local id = w:getId() or '' local barra, boton = id:match('^(%d+)%.(%d+)$') if barra and w:getParent() and w:getParent():getId() == 'tabBar' then return tonumber(barra), tonumber(boton), w end w = w:getParent() end end local function soltarFlotante() if flotante then flotante:destroy() flotante = nil end end -- Celdas ----------------------------------------------------------------------------------------------------------------- local function celda(s) local w = g_ui.createWidget('LibroCelda', ventana.rejilla) local motivo = bloqueo(s) icono(w.marco.icono, s) w.nombre:setText(s.name) if motivo then w.marco.icono:setOpacity(0.35) w.nombre:setColor(BLOQUEADA) end w.marco:setBorderColor(s == elegido and ORO or CENIZA) w.onHoverChange = function(self, hovered) self.marco:setBorderColor(s == elegido and ORO or (hovered and HOVER or CENIZA)) mostrarFicha(hovered and s or elegido) end w.onMouseRelease = function(self, mousePos, boton) if boton == MouseRightButton then if bloqueo(s) then modules.game_textmessage.displayFailureMessage(bloqueo(s) .. '.') else primerHueco(s) end return true end elegido = s for _, c in ipairs(ventana.rejilla:getChildren()) do c.marco:setBorderColor(c == self and ORO or CENIZA) end mostrarFicha(s) return false end if not motivo then w:setDraggable(true) w.onDragEnter = function(self, mousePos) soltarFlotante() flotante = g_ui.createWidget('UIWidget', rootWidget) flotante:setSize({ width = 32, height = 32 }) flotante:setPhantom(true) flotante:setFocusable(false) icono(flotante, s) flotante:setPosition({ x = mousePos.x - 16, y = mousePos.y - 16 }) g_mouse.pushCursor('target') return true end w.onDragMove = function(self, mousePos, moved) if flotante then flotante:setPosition({ x = mousePos.x - 16, y = mousePos.y - 16 }) flotante:raise() -- la ventana del libro se sube al pulsar y lo taparía end return true end w.onDragLeave = function(self, droppedWidget, mousePos) g_mouse.popCursor('target') soltarFlotante() local barra, boton, destino = casillaBajo(mousePos) if barra and asignar(barra, boton, s.words, destino) then mostrarFicha(s) end return true end end return w end local function pintar() if not ventana then return end local _, mia, nombreRama, conDatos = ramas() for _, p in ipairs(ventana.pestanas:getChildren()) do p:setChecked(p:getId() == pestana) end ventana.pestanas.clase:setText(CLASES[vocacion()] or 'Clase') ventana.pestanas.rama:setText(nombreRama and ('Rama: ' .. nombreRama) or 'Rama de talentos') -- 23-oct: si se repinta con un arrastre en curso, la celda arrastrada se destruye sin pasar por su onDragLeave -- (sus campos Lua ya no existen): el ícono flotante y el cursor de mira se quedaban pegados. if flotante then g_mouse.popCursor('target') soltarFlotante() end ventana.rejilla:destroyChildren() local lista = hechizos(pestana == 'rama') if not elegido or not table.contains(lista, elegido) then elegido = nil for _, s in ipairs(lista) do -- la primera que ya puede usar; si no hay, la primera if not elegido and not bloqueo(s) then elegido = s end end elegido = elegido or lista[1] end for _, s in ipairs(lista) do celda(s) end ventana.vacia:setVisible(#lista == 0) ventana.vacia:setText('Tu clase todavía no tiene habilidades aquí.') if pestana == 'rama' and not mia then ventana.vacia:setVisible(true) ventana.vacia:setText(nivel() < 10 and 'A nivel 10 eliges una rama de talentos (N) y ganas su habilidad propia. Estas son las de tu clase:' or 'Elige tu rama de talentos (N) para ganar su habilidad propia. Estas son las de tu clase:') ventana.rejilla:setMarginTop(76) -- debajo del aviso (3 líneas) else ventana.rejilla:setMarginTop(12) end if not conDatos and modules.game_talentos and modules.game_talentos.pedirDatos then modules.game_talentos.pedirDatos() -- llegan por el opcode de talentos y vuelven a pintar (alCambiarRama) end mostrarFicha(elegido) end function alCambiarRama() if ventana and ventana:isVisible() then pintar() end end function elegirPestana(id) pestana = id elegido = nil pintar() end function toggle() if not ventana then return end if ventana:isVisible() then return ventana:hide() end pintar() ventana:show() ventana:raise() ventana:focus() end function libroController:onInit() ventana = g_ui.displayUI('libro') OtmmoVentanas.recordar(ventana, 'libro') -- posición entre sesiones (fase 4) ventana:hide() for _, p in ipairs({ { 'clase', 'Clase' }, { 'rama', 'Rama de talentos' } }) do local w = g_ui.createWidget('GameTab', ventana.pestanas) w:setId(p[1]) w:setText(p[2]) w:setTextAutoResize(true) w.onClick = function() elegirPestana(p[1]) end end Keybind.new('Ventanas', 'Libro de hechizos', { [CHAT_MODE.ON] = '', [CHAT_MODE.OFF] = 'P' }, '') Keybind.bind('Ventanas', 'Libro de hechizos', { { type = KEY_DOWN, callback = function() if not escribiendo() then toggle() end end } }) end function libroController:onGameEnd() soltarFlotante() if ventana then ventana:hide() end end function libroController:onTerminate() soltarFlotante() Keybind.delete('Ventanas', 'Libro de hechizos') if ventana then ventana:destroy() ventana = nil end end