-- OT-MMO: ventana de talentos (N). Solo dibuja y planea: el servidor (server/scripts/talentos/talentos.lua) manda -- las ramas de la clase, los puntos y los rangos aprendidos, y valida cada cambio. Opcode extendido 109: -- «D|» datos (y estado) · «M|texto» aviso -- El cliente envía «P» (pedir datos), «S|rama» (elegir especialización) y «A|id:n,id:n» (aplicar el plan). -- Reglas del árbol (las mismas que el servidor): la fila f se abre con 5*(f-1) puntos en el árbol; un requisito -- debe estar al máximo; un punto planeado solo se quita si no deja huérfano a otro. local OPCODE = 109 local LADO, PASO_X, PASO_Y, MARGEN = 40, 64, 56, 34 -- Tokens (corelib/ui/theme.lua). Acento = al máximo o con puntos libres; borde claro = se puede sumar. local C = OtmmoTheme.color local BRASA, ORO, VETA_ON, VETA_OFF, CENIZA, APAGADO = C.accent, C.accent, C.text2, C.border, C.border, C.text2 local TIPOS = { atributo = 'Pasivo', mejora = 'Mejora', critico = 'Pasivo', disparador = 'Pasivo', amenaza = 'Pasivo', mascota = 'Mascota', activa = 'Habilidad nueva' } talentosController = Controller:new() local ventana, datos, plan = nil, nil, {} local abrirAlLlegar, confirmando = false, nil local function enviar(texto) local protocol = g_game.getProtocolGame() if protocol then protocol:sendExtendedOpcode(OPCODE, texto) end end local function aviso(texto) modules.game_textmessage.displayStatusMessage(texto) end local function ramaActual() for _, r in ipairs(datos.ramas) do if r.id == datos.espec then return r end end end local function talento(r, id) for _, t in ipairs(r.talentos) do if t.id == id then return t end end end local function total(id) return (datos.rangos[id] or 0) + (plan[id] or 0) end -- puntos en el árbol; con «antes», solo los de filas anteriores a esa local function gastados(r, antes) local n = 0 for _, t in ipairs(r.talentos) do if not antes or t.fila < antes then n = n + total(t.id) end end return n end local function libres(r) return datos.puntos - gastados(r) end local function filaAbierta(r, fila) return gastados(r) >= 5 * (fila - 1) end local function requisitoOk(r, t) local q = t.requiere and talento(r, t.requiere) return not q or total(q.id) >= q.rangos end local function puedeSumar(r, t) return libres(r) > 0 and total(t.id) < t.rangos and filaAbierta(r, t.fila) and requisitoOk(r, t) end local function puedeQuitar(r, t) if (plan[t.id] or 0) <= 0 then return false end plan[t.id] = plan[t.id] - 1 local ok = true for _, u in ipairs(r.talentos) do if total(u.id) > 0 and (gastados(r, u.fila) < 5 * (u.fila - 1) or not requisitoOk(r, u)) then ok = false break end end plan[t.id] = plan[t.id] + 1 return ok end local function planeados() local n = 0 for _, v in pairs(plan) do n = n + v end return n end -- «Haces {1/2/3} % más daño.» con el rango 2 da «Haces 2 % más daño.» local function conRango(texto, rango) return (texto:gsub('{(.-)}', function(s) local p = s:split('/') return p[math.max(1, math.min(#p, rango))] end)) end local function clip(w, icono) w:setImageClip({ x = (icono or 0) * 32, y = 0, width = 32, height = 32 }) end local function datosActiva(a) local p = {} if a.mana and a.mana > 0 then table.insert(p, a.mana .. ' de man\225') end if a.espera_s and a.espera_s > 0 then table.insert(p, a.espera_s .. ' s de espera') end if a.carga_ms and a.carga_ms > 0 then table.insert(p, (string.format('carga %.1f s', a.carga_ms / 1000):gsub('%.', ','))) end if a.alcance and a.alcance > 0 then table.insert(p, 'alcance ' .. a.alcance) end local lineas = {} -- de dos en dos: la ficha es estrecha y no debe partir «carga 1,2 s» for i = 1, #p, 2 do table.insert(lineas, table.concat({ p[i], p[i + 1] }, ' \183 ')) end return table.concat(lineas, '\n') end -- Ficha de la derecha: el talento bajo el ratón o, sin nada, la habilidad propia de la rama. local function mostrarFicha(t) local r = ramaActual() local f = ventana.arbolVista.ficha f.requisito:setText('') if not t then local firma = r.firma clip(f.icono, firma.icono) f.marco:setBorderColor(ORO) f.nombre:setText(firma.nombre) f.tipo:setText('Habilidad propia de ' .. r.nombre) f.datos:setText(datosActiva(firma)) f.tAhora:setText('Qu\233 hace') f.ahora:setText(firma.efecto) f.tSiguiente:setText('') f.siguiente:setText('') return end local n = total(t.id) clip(f.icono, t.icono) f.marco:setBorderColor(n > 0 and ORO or C.border) f.nombre:setText(t.nombre) f.tipo:setText(string.format('%s \183 fila %d', TIPOS[t.tipo] or 'Pasivo', t.fila)) local d = 'Rango ' .. n .. '/' .. t.rangos if t.activa then d = d .. '\n' .. datosActiva(t.activa) end f.datos:setText(d) if n == 0 then f.tAhora:setText('Primer rango') f.ahora:setText(conRango(t.efecto, 1)) f.tSiguiente:setText('') f.siguiente:setText('') else f.tAhora:setText('Ahora') f.ahora:setText(conRango(t.efecto, n)) f.tSiguiente:setText(n < t.rangos and 'Siguiente rango' or 'Al m\225ximo') f.siguiente:setText(n < t.rangos and conRango(t.efecto, n + 1) or '') end f.requisito:setMarginTop(f.siguiente:getText() == '' and -16 or 12) if n < t.rangos then local q = t.requiere and talento(r, t.requiere) if not filaAbierta(r, t.fila) then f.requisito:setText(string.format('Necesitas %d puntos en %s (tienes %d).', 5 * (t.fila - 1), r.nombre, gastados(r))) elseif q and total(q.id) < q.rangos then f.requisito:setText(string.format('Necesitas %s %d/%d.', q.nombre, q.rangos, q.rangos)) elseif libres(r) <= 0 then f.requisito:setText('No te quedan puntos libres.') end end end local function pintarCuadro(r, t, w) local n, pend = total(t.id), plan[t.id] or 0 local abierto = filaAbierta(r, t.fila) and requisitoOk(r, t) local borde, ancho, color = CENIZA, 1, APAGADO if n >= t.rangos then borde, ancho, color = ORO, 2, ORO elseif n > 0 then borde, color = ORO, C.text elseif puedeSumar(r, t) then borde, color = C.borderHover, C.text end w:setBorderWidth(ancho) w:setBorderColor(borde) w.icono:setImageColor(abierto and '#ffffff' or C.text2) w.icono:setOpacity(abierto and 1 or 0.55) w.rango:setText(n .. '/' .. t.rangos) w.rango:setColor(color) w.rango:setBorderColor(pend > 0 and BRASA or CENIZA) w.rango:setBackgroundColor(pend > 0 and C.surface or C.bg) end local function pintarArbol() local r = ramaActual() local v = ventana.arbolVista v.espec:setText(r.nombre) v.rol:setText(r.rol) v.raya:setBackgroundColor(r.color or CENIZA) -- el único toque de color de la rama local l = libres(r) v.libres:setText(l == 1 and '1 punto libre' or (l .. ' puntos libres')) v.libres:setColor(l > 0 and BRASA or APAGADO) v.gastados:setText(string.format('%d en el \225rbol \183 nivel %d', gastados(r), datos.nivel)) for _, t in ipairs(r.talentos) do pintarCuadro(r, t, v.arbol['t_' .. t.id]) end for _, veta in ipairs(v.arbol:getChildren()) do if veta.req then local q = talento(r, veta.req) veta:setBackgroundColor(total(q.id) >= q.rangos and VETA_ON or VETA_OFF) end end for f = 1, 7 do v.arbol['f_' .. f]:setColor(filaAbierta(r, f) and APAGADO or C.textDisabled) end local p = planeados() v.pie.aplicar:setText(p > 0 and ('Aplicar (' .. p .. ')') or 'Aplicar') v.pie.aplicar:setEnabled(p > 0) v.pie.deshacer:setEnabled(p > 0) v.pie.reinicio:setText(string.format('Reiniciar cuesta %d de oro con %s', datos.reinicio.precio, datos.reinicio.donde)) end local function veta(arbol, req, x, y, w, h) local s = g_ui.createWidget('TlVeta', arbol) s.req = req s:addAnchor(AnchorTop, 'parent', AnchorTop) s:addAnchor(AnchorLeft, 'parent', AnchorLeft) s:setMarginLeft(x) s:setMarginTop(y) s:resize(w, h) end local function pos(t) return MARGEN + (t.col - 1) * PASO_X, (t.fila - 1) * PASO_Y end -- Construye los cuadros y las vetas (flechas) de la rama elegida; luego pintarArbol les da el estado. local function armarArbol() local r = ramaActual() local arbol = ventana.arbolVista.arbol arbol:destroyChildren() for f = 1, 7 do local e = g_ui.createWidget('TlFila', arbol) e:setId('f_' .. f) e:setText(tostring(5 * (f - 1))) e:addAnchor(AnchorTop, 'parent', AnchorTop) e:addAnchor(AnchorLeft, 'parent', AnchorLeft) e:setMarginTop((f - 1) * PASO_Y + 13) end for _, t in ipairs(r.talentos) do -- vetas primero: quedan debajo de los cuadros local q = t.requiere and talento(r, t.requiere) if q then local qx, qy = pos(q) local tx, ty = pos(t) local cx = tx + LADO / 2 - 1 if q.col == t.col then veta(arbol, q.id, cx, qy + LADO, 2, ty - qy - LADO) else local desde = q.col < t.col and (qx + LADO) or (cx + 2) veta(arbol, q.id, math.min(desde, cx), qy + LADO / 2 - 1, math.abs(cx - desde) + 2, 2) veta(arbol, q.id, cx, qy + LADO / 2, 2, ty - qy - LADO / 2) end for i, ancho in ipairs({ 8, 6, 4, 2 }) do -- punta de flecha sobre el cuadro veta(arbol, q.id, tx + (LADO - ancho) / 2, ty - 5 + i, ancho, 1) end end end for _, t in ipairs(r.talentos) do local w = g_ui.createWidget('TlCuadro', arbol) w:setId('t_' .. t.id) local x, y = pos(t) w:addAnchor(AnchorTop, 'parent', AnchorTop) w:addAnchor(AnchorLeft, 'parent', AnchorLeft) w:setMarginLeft(x) w:setMarginTop(y) clip(w.icono, t.icono) w.onHoverChange = function(_, dentro) mostrarFicha(dentro and t or nil) end w.onMouseRelease = function(_, _, boton) if boton == MouseLeftButton and puedeSumar(r, t) then plan[t.id] = (plan[t.id] or 0) + 1 elseif boton == MouseRightButton and puedeQuitar(r, t) then plan[t.id] = plan[t.id] - 1 if plan[t.id] == 0 then plan[t.id] = nil end else return true end pintarArbol() mostrarFicha(t) return true end end end local function pintarEleccion() local e = ventana.eleccion local falta = datos.nivelEspec - datos.nivel e.aviso:setText(falta > 0 and string.format('Eliges tu camino a nivel %d. Te faltan %d niveles.', datos.nivelEspec, falta) or 'Tu clase se divide aqu\237. Cambiar despu\233s cuesta oro con el maestro de clase.') e.ramas:destroyChildren() confirmando = nil for _, r in ipairs(datos.ramas) do local c = g_ui.createWidget('TlRama', e.ramas) c:setId('r_' .. r.id) c.veta:setBackgroundColor(r.color or ORO) clip(c.icono, r.firma.icono) c.nombre:setText(r.nombre) c.rol:setText(r.rol) c.lema:setText(r.lema) c.firma:setText(r.firma.nombre .. ': ' .. r.firma.efecto) c.elegir:setText('Elegir ' .. r.nombre) c.elegir:setEnabled(falta <= 0) c.elegir.onClick = function() if confirmando ~= r.id then -- dos pasos, sin ventana emergente for _, otra in ipairs(e.ramas:getChildren()) do otra.elegir:setText('Elegir ' .. otra:getId():sub(3):gsub('^%l', string.upper)) otra:setBorderColor(CENIZA) end confirmando = r.id c.elegir:setText('Confirmar ' .. r.nombre) c:setBorderColor(BRASA) return end enviar('S|' .. r.id) end end if #datos.ramas == 2 then e.ramas:setWidth(2 * 196 + 10) end end local function pintar() local elegir = not datos.espec ventana.eleccion:setVisible(elegir) ventana.arbolVista:setVisible(not elegir) ventana:setText(datos.claseNombre .. ': talentos') if elegir then pintarEleccion() else armarArbol() pintarArbol() mostrarFicha(nil) end end -- Hechizos aprendidos (needLearn: maestro de clase y activas del árbol), del servidor por el opcode 115 «L|n;n…» -- (server/scripts/clases/maestro.lua). Los usa game_libro para pintar en gris lo que aún no sabes. local aprendidos = {} local raza = 'humano' -- «Z|raza» del opcode 115 (server/scripts/razas/razas.lua) -- «R|viejas|nuevas»: versión «Gran …» aprendida; en todas las barras, las palabras viejas pasan a ser las nuevas. local function reemplazarEnBarras(viejas, nuevas) local ab = modules.game_actionbar local api = ab and ab.ApiJson if not api or not viejas or not nuevas then return end local cambio = false for _, e in pairs(api.getMappings() or {}) do local a = e.actionsetting if a and a.chatText == viejas and e.actionBar and e.actionButton then api.createOrUpdateText(e.actionBar, e.actionButton, nuevas, a.sendAutomatically ~= false) cambio = true local barra = ab.actionBars and ab.actionBars[e.actionBar] local w = barra and barra.tabBar:getChildById(e.actionBar .. '.' .. e.actionButton) if w and ab.updateButton then ab.updateButton(w) end end end if cambio then api.saveData() end end -- Al elegir rama, su habilidad propia va al primer hueco de la barra principal (como WoW). Botones 1-10. local function ponerEnBarra(palabras) local ab = modules.game_actionbar local api = ab and ab.ApiJson if not api or not palabras then return end local hueco for b = 1, 10 do local m = api.getMapping(1, b) local a = m and m.actionsetting if a and a.chatText == palabras then return end if not hueco and not (a and next(a)) then hueco = b end end if hueco then api.createOrUpdateText(1, hueco, palabras, true) api.saveData() -- el hueco es un UIWidget vacío; updateButton lo cambia por un botón de verdad (como al arrastrar un hechizo) local barra = ab.actionBars and ab.actionBars[1] local w = barra and barra.tabBar:getChildById('1.' .. hueco) if w and ab.updateButton then ab.updateButton(w) end end end local function onOpcode(protocol, opcode, buffer) local tipo, resto = buffer:sub(1, 1), buffer:sub(3) if tipo == 'D' then local ok, d = pcall(json.decode, resto) if not ok or type(d) ~= 'table' then return g_logger.error('[talentos] datos ilegibles: ' .. tostring(d)) end local antes = datos and datos.espec datos, plan = d, {} datos.rangos = datos.rangos or {} if d.espec and antes == nil and d.abrir then -- recién elegida local r = ramaActual() ponerEnBarra(r and r.firma.palabras) end if modules.game_libro and modules.game_libro.alCambiarRama then modules.game_libro.alCambiarRama() end if abrirAlLlegar or d.abrir or ventana:isVisible() then abrirAlLlegar = false pintar() ventana:show() ventana:raise() ventana:focus() end elseif tipo == 'M' then aviso(resto) end end function toggle() if not ventana then return end if ventana:isVisible() then return cerrar() end abrirAlLlegar = true enviar('P') end function cerrar() if ventana then ventana:hide() end end function aplicar() local p = {} for id, n in pairs(plan) do table.insert(p, id .. ':' .. n) end if #p > 0 then enviar('A|' .. table.concat(p, ',')) end end function deshacer() plan = {} pintarArbol() mostrarFicha(nil) end -- Para game_libro: raza del personaje («humano», «orco», «elfo», «enano»); la manda el servidor al entrar. function getRaza() return raza end -- Para game_libro: { [nombre] = true } de los hechizos needLearn que ya sabes. function getAprendidos() return aprendidos end -- Para game_libro (libro de hechizos): ramas de la clase y la elegida, sin abrir la ventana. function getRamas() return datos and datos.ramas, datos and datos.espec end -- Para game_hud (contador del botón de talentos): puntos sin gastar en la rama elegida; nil si aún no hay datos. function getLibres() local r = datos and ramaActual() return r and libres(r) or nil end function pedirDatos() enviar('P') end -- Para probar la ventana sin servidor: modules.game_talentos.probar('') function probar(texto) abrirAlLlegar = true onOpcode(nil, OPCODE, 'D|' .. texto) end function talentosController:onInit() ventana = g_ui.displayUI('talentos') OtmmoVentanas.recordar(ventana, 'talentos') -- posición entre sesiones (fase 4) ventana:hide() ProtocolGame.registerExtendedOpcode(OPCODE, onOpcode) -- el servidor pide poner una habilidad en la barra (server/scripts/monturas/monturas.lua: al comprar una montura) ProtocolGame.registerExtendedOpcode(115, function(protocol, opcode, buffer) local r = buffer:match('^Z|(%a+)$') -- raza del personaje (server/scripts/razas/razas.lua) if r then raza = r if modules.game_libro and modules.game_libro.alCambiarRama then modules.game_libro.alCambiarRama() end return end local palabras = buffer:match('^B|(.+)$') if palabras then return ponerEnBarra(palabras) end local lista = buffer:match('^L|(.*)$') if lista then aprendidos = {} for n in lista:gmatch('[^;]+') do aprendidos[n] = true end if modules.game_libro and modules.game_libro.alCambiarRama then modules.game_libro.alCambiarRama() end return end local viejas, nuevas = buffer:match('^R|([^|]+)|(.+)$') if viejas then reemplazarEnBarras(viejas, nuevas) end end) Keybind.new('Ventanas', 'Talentos', { [CHAT_MODE.ON] = '', [CHAT_MODE.OFF] = 'N' }, '') Keybind.bind('Ventanas', 'Talentos', { { type = KEY_DOWN, callback = function() toggle() end } }) end function talentosController:onTerminate() ProtocolGame.unregisterExtendedOpcode(OPCODE) ProtocolGame.unregisterExtendedOpcode(115) Keybind.delete('Ventanas', 'Talentos') if ventana then ventana:destroy() ventana = nil end end -- Cazadores ya creados: «Crear Flechas» llegó el 22-oct; se pone una vez por personaje en el primer hueco de la barra -- (los nuevos la traen en la casilla 5 del perfil Paladin de game_actionbar). function talentosController:onGameStart() scheduleEvent(function() local yo = g_game.getLocalPlayer() local voc = yo and yo:getVocation() if not (voc == 2 or voc == 12) then -- Paladin = Cazador (game_actionbar: perfilPorVocacion) return end local clave = 'flechasEnBarra_' .. yo:getName() if not g_settings.getBoolean(clave, false) then ponerEnBarra('crear flechas') g_settings.set(clave, true) end end, 2500) end function talentosController:onGameEnd() cerrar() datos, plan, aprendidos = nil, {}, {} end