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);
})();