-- OT-MMO: marcos de grupo estilo WoW. El servidor (server/scripts/grupo.lua) manda por el opcode 105 la -- lista completa del grupo con vida y maná, también de quienes no están a la vista: -- «G|id^nombre^vocacion^vida%^mana%^lider^cerca~...» («G|» = sin grupo) -- Clic izquierdo en un marco: elegir aliado (la Curación Directa de la barra irá a él). -- Clic derecho: curarlo ya. La cabecera se arrastra; el botón cambia entre columna, fila y cuadrícula. -- Posición y forma se guardan en los ajustes. local OPCODE = 105 local ANCHO, ALTO, HUECO = 190, 44, 3 local ICONOS_CLASE = { [4] = 0, [3] = 9, [1] = 18, [2] = 27, [11] = 36, [12] = 45 } -- 45: laúd del Bardo (añadido a icons-vocations.png) local marcasMapa = {} local cicloMapa local plegado = false local oculto = false -- vocación del servidor -> clase, rol y símbolo de rol. La vida va en hp para todos (guía: el color marca el -- estado, no la clase; antes cada clase tenía su tono, varios marrones). local C = OtmmoTheme.color local VIDA, FONDO = C.hp .. 'B3', C.bg2 .. 'F2' -- relleno al 70 %: el marco entero en rojo pleno se leía como alarma local CLASES = { [4] = { nombre = 'Guardián', rol = 'Tanque', color = VIDA, fondo = FONDO, simbolo = 'T' }, [2] = { nombre = 'Druida', rol = 'Sanador', color = VIDA, fondo = FONDO, simbolo = '+' }, [1] = { nombre = 'Hechicero', rol = 'Daño', color = VIDA, fondo = FONDO, simbolo = 'D' }, [3] = { nombre = 'Cazador', rol = 'Daño', color = VIDA, fondo = FONDO, simbolo = 'D' }, [11] = { nombre = 'Brujo', rol = 'Daño', color = VIDA, fondo = FONDO, simbolo = 'D' }, [12] = { nombre = 'Bardo', rol = 'Daño', color = VIDA, fondo = FONDO, simbolo = 'D' }, -- el de Himno cura: el rol lo fija la rama en el buscador } local FORMAS = { 'columna', 'fila', 'cuadricula' } local ETIQUETA = { columna = 'Columna', fila = 'Fila', cuadricula = 'Rejilla' } local SIN_CLASE = { nombre = '', rol = '', color = VIDA, fondo = FONDO, simbolo = '' } -- Hechizos que van al aliado elegido: los que el servidor lanza con el nombre como parámetro (y sin él, sobre uno -- mismo; Magia.aliadoDe en server/scripts/clases/magia.lua). local CON_ALIADO = { ['curar'] = true, ['gran curacion'] = true, ['floracion'] = true, ['corteza protectora'] = true, ['semilla de vida'] = true, ['purificar'] = true, ['brote'] = true, ['revivir'] = true, ['escudo de savia'] = true, ['renacer'] = true, ['sauce lloron'] = true, ['espinas'] = true, ['piel de roble'] = true, ['interponerse'] = true, } grupoController = Controller:new() grupoController:setUI('grupo', modules.game_interface.getRootPanel()) local miembros = {} local aliado = nil -- nombre del aliado elegido local forma = 'columna' local function guardar() local ui = grupoController.ui g_settings.setNode('game_grupo', { forma = forma, x = ui:getX(), y = ui:getY() }) g_settings.save() end local function columnas(n) if forma == 'fila' then return math.max(1, n) elseif forma == 'cuadricula' then return math.min(n, 3) end return 1 end -- Lo llama la barra de habilidades: «curar» pasa a «curar "Nombre» si hay aliado elegido. function conAliado(palabras) if aliado and CON_ALIADO[palabras] then return string.format('%s "%s', palabras, aliado) end return palabras end local dibujar local function elegir(nombre) aliado = aliado ~= nombre and nombre or nil dibujar() end local function curar(nombre) g_game.talk(string.format('curar "%s', nombre)) end dibujar = function() local ui = grupoController.ui if not ui then return end local lista = ui.lista lista:destroyChildren() ui:setVisible(#miembros > 0 and not oculto) if #miembros == 0 then aliado = nil return end local cols = columnas(#miembros) local filas = math.ceil(#miembros / cols) local sigue = false for i, m in ipairs(miembros) do local clase = CLASES[m.voc] or SIN_CLASE local marco = g_ui.createWidget('MarcoMiembro', lista) local col, fila = (i - 1) % cols, math.floor((i - 1) / cols) marco:addAnchor(AnchorTop, 'parent', AnchorTop) marco:addAnchor(AnchorLeft, 'parent', AnchorLeft) marco:setMarginTop(fila * (ALTO + HUECO)) marco:setMarginLeft(col * (ANCHO + HUECO)) marco.vida:setBackgroundColor(m.vida > 0 and clase.color or C.bg) marco.vida:setWidth(math.max(1, math.floor((ANCHO - 2) * m.vida / 100))) marco.mana:setWidth(math.max(1, math.floor((ANCHO - 2) * m.mana / 100))) marco:setBackgroundColor(clase.fondo) marco.rol:setImageSource('/images/icons/icons-vocations') marco.rol:setImageClip({ x = ICONOS_CLASE[m.voc] or 0, y = 0, width = 9, height = 9 }) marco.nivel:setText('Nv. ' .. (m.nivel or '?')) marco.nombre:setText((m.lider and '* ' or '') .. m.nombre) marco.pct:setText(m.vida > 0 and (m.hp and (m.hp .. ' / ' .. m.maxHp) or '--') or 'Muerto') marco.pct:setColor(m.vida > 0 and C.text or C.errorText) marco:setOpacity(m.cerca and 1 or 0.45) -- lejos: no se le puede curar marco:setTooltip(string.format('%s · %s (%s)\nClic: elegir · Clic derecho: curar', m.nombre, clase.nombre, clase.rol)) if m.vida > 0 and m.vida < 35 then marco:setBorderColor(C.hp) end if m.nombre == aliado then sigue = true marco:setBorderWidth(2) marco:setBorderColor(C.accent) end local nombre = m.nombre marco.onMouseRelease = function(w, pos, boton) if not w:containsPoint(pos) then return false end if boton == MouseRightButton then curar(nombre) else elegir(nombre) end return true end end if not sigue then aliado = nil end local w = cols * ANCHO + (cols - 1) * HUECO local h = filas * ALTO + (filas - 1) * HUECO lista:setSize({ width = w, height = h }) ui:setSize({ width = math.max(w, ANCHO), height = h + 20 }) -- cabecera 18 + 2 de aire ui.lista:setVisible(not plegado) if plegado then ui:setHeight(20) end ui.cabecera.forma:setText(ETIQUETA[forma]) end local function onOpcode(protocol, opcode, buffer) miembros = {} local vistos = {} for _, fila in ipairs((buffer:sub(3)):split('~')) do local f = fila:split('^') if #f >= 7 and not vistos[f[2]] then vistos[f[2]] = true miembros[#miembros + 1] = { id = tonumber(f[1]), nombre = f[2], voc = tonumber(f[3]), vida = tonumber(f[4]) or 0, mana = tonumber(f[5]) or 0, lider = f[6] == '1', cerca = f[7] == '1', nivel = tonumber(f[8]), hp = tonumber(f[9]), maxHp = tonumber(f[10]), x = tonumber(f[11]), y = tonumber(f[12]), z = tonumber(f[13]) } end end dibujar() end function cambiarForma() for i, f in ipairs(FORMAS) do if f == forma then forma = FORMAS[i % #FORMAS + 1] break end end dibujar() guardar() end local invitaciones = {} local function cerrarInvitacion(id) local box = invitaciones[id] if box and not box:isDestroyed() then box:destroy() end invitaciones[id] = nil end function invitacionGrupo(creature, shield) local localPlayer = g_game.getLocalPlayer() if not localPlayer or creature == localPlayer then return end local id = creature:getId() if shield ~= ShieldWhiteYellow then cerrarInvitacion(id) return end local ownShield = localPlayer:getShield() if ownShield ~= ShieldNone and ownShield ~= ShieldWhiteBlue then return end if invitaciones[id] then return end local function aceptar() local leader = g_map.getCreatureById(id) if leader and leader:getShield() == ShieldWhiteYellow then g_game.partyJoin(id) end cerrarInvitacion(id) end local function rechazar() cerrarInvitacion(id) end invitaciones[id] = displayGeneralBox('Invitacion de grupo', creature:getName() .. ' te invita a su party.', {{text='Aceptar',callback=aceptar},{text='Rechazar',callback=rechazar}},aceptar,rechazar) end local function limpiarInvitaciones() for id in pairs(invitaciones) do cerrarInvitacion(id) end end function grupoController:onInit() connect(Creature, { onShieldChange = invitacionGrupo }) ProtocolGame.registerExtendedOpcode(OPCODE, onOpcode) local ui = self.ui local ajustes = g_settings.getNode('game_grupo') or {} forma = ajustes.forma or 'columna' if ajustes.x and ajustes.y then ui:breakAnchors() ui:setPosition({ x = ajustes.x, y = ajustes.y }) end OtmmoVentanas.moverCon(ui, ui.cabecera, 'grupo') ui.cabecera.plegar.onClick = function() plegado = not plegado dibujar() end end -- Al entrar se pide la lista: lo que el servidor manda en el primer instante del login puede perderse. function grupoController:onGameStart() detenerMarcas() cicloMapa = cycleEvent(pintarAliados, 150) scheduleEvent(function() local protocol = g_game.getProtocolGame() if protocol then protocol:sendExtendedOpcode(OPCODE, 'P') end end, 1000) end function grupoController:onTerminate() disconnect(Creature, { onShieldChange = invitacionGrupo }) limpiarInvitaciones() detenerMarcas() ProtocolGame.unregisterExtendedOpcode(OPCODE) end function grupoController:onGameEnd() limpiarInvitaciones() detenerMarcas() miembros = {} aliado = nil dibujar() end local function limpiarMarcas() for _, w in pairs(marcasMapa) do if not w:isDestroyed() then w:destroy() end end marcasMapa = {} end function pintarAliados() local maps = {} local mini = modules.game_minimap and modules.game_minimap.getMiniMapUi() local world = modules.game_mapa and modules.game_mapa.getMapa() if mini then maps[#maps + 1] = mini end if world then maps[#maps + 1] = world end local keep = {} for index, map in ipairs(maps) do local cam = map:getCameraPosition() for _, member in ipairs(miembros) do if member.x and member.y and member.z then local key = index .. ':' .. member.id keep[key] = true local w = marcasMapa[key] if not w or w:isDestroyed() then w = g_ui.createWidget('UIWidget', map) w:setSize({ width = 9, height = 9 }) w:setBorderWidth(1) w:setBorderColor(C.bg) marcasMapa[key] = w end w:setBackgroundColor(member.lider and C.accent or C.ok) w:setTooltip(member.nombre .. ' - Nv. ' .. (member.nivel or '?')) map:centerInPosition(w, { x = member.x, y = member.y, z = member.z }) local visible = cam and cam.z == member.z if visible then local r, p = map:getPaddingRect(), w:getRect() visible = p.x >= r.x and p.y >= r.y and p.x + p.width <= r.x + r.width and p.y + p.height <= r.y + r.height if visible and map.isCircular and map:isCircular() then local dx, dy = p.x + 4 - r.x - r.width / 2, p.y + 4 - r.y - r.height / 2 visible = dx * dx + dy * dy < (math.min(r.width, r.height) / 2 - 5) ^ 2 end end w:setVisible(visible == true) w:raise() end end end for key, w in pairs(marcasMapa) do if not keep[key] then if not w:isDestroyed() then w:destroy() end marcasMapa[key] = nil end end end function detenerMarcas() if cicloMapa then removeEvent(cicloMapa) cicloMapa = nil end limpiarMarcas() end function toggle() oculto = not oculto dibujar() end