-- OT-MMO: botín de jefe a dados, como en WoW. El servidor (server/scripts/botin_dados.lua) es la autoridad: elige las -- piezas, decide quién puede pedir Necesidad, tira los dados y entrega. Aquí solo se pinta y se pide. -- Opcode 122 (servidor -> cliente): -- T|id|item|calidad|nombre|tipo|nivel|necesidad|motivo|ms|tooltip nueva pieza (tooltip: último campo, puede llevar «|») -- V|id|eleccion tu elección quedó registrada (N, C, P) -- E|id|respondieron|total cuántos ya eligieron -- R|id|ganador|nombre^eleccion^tirada~...|texto resultado; texto = línea para el chat -- Cliente -> servidor: N|id, C|id o P|id. -- Las tarjetas salen EN MEDIO, en el tercio inferior del mapa, justo encima del estado (game_vitals) y de la barra de -- acción. La primera queda abajo y las nuevas se apilan hacia ARRIBA (las que ya estaban no se mueven bajo el ratón). -- La pila no sube de MAX_ALTO del mapa (el personaje, su barra de lanzamiento y el centro del combate quedan libres): -- si no caben, se abre otra columna a la derecha y el conjunto se centra. Se recoloca al cambiar el tamaño del mapa. local OPCODE = 122 local SEP = 8 -- px entre tarjetas y sobre el estado (escala de 4 de la guía) local MAX_ALTO = 0.56 -- tope de la pila: 56 % del alto del mapa desde arriba (justo bajo los pies del personaje) local CIERRE = 8000 -- ms que se ve el resultado antes de cerrarse sola local AVISO = 5000 -- ms: en los últimos 5 s la barra pasa a advertencia local ICONOS = '/game_botin/iconos/' -- Tokens de la guía (data/styles/0-vars.otui): Lua no lee $var local C = OtmmoTheme.color local TEXTO, TEXTO2, ACENTO, AVISOC, ACENTO_BARRA = C.text, C.text2, C.accent, C.warn, C.accent local DESACTIVADO = C.textDisabled local NOMBRES = { N = 'Necesidad', C = 'Codicia', P = 'Pasar' } botinController = Controller:new() local pila = nil local tiradas = {} -- [id] = { w, fin, total, eleccion, resuelta } local orden = {} -- tarjetas por orden de llegada (la primera abajo) local reloj = nil -- Coloca las tarjetas: columnas que crecen hacia arriba desde encima del estado, centradas en el mapa. local function colocar() if not pila or pila:isDestroyed() then return end local vivas = {} for _, w in ipairs(orden) do if not w:isDestroyed() then vivas[#vivas + 1] = w end end orden = vivas if #orden == 0 then return end local mapa = pila:getRect() local suelo = mapa.y + mapa.height - SEP local raiz = modules.game_interface.getRootPanel() local estado = raiz and raiz:recursiveGetChildById('vitals') local barra = raiz and raiz:recursiveGetChildById('gameBottomActionPanel') if estado and estado:isVisible() then suelo = math.min(suelo, estado:getY() - SEP) elseif barra and barra:isVisible() then suelo = math.min(suelo, barra:getY() - SEP) end local techo = mapa.y + math.floor(mapa.height * MAX_ALTO) local alto = math.max(0, suelo - techo) local ancho = orden[1]:getWidth() -- reparto en columnas: cada una se llena de abajo arriba hasta el techo local columnas, col, usado = {}, {}, 0 for _, w in ipairs(orden) do local h = w:getHeight() if #col > 0 and usado + SEP + h > alto then columnas[#columnas + 1] = col col, usado = {}, 0 end usado = usado + (#col > 0 and SEP or 0) + h col[#col + 1] = w end columnas[#columnas + 1] = col local caben = math.max(1, math.floor((mapa.width - 2 * SEP + SEP) / (ancho + SEP))) local n = math.min(#columnas, caben) local x0 = mapa.x + math.floor((mapa.width - (n * ancho + (n - 1) * SEP)) / 2) for i, c in ipairs(columnas) do local y = suelo for _, w in ipairs(c) do if i <= n then y = y - w:getHeight() w:setPosition({ x = x0 + (i - 1) * (ancho + SEP), y = y }) y = y - SEP w:setVisible(true) else w:setVisible(false) -- no caben: esperan a que se cierre otra end end end end local function enviar(texto) local protocol = g_game.getProtocolGame() if protocol then protocol:sendExtendedOpcode(OPCODE, texto) end end -- Alto de la tarjeta: una fila de 32 (objeto, nombre, botones), las filas del resultado si las hay y la barra de 3. local function ajustarAlto(t) local w = t.w local filas = w.filas:isVisible() and (8 + w.filas:getChildCount() * 18) or 0 local barra = w.barra:isVisible() and (8 + 3) or 0 w:setHeight(8 + 32 + filas + barra + 8) colocar() end local function activarBoton(b, si) b:setEnabled(si) b.icono:setImageColor(si and TEXTO or DESACTIVADO) end local function cerrar(id) local t = tiradas[id] tiradas[id] = nil if t and t.w and not t.w:isDestroyed() then local w = t.w g_effects.fadeOut(w, 150) scheduleEvent(function() if not w:isDestroyed() then w:destroy() end colocar() end, 160) end end local function mostrarEstado(t, texto) local w = t.w w.botones:hide() w.motivo:hide() w.estado:setText(texto) w.estado:show() ajustarAlto(t) end local function textoEstado(t) local s = 'Elegiste ' .. NOMBRES[t.eleccion] if t.respondieron and t.total then s = s .. string.format(' · %d de %d eligieron', t.respondieron, t.total) end return s end local function elegir(id, r) local t = tiradas[id] if not t or t.eleccion or t.resuelta then return end for _, b in ipairs({ t.w.botones.necesidad, t.w.botones.codicia, t.w.botones.pasar }) do activarBoton(b, false) end t.pedida = r enviar(r .. '|' .. id) end -- Tiempo restante y barra. Lo llama el reloj cada 100 ms. local function avanzar() local ahora = g_clock.millis() for _, t in pairs(tiradas) do if not t.resuelta then local resta = math.max(0, t.fin - ahora) local barra = t.w.barra barra.fill:setWidth(math.max(0, math.floor(barra:getWidth() * resta / t.plazo))) barra.fill:setBackgroundColor(resta <= AVISO and AVISOC or ACENTO_BARRA) t.w.cuenta:setText(math.ceil(resta / 1000) .. ' s') if resta <= 0 and not t.eleccion and not t.vencida then t.vencida = true mostrarEstado(t, 'Sin respuesta: pasas. Esperando el reparto...') end end end end local function nuevaTirada(buffer) -- 10 campos fijos y el tooltip entero (puede llevar «|») local f, resto = {}, buffer for i = 1, 10 do local a = resto:find('|', 1, true) if not a then return end f[i] = resto:sub(1, a - 1) resto = resto:sub(a + 1) end local id, itemId, calidad, nivel = tonumber(f[2]), tonumber(f[3]), tonumber(f[4]) or 0, tonumber(f[7]) or 0 local necesidad, ms = f[8] == '1', tonumber(f[10]) or 30000 if not id or not itemId or not pila then return end if tiradas[id] then -- reenviada al reconectar: se rehace tiradas[id].w:destroy() tiradas[id] = nil end local w = g_ui.createWidget('BotinTirada', pila) orden[#orden + 1] = w local t = { w = w, fin = g_clock.millis() + ms, plazo = ms } tiradas[id] = t w.item:setItemId(itemId) w.item:setImageClip({ x = ItemsDatabase.calidadClip[calidad] or 0, y = 0, width = 32, height = 32 }) -- «{q2|nombre}» del servidor -> etiquetas que entiende el tooltip coloreado (UIWidget:parseColoredText) local tooltip = resto:gsub('{q(%d)|(.-)}', function(n, texto) return '[color=' .. (ItemsDatabase.calidadColor[n] or TEXTO) .. ']' .. texto .. '[/color]' end) w.item.parseColoreDisplay = tooltip w.nombre.parseColoreDisplay = tooltip w.nombre:setText(f[5]) w.nombre:setColor(ItemsDatabase.calidadColor[tostring(calidad)] or TEXTO) local detalle = f[6] if nivel > 0 then detalle = detalle .. ' · nivel ' .. nivel end w.detalle:setText(detalle) local b = w.botones b.necesidad.icono:setImageSource(ICONOS .. 'dado') b.codicia.icono:setImageSource(ICONOS .. 'moneda') b.pasar.icono:setImageSource(ICONOS .. 'pasar') b.necesidad.onClick = function() elegir(id, 'N') end b.codicia.onClick = function() elegir(id, 'C') end b.pasar.onClick = function() elegir(id, 'P') end activarBoton(b.necesidad, necesidad) activarBoton(b.codicia, true) activarBoton(b.pasar, true) if not necesidad then local motivo = f[9] ~= '' and f[9] or 'No es para tu clase' local frase = 'Necesidad: ' .. motivo:gsub('^%u', string.lower) .. '.' b.necesidad:setTooltip(frase) w.motivo:setText(motivo) -- la frase entera queda en el tooltip del botón w.motivo:show() end ajustarAlto(t) g_effects.fadeIn(w, 150) avanzar() end local function confirmada(id, r) local t = tiradas[id] if not t or t.resuelta then return end t.eleccion = r mostrarEstado(t, textoEstado(t)) end local function progreso(id, n, total) local t = tiradas[id] if not t or t.resuelta then return end t.respondieron, t.total = n, total if t.eleccion then t.w.estado:setText(textoEstado(t)) end end -- Resultado en el chat: la pestaña del grupo si está abierta; si no, el chat local. local function alChat(texto) local consola = modules.game_console if not consola or not consola.addText or texto == '' then return end local pestana = nil for _, nombre in ipairs({ 'Party', tr('Party'), 'Grupo' }) do if consola.getTab and consola.getTab(nombre) then pestana = nombre break end end local tipo = consola.SpeakTypesSettings and consola.SpeakTypesSettings.channelOrange pcall(consola.addText, 'Botín: ' .. texto, tipo, pestana or tr('Local Chat')) end local function resultado(id, ganador, lista, texto) alChat(texto) local t = tiradas[id] if not t then return end t.resuelta = true local w = t.w w.botones:hide() w.motivo:hide() w.estado:hide() w.barra:hide() w.cuenta:setText('') if ganador ~= '' then w.detalle:setText('Gana ' .. ganador) w.detalle:setColor(ACENTO) else w.detalle:setText('Todos pasaron: nadie se la lleva') w.detalle:setColor(TEXTO2) end w.filas:destroyChildren() for _, fila in ipairs(lista:split('~')) do local c = fila:split('^') if c[1] and c[1] ~= '' then local r = g_ui.createWidget('BotinFila', w.filas) r.nombre:setText(c[1]) r.eleccion:setText(c[2] == 'P' and 'Pasa' or (NOMBRES[c[2]] or '')) r.tirada:setText(c[3] or '') if c[1] == ganador then r.nombre:setColor(TEXTO) r.eleccion:setColor(TEXTO) r.tirada:setColor(ACENTO) end end end w.filas:show() ajustarAlto(t) scheduleEvent(function() cerrar(id) end, CIERRE) end local function onOpcode(protocol, opcode, buffer) local tipo = buffer:sub(1, 1) if tipo == 'T' then nuevaTirada(buffer) elseif tipo == 'V' then local id, r = buffer:match('^V|(%d+)|([NCP])$') if id then confirmada(tonumber(id), r) end elseif tipo == 'E' then local id, n, total = buffer:match('^E|(%d+)|(%d+)|(%d+)$') if id then progreso(tonumber(id), tonumber(n), tonumber(total)) end elseif tipo == 'R' then local id, ganador, lista, texto = buffer:match('^R|(%d+)|([^|]*)|([^|]*)|(.*)$') if id then resultado(tonumber(id), ganador, lista, texto) end elseif tipo == 'X' then for id in pairs(tiradas) do cerrar(id) end end end local function limpiar() for _, t in pairs(tiradas) do if t.w and not t.w:isDestroyed() then t.w:destroy() end end tiradas = {} orden = {} end function botinController:onInit() g_ui.importStyle('/game_botin/botin.otui') ProtocolGame.registerExtendedOpcode(OPCODE, onOpcode) if g_game.isOnline() then -- recargado a mitad de partida self:onGameStart() end end function botinController:onGameStart() if not pila then -- capa sin layout que cubre el mapa (fantasma: los clics pasan salvo sobre las tarjetas) pila = g_ui.createWidget('BotinPila', modules.game_interface.getRootPanel()) pila:fill('gameMapPanel') pila.onGeometryChange = function() colocar() end end if not reloj then reloj = cycleEvent(avanzar, 100) end limpiar() end function botinController:onGameEnd() limpiar() if reloj then removeEvent(reloj) reloj = nil end end function botinController:onTerminate() ProtocolGame.unregisterExtendedOpcode(OPCODE) if reloj then removeEvent(reloj) reloj = nil end limpiar() if pila then pila:destroy() pila = nil end end