-- OT-MMO: ventana de entrada a un dungeon y cuenta atrás. Solo dibuja y pide: el servidor -- (server/scripts/sala_dungeon.lua) decide quién entra, cuándo y con qué grupo. Opcode extendido 108: -- «A|ficha|miembros|plazo» abre o actualiza · «C|segundos|nombre» cuenta atrás · «X» cierra todo -- El cliente envía «L» (listo), «N» (no / salir) o «H» (el líder cambia entre normal y HEROICA; el servidor responde con -- otra «A» cuya dificultad es «Heroica» o «Normal»). local OPCODE = 108 local CLASES = { [4] = 'Guardián', [2] = 'Druida', [1] = 'Hechicero', [3] = 'Cazador', [11] = 'Brujo', [12] = 'Bardo' } local ORDEN = { 4, 2, 1, 3 } -- Rareza: común en borde neutro; poco común ok, rara mana, épica púrpura (el color de rareza es de los pocos fuertes que la guía permite). local C = OtmmoTheme.color local RAREZA = { [0] = C.border, C.ok, C.mana, '#B56AE0' } -- común, poco común, rara, épica local ESTADOS = { -- texto, color del texto, fondo de la franja, borde de la tarjeta (tokens: listo = acento, problema = error) L = { 'Listo', C.accent, C.surface, C.accent }, E = { 'Esperando', C.text2, C.bg2, C.border }, F = { 'Lejos del portal', C.errorText, C.bg2, C.hp }, R = { 'Listo, ven al portal', C.errorText, C.bg2, C.hp }, } dungeonController = Controller:new() local ventana, conteo = nil, nil local fichaTexto, botin, pestana = nil, {}, nil local plazoFin, plazoTotal = 0, 30000 local eventoPlazo, eventoConteo = nil, nil local function outfit(look) local t = look:split('.') return { type = tonumber(t[1]) or 0, head = tonumber(t[2]) or 0, body = tonumber(t[3]) or 0, legs = tonumber(t[4]) or 0, feet = tonumber(t[5]) or 0, addons = tonumber(t[6]) or 0 } end local function enviar(texto) local protocol = g_game.getProtocolGame() if protocol then protocol:sendExtendedOpcode(OPCODE, texto) end end local function pararPlazo() if eventoPlazo then removeEvent(eventoPlazo) eventoPlazo = nil end end local function pintarPlazo() if not ventana or not ventana:isVisible() then return pararPlazo() end local resta = math.max(0, plazoFin - g_clock.millis()) local barra = ventana.plazo barra.fill:setWidth(math.max(1, math.floor(barra:getWidth() * resta / plazoTotal))) ventana.listos:setText(string.format('%s 0:%02d', ventana.listos.cuenta or '', math.ceil(resta / 1000))) end local function mostrarBotin() ventana.rejilla:destroyChildren() for _, it in ipairs(botin[pestana] or {}) do local c = g_ui.createWidget('DgBotin', ventana.rejilla) c.item:setItemId(it.id) c:setBorderColor(RAREZA[it.rareza] or RAREZA[0]) end for _, tab in ipairs(ventana.pestanas:getChildren()) do local activa = tab.voc == pestana tab:setChecked(activa) -- GameTab (data/styles/20-tabbars.otui): subrayado de acento end end local function pintarFicha(texto, miVoc) local f = texto:split('^') local nombre, dificultad, nmin, nmax, jmin, jmax, lema = f[1], f[2], tonumber(f[3]), tonumber(f[4]), f[5], f[6], f[7] local encuentros = (f[11] or ''):split(';') ventana:setText(nombre) if modules.game_viaje and modules.game_viaje.ponerArte then modules.game_viaje.ponerArte(ventana.arte, nombre, 66, 58) end local nivel = nmax >= 99 and (nmin .. '+') or (nmin .. '-' .. nmax) ventana.meta:setText(string.format('%s · Nivel %s · %s a %s jugadores · %d encuentros', dificultad, nivel, jmin, jmax, #encuentros)) ventana.lema:setText(lema) local heroica = dificultad == 'Heroica' ventana.heroica:setChecked(heroica) ventana.heroica:setText(heroica and 'HEROICA ACTIVA' or 'HEROICA') ventana.heroica:setTooltip(heroica and 'Heroica elegida: nivel 100, una vez cada 24 h. Pulsa para volver a la normal.' or 'Versión heroica: nivel 100, una vez cada 24 h, enemigos reforzados, mecánicas nuevas y equipo de índice 110. Solo el líder.') ventana.heroicaBrillo:setBackgroundColor(heroica and '#FF3B3B70' or '#B3141C38') ventana.enemigos:destroyChildren() for _, e in ipairs((f[8] or ''):split(',')) do local n, look = e:match('^(.-):(.+)$') if n then local w = g_ui.createWidget('DgEnemigo', ventana.enemigos) w.cara:setOutfit(outfit(look)) w.nombre:setText(n) end end local jn, jlook, jmec = (f[9] or ''):match('^(.-):(.-):(.*)$') ventana.jefe.cara:setOutfit(outfit(jlook or '0')) ventana.jefe.nombre:setText(jn or '') ventana.jefe.mecanicas:setText(jmec or '') botin = {} for _, grupo in ipairs((f[10] or ''):split(';')) do local voc, lista = grupo:match('^(%d+)=(.*)$') if voc then local items = {} for _, par in ipairs(lista:split(',')) do local id, rareza = par:match('(%d+)%.(%d+)') if id then items[#items + 1] = { id = tonumber(id), rareza = tonumber(rareza) } end end botin[tonumber(voc)] = items end end ventana.pestanas:destroyChildren() -- La vocación viene en la fila del grupo (id del servidor); la del LocalPlayer es el id de cliente de Tibia. pestana = botin[miVoc] and miVoc or nil for _, voc in ipairs(ORDEN) do if botin[voc] then pestana = pestana or voc local tab = g_ui.createWidget('GameTab', ventana.pestanas) tab.voc = voc tab:setText(CLASES[voc]) tab.onClick = function() pestana = voc mostrarBotin() end end end mostrarBotin() local lineas = {} for i, e in ipairs(encuentros) do lineas[#lineas + 1] = string.format('%d %s', i, e) end ventana.encuentros:setText(table.concat(lineas, '\n')) ventana.jmax = tonumber(jmax) or 4 end local function pintarMiembros(texto) local yo = g_game.getCharacterName() local filas = texto:split('~') ventana.miembros:destroyChildren() local listos, miEstado = 0, nil for i = 1, ventana.jmax do local w = g_ui.createWidget('DgMiembro', ventana.miembros) local m = filas[i] and filas[i]:split('^') if m and #m >= 6 then local est = ESTADOS[m[5]] or ESTADOS.E w.cara:setOutfit(outfit(m[4])) w.nombre:setText(m[1]) w.clase:setText(string.format('%s · %s%s', CLASES[tonumber(m[2])] or 'Sin clase', m[3], m[6] == '1' and ' · líder' or '')) w.estado:setText(est[1]) w.estado:setColor(est[2]) w.estado:setBackgroundColor(est[3]) w:setBorderColor(est[4]) if m[5] == 'L' or m[5] == 'R' then listos = listos + 1 end if m[1] == yo then miEstado = m[5] end else w.cara:hide() w.nombre:setText('Libre') w.nombre:setColor(C.textDisabled) w:setBackgroundColor(C.bg) w:setBorderColor(C.border) w:setOpacity(0.6) end end ventana.tGrupo:setText(string.format('Grupo %d / %d', #filas, ventana.jmax)) ventana.listos.cuenta = string.format('%d de %d listos', listos, #filas) -- Ya listo (aunque esté lejos): el botón espera a los demás; la cuenta atrás empieza al estar todos junto al portal. local yaListo = miEstado == 'L' or miEstado == 'R' ventana.listo:setEnabled(not yaListo) ventana.listo:setText(yaListo and 'Esperando...' or 'Listo') ventana.aviso:setColor((miEstado == 'F' or miEstado == 'R') and C.errorText or C.text2) end local function ocultarConteo() if eventoConteo then removeEvent(eventoConteo) eventoConteo = nil end if conteo then conteo:hide() end end local function cerrarVentana() pararPlazo() if ventana then ventana:hide() end end local function abrir(partes) if not ventana then g_ui.importStyle('/game_viaje/valdren') -- marco y estilos VALDREN (modules/game_viaje/valdren.otui) ventana = g_ui.displayUI('dungeon') OtmmoVentanas.recordar(ventana, 'dungeon') -- posición entre sesiones (fase 4) end if partes[2] ~= fichaTexto then fichaTexto = partes[2] local miVoc = nil for _, fila in ipairs((partes[3] or ''):split('~')) do local m = fila:split('^') if m[1] == g_game.getCharacterName() then miVoc = tonumber(m[2]) end end pintarFicha(partes[2], miVoc) end pintarMiembros(partes[3] or '') local segundos = tonumber(partes[4]) or 30 if not ventana:isVisible() or not eventoPlazo then plazoTotal = 30000 plazoFin = g_clock.millis() + segundos * 1000 end ocultarConteo() ventana:show() ventana:raise() pintarPlazo() if not eventoPlazo then eventoPlazo = cycleEvent(pintarPlazo, 250) end end local function empezarConteo(segundos, nombre) cerrarVentana() if not conteo then conteo = g_ui.displayUI('conteo') end conteo.nombre:setText(nombre or '') conteo:show() local n = segundos local function paso() if n <= 0 then return ocultarConteo() end conteo.numero:setText(tostring(n)) n = n - 1 eventoConteo = scheduleEvent(paso, 1000) end paso() end local function onOpcode(protocol, opcode, buffer) local partes = buffer:split('|') if partes[1] == 'A' then abrir(partes) elseif partes[1] == 'C' then empezarConteo(tonumber(partes[2]) or 10, partes[3]) elseif partes[1] == 'X' then cerrarVentana() ocultarConteo() end end function listo() enviar('L') end function heroica() enviar('H') end -- Para probar sin servidor: modules.game_dungeon.probar('A|ficha|miembros|30') function probar(buffer) onOpcode(nil, OPCODE, buffer) end function salir() enviar('N') cerrarVentana() end function dungeonController:onInit() ProtocolGame.registerExtendedOpcode(OPCODE, onOpcode) end function dungeonController:onTerminate() ProtocolGame.unregisterExtendedOpcode(OPCODE) cerrarVentana() ocultarConteo() if ventana then ventana:destroy() end if conteo then conteo:destroy() end ventana, conteo, fichaTexto = nil, nil, nil end function dungeonController:onGameEnd() cerrarVentana() ocultarConteo() end