您需要先安装一个扩展,例如 篡改猴、Greasemonkey 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 Userscripts ,之后才能安装此脚本。
您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。
您需要先安装用户脚本管理器扩展后才能安装此脚本。
Affiche les avatars améliorés des PJs avec couleurs de faction, bordures stylisées, infos profil (fonction + domiciliation) en ligne. Version stable visuellement harmonieuse (v4.4.0). 🦝
// ==UserScript== // @name Kraland - Avatars personnalisés // @namespace http://tampermonkey.net/ // @version 4.4.0 // @description Affiche les avatars améliorés des PJs avec couleurs de faction, bordures stylisées, infos profil (fonction + domiciliation) en ligne. Version stable visuellement harmonieuse (v4.4.0). 🦝 // @author Racket Raccoon / Th3rd // @match http://www.kraland.org/* // @match https://www.kraland.org/* // @icon data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHdpZHRoPSIyNCIgaGVpZ2h0PSIyNCIgdmlld0JveD0iMCAwIDI0IDI0IiBmaWxsPSJub25lIiBzdHJva2U9IiNlNjBmMGYiIHN0cm9rZS13aWR0aD0iMiIgc3Ryb2tlLWxpbmVjYXA9InJvdW5kIiBzdHJva2UtbGluZWpvaW49InJvdW5kIiBjbGFzcz0ibHVjaWRlIGx1Y2lkZS1wYXctcHJpbnQtaWNvbiBsdWNpZGUtcGF3LXByaW50Ij48Y2lyY2xlIGN4PSIxMSIgY3k9IjQiIHI9IjIiLz48Y2lyY2xlIGN4PSIxOCIgY3k9IjgiIHI9IjIiLz48Y2lyY2xlIGN4PSIyMCIgY3k9IjE2IiByPSIyIi8+PHBhdGggZD0iTTkgMTBhNSA1IDAgMCAxIDUgNXYzLjVhMy41IDMuNSAwIDAgMS02Ljg0IDEuMDQ1UTYuNTIgMTcuNDggNC40NiAxNi44NEEzLjUgMy41IDAgMCAxIDUuNSAxMFoiLz48L3N2Zz4= // @license MIT // @run-at document-start // ==/UserScript== (function () { 'use strict'; // Définition des constantes visuelles de base (taille des avatars, opacité, tailles de police) const AVATAR_SIZE = '75px'; const PNJ_SIZE = '50px'; const FONT_SMALL = '0.85em'; const OPACITY_LIGHT = '0.9'; // Couleurs associées à chaque drapeau de nation const flagColors = { 'f1.png': '#B22222', 'f2.png': '#8B5A2B', 'f3.png': '#DAA520', 'f4.png': '#055cb5', 'f5.png': '#025e19', 'f6.png': '#7A1C81', 'f7.png': '#262424', 'f8.png': '#008080', 'f9.png': '#556B2F' }; // Cache pour éviter de refaire des requêtes inutiles aux profils const functionCache = {}; // Icônes Lucide const lucideIcons = { briefcase: '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" style="width:14px;height:14px;margin-right:4px;vertical-align:middle;opacity:0.7"><rect x="2" y="7" width="20" height="14" rx="2" ry="2"/><path d="M16 3h-8v4h8V3z"/></svg>', mapPin: '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" style="width:14px;height:14px;margin-right:4px;vertical-align:middle;opacity:0.7"><path d="M12 21s6-5.686 6-10a6 6 0 1 0 -12 0c0 4.314 6 10 6 10z"/><circle cx="12" cy="11" r="2"/></svg>', user: '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" style="width:14px;height:14px;margin-right:4px;vertical-align:middle;opacity:0.7"><path d="M20 21v-2a4 4 0 0 0 -4-4H8a4 4 0 0 0 -4 4v2"/><circle cx="12" cy="7" r="4"/></svg>', link: '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" style="width:14px;height:14px;margin-right:4px;vertical-align:middle;opacity:0.7"><path d="M10 13a5 5 0 0 1 7 0l1.5 1.5a5 5 0 0 1 0 7 5 5 0 0 1 -7 0L10 20a5 5 0 0 1 0-7m-6-6a5 5 0 0 1 7 0l1.5 1.5a5 5 0 0 1 0 7 5 5 0 0 1 -7 0L4 10a5 5 0 0 1 0-7"/></svg>' }; // Applique une typographie à la cellule de nom function styleNameCell(nameCell) { nameCell.style.fontFamily = "'Roboto', sans-serif"; nameCell.style.lineHeight = '1.6'; } // Crée une <div> contenant une info stylisée (fonction ou domiciliation) function createInfoDiv(content, options = {}) { const div = document.createElement('div'); div.innerHTML = content; div.style.fontSize = FONT_SMALL; div.style.opacity = OPACITY_LIGHT; if (options.color) div.style.color = options.color; if (options.italic) div.style.fontStyle = 'italic'; if (options.bold) div.style.fontWeight = 'bold'; if (options.marginTop) div.style.marginTop = options.marginTop; if (options.className) div.className = options.className; return div; } // Insère sous le nom du PJ sa fonction et éventuellement sa domiciliation function insertFunctionBelowName(row, functionText, domiciliationText = '', placeholder = false) { const nameCell = row.querySelectorAll('td')[1]; if (nameCell?.querySelector('.avatar-function')) return; styleNameCell(nameCell); const isLoading = placeholder && functionText === 'Chargement...'; const isEmpty = !functionText || functionText === '-' || (!placeholder && functionText === 'Chargement...'); const funcDiv = createInfoDiv( isLoading ? 'Chargement...' : (isEmpty ? lucideIcons.briefcase + 'Aucune fonction' : lucideIcons.briefcase + functionText), { color: isEmpty ? '#666' : (isLoading ? '#999' : ''), italic: isEmpty || isLoading, bold: !(isEmpty || isLoading), marginTop: '0.1em', className: 'avatar-function' } ); nameCell.appendChild(funcDiv); if (domiciliationText) { const domDiv = createInfoDiv(lucideIcons.mapPin + domiciliationText, { italic: true }); nameCell.appendChild(domDiv); } const nameLink = nameCell.querySelector('a'); if (nameLink) { nameLink.style.fontWeight = 'bold'; nameLink.style.fontSize = '1.1em'; } } // Applique une couleur de fond (semi-transparente) à toute la ligne du PJ selon sa nationnalité function colorRow(row, color) { row.querySelectorAll('td').forEach(cell => { cell.style.setProperty('background-color', `${color}4D`, 'important'); cell.style.setProperty('background-clip', 'padding-box', 'important'); }); // Cible aussi directement la cellule contenant l’avatar s’il y a un lien avec l’id const avatarCell = row.querySelector('td.tdbc'); if (avatarCell) { avatarCell.style.setProperty('background-color', `${color}4D`, 'important'); } } // Crée un conteneur autour de l'avatar avec style et bordure function createAvatarWrapper(img, color, linkHref) { const isPNJ = img.src.includes('/npc/'); const wrapper = document.createElement('a'); wrapper.href = linkHref || '#'; wrapper.style.display = 'inline-block'; wrapper.style.position = 'relative'; wrapper.style.width = isPNJ ? PNJ_SIZE : AVATAR_SIZE; wrapper.style.height = isPNJ ? PNJ_SIZE : AVATAR_SIZE; wrapper.style.backgroundColor = 'transparent'; wrapper.style.zIndex = '1'; if (isPNJ) { wrapper.style.border = `3px solid ${color}`; wrapper.style.borderRadius = '0'; } else { wrapper.style.borderRadius = '50%'; } img.style.width = isPNJ ? 'auto' : AVATAR_SIZE; img.style.height = isPNJ ? 'auto' : AVATAR_SIZE; img.style.maxHeight = isPNJ ? PNJ_SIZE : ''; img.style.maxWidth = isPNJ ? 'none' : ''; img.style.zIndex = '2'; let imgContainer; if (isPNJ) { wrapper.style.border = `3px solid ${color}`; wrapper.style.borderRadius = '0'; imgContainer = document.createElement('div'); imgContainer.style.width = PNJ_SIZE; imgContainer.style.height = PNJ_SIZE; imgContainer.style.overflow = 'hidden'; imgContainer.style.display = 'flex'; imgContainer.style.alignItems = 'center'; imgContainer.style.justifyContent = 'center'; imgContainer.style.position = 'relative'; img.style.position = 'relative'; img.style.width = '100%'; img.style.height = '100%'; img.style.objectFit = 'cover'; img.style.transform = 'none'; imgContainer.appendChild(img); } else { img.style.borderRadius = '50%'; img.style.position = 'relative'; imgContainer = img; const overlay = document.createElement('div'); overlay.style.position = 'absolute'; overlay.style.inset = '0'; overlay.style.borderRadius = '50%'; overlay.style.boxShadow = `inset 0 0 0 4px ${color}`; overlay.style.pointerEvents = 'none'; overlay.style.zIndex = '3'; wrapper.appendChild(overlay); } wrapper.appendChild(imgContainer); return wrapper; } // Traitement des avatars pour les personnages joueurs (PJs) function handlePJAvatar(img, row, td) { const flagImg = Array.from(row.querySelectorAll('img')).find(img => { const file = img.src.split('/').pop(); return flagColors.hasOwnProperty(file); }); const color = flagImg ? flagColors[flagImg.src.split('/').pop()] : '#888'; const link = td.querySelector('a[href*="main.php?p=6_1_0_1"]'); const match = link?.href.match(/p1=(\d+)/); const wrapper = createAvatarWrapper(img, color, link?.href); td.replaceChildren(wrapper); // Ajout fond sur la cellule AVATAR td.style.setProperty('background-color', `${color}4D`, 'important'); colorRow(row, color); // garde la coloration des autres cellules // Vérifie si le personnage est recherché (n'importe où dans la cellule de nom) const nameCell = row.querySelectorAll('td')[1]; if (nameCell) { const originalHTML = nameCell.innerHTML; if (/\[recherchée?\]/i.test(originalHTML)) { nameCell.innerHTML = originalHTML.replace(/\s*\[recherchée?\]/gi, ''); // Ajout du label "Recherché" const label = document.createElement('div'); label.textContent = 'RECHERCHÉ'; Object.assign(label.style, { position: 'absolute', bottom: '2px', left: '50%', transform: 'translateX(-50%)', background: 'rgba(192,57,43,0.9)', color: '#fff', padding: '2px 6px', fontSize: '1em', borderRadius: '4px', pointerEvents: 'none', whiteSpace: 'nowrap', zIndex: '6', fontFamily: "'Roboto', sans-serif", lineHeight: '1.6' }); wrapper.appendChild(label); } } if (match) { const observer = new IntersectionObserver(entries => { entries.forEach(entry => { if (entry.isIntersecting) { fetchFunctionFromProfile(match[1], row); observer.disconnect(); } }); }); observer.observe(row); } } // Traitement des avatars pour les PNJs (mise en forme différente) function handlePNJAvatar(img, row) { const td = img.closest('td.tdbc'); if (!td) return; const flagImg = Array.from(row.querySelectorAll('img')).find(img => { const file = img.src.split('/').pop(); return flagColors.hasOwnProperty(file); }); const color = flagColors[flagImg?.src.split('/').pop()] || '#444'; colorRow(row, color); td.replaceChildren(createAvatarWrapper(img, color)); const nameCell = row.querySelectorAll('td')[1]; if (nameCell) { styleNameCell(nameCell); const nameLink = nameCell.querySelector('a'); if (nameLink) { nameLink.style.fontWeight = 'normal'; nameLink.style.fontSize = '1.1em'; } let nameText = nameCell.innerHTML; nameText = nameText.replace(/<p[^>]*>/g, '').replace(/<\/p>/g, '').replace(/<br><br>/g, '<br>'); const regex = /(.+?)\s*\[(.+?)\]/; const match = nameText.match(regex); if (match) { const name = match[1].trim(); const rawTitle = match[2].trim(); const title = rawTitle.replace(/^\p{L}/u, c => c.toUpperCase()); // Majuscule Unicode let titleIcon = title === 'Capturé' ? lucideIcons.link : lucideIcons.user; let titleHtml = `${titleIcon}<i>${title}</i>`; let nameHtml = `<a href="${nameLink.href}" style="font-weight: bold; font-size: 1.1em;">${name}</a>`; if (!nameText.includes(titleHtml)) { nameCell.innerHTML = `${nameHtml}<br>${titleHtml}`; } else { nameCell.innerHTML = nameHtml; } } } } // Récupère les infos "fonction" et "domiciliation" depuis la page de profil d’un personnage async function fetchFunctionFromProfile(persoId, row) { if (functionCache[persoId] !== undefined) { insertFunctionBelowName(row, functionCache[persoId].func, functionCache[persoId].dom); return; } insertFunctionBelowName(row, 'Chargement...', '', true); try { const res = await fetch(`main.php?p=6_1_0_1&p1=${persoId}`); if (!res.ok) throw new Error('Network response was not ok'); const buffer = await res.arrayBuffer(); const html = new TextDecoder('iso-8859-1').decode(buffer); const doc = new DOMParser().parseFromString(html, 'text/html'); const tds = Array.from(doc.querySelectorAll('td')); const fonction = tds.find(td => td.textContent.trim() === 'Fonction')?.nextElementSibling?.textContent.trim() || ''; const domiciliation = tds.find(td => td.textContent.trim() === 'Domiciliation')?.nextElementSibling?.textContent.trim() || ''; functionCache[persoId] = { func: fonction, dom: domiciliation }; row.querySelector('.avatar-function')?.remove(); insertFunctionBelowName(row, fonction, domiciliation); } catch (error) { insertFunctionBelowName(row, 'Erreur de chargement', '', true); } } // Vérifie si une ligne <tr> appartient à un groupe (nécessaire pour ne pas traiter les lignes isolées) function isInsideGroupRow(row) { let prev = row?.previousElementSibling; while (prev) { if (prev.tagName === 'TR' && prev.textContent.includes('Groupe')) return true; prev = prev.previousElementSibling; } return false; } // Traite toutes les lignes du tableau pour modifier les avatars function resizeAvatars() { document.querySelectorAll('table .tdbc img').forEach(img => { const src = img.src; if (src.includes('img.kraland.org/2/mat/')) return; const td = img.closest('td.tdbc'); const row = img.closest('tr'); if (!row || !td || !isInsideGroupRow(row)) return; if (row.querySelector('a[href*="order.php?p1=200"]')) return; const isPNJ = src.includes('img.kraland.org/2/npc/'); if (isPNJ) return handlePNJAvatar(img, row); handlePJAvatar(img, row, td); }); } // Ajoute un séparateur entre deux groupes affichés dans la même table function insertGroupSeparator() { const rows = document.querySelectorAll('table tr'); let groupCount = 0; rows.forEach(row => { const hdr = row.querySelector('th a[href*="order.php"]'); const isSpecial = row.querySelector('th a[href="order.php?p1=3004"]'); if (!hdr || isSpecial) return; groupCount++; const next = row.nextElementSibling; if (groupCount > 1 && next && next.querySelector('td')) { const sep = document.createElement('tr'); sep.innerHTML = '<td colspan="2"> </td>'; row.parentNode.insertBefore(sep, row); } }); } // Lancement du traitement une fois que le DOM est chargé if (document.readyState === 'loading') { document.addEventListener('DOMContentLoaded', () => { resizeAvatars(); insertGroupSeparator(); }); } else { resizeAvatars(); insertGroupSeparator(); } // Ajout de styles CSS pour les en-têtes de groupe arrondis const style = document.createElement('style'); style.textContent = ` .thb { padding: 8px; text-align: center; border-radius: 15px; border-left: none !important; border-right: none !important; border-top: none !important; border-bottom: none !important; box-shadow: 0 4px 8px rgba(0, 0, 0, 0.1); } `; document.head.appendChild(style); })();