Version TEST
// ==UserScript==
// @name Dreadcast - PimpMyPion v0.5.2 [VERSION TEST]
// @namespace http://tampermonkey.net/
// @version 0.5.2
// @description Version TEST
// @author Darlene
// @match https://www.dreadcast.net/*
// @match http://www.dreadcast.net/*
// @grant none
// @run-at document-end
// ==/UserScript==
(function () {
'use strict';
// ============================================================
// CONFIGURATION
// ============================================================
const CONFIG = Object.freeze({
// Clefs stockage
STORAGE_KEY_SIZE: 'dreadcast_avatar_size',
STORAGE_KEY_ENABLED: 'dreadcast_avatar_enabled',
STORAGE_KEY_EMOJI_ENABLED: 'dreadcast_emoji_enabled',
STORAGE_KEY_EMOJI_SIZE: 'dreadcast_emoji_size',
STORAGE_KEY_CUSTOM_COLORS: 'dreadcast_custom_colors',
STORAGE_KEY_COLOR_OPACITY: 'dreadcast_color_opacity',
// Paramètres de taille(en %)
DEFAULT_SIZE: 100,
MIN_SIZE: 75,
MAX_SIZE: 125,
// Paramètres de taille emoji (en px)
DEFAULT_EMOJI_SIZE: 12,
MIN_EMOJI_SIZE: 12,
MAX_EMOJI_SIZE: 28,
// Paramètres d'opacité des couleurs (en %)
DEFAULT_COLOR_OPACITY: 100,
MIN_COLOR_OPACITY: 0,
MAX_COLOR_OPACITY: 100,
// URLs & chemins
AVATAR_BASE_URL: 'https://www.dreadcast.net/images/avatars/',
// Timing (millisecondes)
REAPPLY_INTERVAL: 50,
RAF_THROTTLE: 0,
INIT_DELAY: 2000,
SECONDARY_DELAY: 5000,
MENU_CHECK_INTERVAL: 500,
MENU_CHECK_TIMEOUT: 10000,
EVENT_ATTACH_DELAY: 100,
COMBAT_CHECK_INTERVAL: 200,
// Z-indices
Z_INDEX_AVATAR: 999,
Z_INDEX_EMOJI: 1001,
Z_INDEX_OVERLAY: 999999,
Z_INDEX_PANEL: 1000000,
// Selecteurs CSS
SELECTOR_PIONS: '.personnages .icon_perso',
SELECTOR_ICON: '.le_icon_perso',
SELECTOR_INFO: '.info_a_afficher',
SELECTOR_SETTINGS_MENU: '.parametres ul',
SELECTOR_COMBAT: '#combat_carte',
SELECTOR_PLAYER_ACTION: '#icon_action',
// Classes CSS
CLASS_AVATAR_IMG: 'custom-avatar-img',
CLASS_CONNECTED: 'connecte',
CLASS_ACTION_EMOJI: 'action-emoji',
// Data attributes
ATTR_AVATAR_STATUS: 'data-avatar-applied',
ATTR_PLAYER_NAME: 'data-player-name',
ATTR_CURRENT_ACTION: 'data-current-action',
// Status avatars
STATUS_SUCCESS: 'success',
STATUS_FAILED: 'failed',
// Couleurs par action
ACTION_COLORS: {
'en_combat': '#ef4444', // Rouge vif - Combat
'encombat': '#ef4444', // Rouge vif - Combat (variante)
'aucune': '#9ca3af', // Gris - Aucune action
'noaction': '#9ca3af', // Gris - Aucune action (variante)
'repos': '#06b6d4', // Cyan - Repos
'recherche': '#f59e0b', // Jaune/Or - Fouille
'cacher': '#8b5cf6', // Violet - Cacher
'scruter': '#f97316', // Orange - Scruter
'soin': '#10b981', // Vert - Soigner
'travail': '#92400e', // Marron - Travailler
'ko': '#1f2937', // Gris foncé - Mort
'destruction': '#ef4444', // Rouge vif
'reparation': '#ef4444' // Rouge vif
},
// Emojis par action
ACTION_EMOJIS: {
'en_combat': '⚔️',
'encombat': '⚔️',
'aucune': '⏸️',
'noaction': '⏸️',
'repos': '😴',
'recherche': '⛏️',
'cacher': '🫣',
'scruter': '👀',
'soin': '💊',
'travail': '⚙️',
'ko': '💀',
'destruction': '💥',
'reparation': '🔧'
},
// Couleurs de base (fallback)
COLOR_CONNECTED: '#4ade80',
COLOR_DISCONNECTED: '#ffffff',
// Divers
DEBUG_MODE: false
});
// ============================================================
// Gestion des état
// ============================================================
class AvatarState {
constructor() {
this.avatarCache = new Map();
this.avatarUrlCache = new Map();
this.actionCache = new Map();
this.reapplyIntervalId = null;
this.reapplyAnimationFrameId = null;
this.lastReapplyTime = 0;
}
clearCaches() {
this.avatarCache.clear();
this.avatarUrlCache.clear();
this.actionCache.clear();
}
stopReapplication() {
if (this.reapplyIntervalId) {
clearInterval(this.reapplyIntervalId);
this.reapplyIntervalId = null;
}
if (this.reapplyAnimationFrameId) {
cancelAnimationFrame(this.reapplyAnimationFrameId);
this.reapplyAnimationFrameId = null;
}
}
}
const state = new AvatarState();
// ============================================================
// Détection des actions
// ============================================================
const ActionDetector = {
/**
* Détecte l'action d'un joueur à partir de son pion
* @param {HTMLElement} pionElement - L'élément .icon_perso
* @returns {string|null} - Le nom de l'action détectée ou null
*/
getPlayerAction(pionElement) {
try {
// Vérifier le cache
if (state.actionCache.has(pionElement)) {
const cached = state.actionCache.get(pionElement);
// Vérifier si le cache est encore valide (< 500ms)
if (Date.now() - cached.timestamp < 500) {
return cached.action;
}
}
// Chercher l'action dans .le_icon_perso
const iconElement = pionElement.querySelector(CONFIG.SELECTOR_ICON);
if (!iconElement) return null;
// Les classes possibles pour les actions
const actionClasses = [
'en_combat', 'encombat',
'recherche', 'fouille',
'repos',
'cacher',
'scruter',
'soin',
'travail',
'destruction',
'reparation',
'aucune', 'noaction',
'ko'
];
// Chercher quelle action est active
for (const actionClass of actionClasses) {
if (iconElement.classList.contains(actionClass)) {
// Mettre en cache
state.actionCache.set(pionElement, {
action: actionClass,
timestamp: Date.now()
});
return actionClass;
}
}
// Si aucune action trouvée, retourner null
return null;
} catch (error) { return null;
}
},
/**
* Récupère la couleur correspondant à une action
* @param {string} action - Le nom de l'action
* @returns {string} - La couleur hexadécimale
*/
getActionColor(action) {
if (!action) return Storage.getColorForStatus(false);
return Storage.getColorForAction(action.toLowerCase());
},
/**
* Détecte l'action du joueur principal via #icon_action
* @returns {string|null} - Le nom de l'action détectée ou null
*/
getMainPlayerAction() {
try {
const iconAction = document.querySelector(CONFIG.SELECTOR_PLAYER_ACTION);
if (!iconAction) return null;
// Les classes possibles sur #icon_action
const actionClasses = [
'actionEncombat',
'actionAucune',
'actionRepos',
'actionFouille',
'actionCacher',
'actionScruter',
'actionSoin',
'actionTravailler',
'actionDetruire',
'actionReparer',
'actionKo',
];
for (const actionClass of actionClasses) {
if (iconAction.classList.contains(actionClass)) {
// Convertir le nom de classe en nom d'action
const actionName = actionClass.replace('action', '').toLowerCase();
return actionName;
}
}
return null;
} catch (error) { return null;
}
},
/**
* Vide le cache des actions
*/
clearCache() {
state.actionCache.clear(); },
/**
* Récupère l'emoji correspondant à une action
* @param {string} action - Le nom de l'action
* @returns {string|null} - L'emoji ou null
*/
getActionEmoji(action) {
if (!action) return null;
const emoji = CONFIG.ACTION_EMOJIS[action.toLowerCase()];
return emoji || null;
},
/**
* Crée ou met à jour l'élément emoji sur un avatar
* @param {HTMLElement} iconElement - L'élément .le_icon_perso
* @param {string} action - Le nom de l'action
*/
updateActionEmoji(iconElement, action) {
try {
if (!Storage.loadEmojiEnabled()) {
// Supprimer l'emoji s'il existe
const existingEmoji = iconElement.querySelector(`.${CONFIG.CLASS_ACTION_EMOJI}`);
if (existingEmoji) {
existingEmoji.remove();
Utils.debugLog('🗑️ Emoji supprimé (fonction désactivée)');
}
return;
}
const emoji = this.getActionEmoji(action);
if (!emoji) {
// Si pas d'emoji pour cette action, supprimer l'emoji existant
const existingEmoji = iconElement.querySelector(`.${CONFIG.CLASS_ACTION_EMOJI}`);
if (existingEmoji) {
existingEmoji.remove();
Utils.debugLog('🗑️ Emoji supprimé (aucun emoji pour cette action)');
}
return;
}
let emojiElement = iconElement.querySelector(`.${CONFIG.CLASS_ACTION_EMOJI}`);
if (!emojiElement) {
// Créer un nouveau span pour l'emoji
emojiElement = document.createElement('span');
emojiElement.className = CONFIG.CLASS_ACTION_EMOJI;
iconElement.appendChild(emojiElement); }
// Mettre à jour le contenu de l'emoji si nécessaire
if (emojiElement.textContent !== emoji) {
emojiElement.textContent = emoji; }
} catch (error) { }
},
/**
* Supprime l'emoji d'action d'un avatar
* @param {HTMLElement} iconElement - L'élément .le_icon_perso
*/
removeActionEmoji(iconElement) {
try {
const emojiElement = iconElement.querySelector(`.${CONFIG.CLASS_ACTION_EMOJI}`);
if (emojiElement) {
emojiElement.remove(); }
} catch (error) { }
}
};
// ============================================================
// COMBAT DETECTOR
// ============================================================
const CombatDetector = {
isInCombat: false,
savedSize: null,
checkInterval: null,
detectCombat() {
return document.querySelector(CONFIG.SELECTOR_COMBAT) !== null;
},
start() {
this.checkInterval = setInterval(() => {
const inCombat = this.detectCombat();
if (inCombat && !this.isInCombat) {
this.onEnterCombat();
} else if (!inCombat && this.isInCombat) {
this.onExitCombat();
}
}, CONFIG.COMBAT_CHECK_INTERVAL);
},
onEnterCombat() {
const currentSize = Storage.loadAvatarSize();
this.savedSize = currentSize;
this.isInCombat = true;
SizingSystem.applyAvatarSize(100);
this.disableSlider();
},
onExitCombat() {
this.isInCombat = false;
if (this.savedSize !== null) {
SizingSystem.applyAvatarSize(this.savedSize); }
this.enableSlider();
this.savedSize = null;
},
disableSlider() {
const slider = document.getElementById('avatar-size-slider');
const valueDisplay = document.getElementById('avatar-size-value');
if (slider) {
slider.disabled = true;
slider.style.opacity = '0.5';
slider.style.cursor = 'not-allowed';
}
if (valueDisplay) {
valueDisplay.textContent = '100% ⚔️';
valueDisplay.style.color = '#ef4444';
}
},
enableSlider() {
const slider = document.getElementById('avatar-size-slider');
const valueDisplay = document.getElementById('avatar-size-value');
if (slider) {
slider.disabled = false;
slider.style.opacity = '1';
slider.style.cursor = 'pointer';
const currentSize = Storage.loadAvatarSize();
slider.value = currentSize;
if (valueDisplay) {
valueDisplay.textContent = `${currentSize}%`;
valueDisplay.style.color = '#667eea';
}
}
},
stop() {
if (this.checkInterval) {
clearInterval(this.checkInterval);
this.checkInterval = null;
} }
};
// ============================================================
// Fonctions utilitaires
// ============================================================
const Utils = {
debugLog(message, ...args) {
if (CONFIG.DEBUG_MODE) {
console.log(`[Dreadcast PimpMyPion] ${message}`, ...args);
}
},
encodePlayerName(name) {
return encodeURIComponent(name);
},
buildAvatarUrl(playerName) {
return `${CONFIG.AVATAR_BASE_URL}${this.encodePlayerName(playerName)}.png`;
},
throttle(fn, delay) {
let lastCall = 0;
return function (...args) {
const now = Date.now();
if (now - lastCall < delay) return;
lastCall = now;
fn.apply(this, args);
};
},
/**
* Convertit une couleur hexadécimale en rgba avec opacité
* @param {string} hex - Couleur hex (#rrggbb)
* @param {number} opacity - Opacité de 0 à 1
* @returns {string} - Couleur rgba
*/
hexToRgba(hex, opacity) {
// Supprimer le # si présent
hex = hex.replace('#', '');
// Convertir en RGB
const r = parseInt(hex.substring(0, 2), 16);
const g = parseInt(hex.substring(2, 4), 16);
const b = parseInt(hex.substring(4, 6), 16);
return `rgba(${r}, ${g}, ${b}, ${opacity})`;
}
};
// ============================================================
// Fonctions de stockage
// ============================================================
const Storage = {
loadAvatarSize() {
const savedSize = localStorage.getItem(CONFIG.STORAGE_KEY_SIZE);
return savedSize ? parseInt(savedSize, 10) : CONFIG.DEFAULT_SIZE;
},
saveAvatarSize(size) {
localStorage.setItem(CONFIG.STORAGE_KEY_SIZE, String(size)); },
loadAvatarEnabled() {
const savedEnabled = localStorage.getItem(CONFIG.STORAGE_KEY_ENABLED);
return savedEnabled === null ? true : savedEnabled === 'true';
},
saveAvatarEnabled(enabled) {
localStorage.setItem(CONFIG.STORAGE_KEY_ENABLED, String(enabled)); },
loadEmojiEnabled() {
const savedEnabled = localStorage.getItem(CONFIG.STORAGE_KEY_EMOJI_ENABLED);
return savedEnabled === null ? true : savedEnabled === 'true';
},
saveEmojiEnabled(enabled) {
localStorage.setItem(CONFIG.STORAGE_KEY_EMOJI_ENABLED, String(enabled)); },
loadEmojiSize() {
const savedSize = localStorage.getItem(CONFIG.STORAGE_KEY_EMOJI_SIZE);
return savedSize ? parseInt(savedSize, 10) : CONFIG.DEFAULT_EMOJI_SIZE;
},
saveEmojiSize(size) {
localStorage.setItem(CONFIG.STORAGE_KEY_EMOJI_SIZE, String(size)); },
loadCustomColors() {
const saved = localStorage.getItem(CONFIG.STORAGE_KEY_CUSTOM_COLORS);
return saved ? JSON.parse(saved) : {};
},
saveCustomColors(colors) {
localStorage.setItem(CONFIG.STORAGE_KEY_CUSTOM_COLORS, JSON.stringify(colors)); },
loadColorOpacity() {
const saved = localStorage.getItem(CONFIG.STORAGE_KEY_COLOR_OPACITY);
return saved ? parseInt(saved, 10) : CONFIG.DEFAULT_COLOR_OPACITY;
},
saveColorOpacity(opacity) {
localStorage.setItem(CONFIG.STORAGE_KEY_COLOR_OPACITY, String(opacity)); },
getColorForAction(action) {
const customColors = this.loadCustomColors();
const opacity = this.loadColorOpacity() / 100; // Convertir % en 0-1
let hexColor;
if (customColors[action]) {
hexColor = customColors[action];
} else {
hexColor = CONFIG.ACTION_COLORS[action] || CONFIG.COLOR_CONNECTED;
}
return Utils.hexToRgba(hexColor, opacity);
},
getColorForStatus(isConnected) {
const customColors = this.loadCustomColors();
const opacity = this.loadColorOpacity() / 100; // Convertir % en 0-1
let hexColor;
if (isConnected && customColors['connected']) {
hexColor = customColors['connected'];
} else if (!isConnected && customColors['disconnected']) {
hexColor = customColors['disconnected'];
} else {
hexColor = isConnected ? CONFIG.COLOR_CONNECTED : CONFIG.COLOR_DISCONNECTED;
}
return Utils.hexToRgba(hexColor, opacity);
}
};
// ============================================================
// DOM UTILITIES
// ============================================================
const DOMUtils = {
getPlayerNameFromPion(pionElement) {
try {
const cachedName = pionElement.getAttribute(CONFIG.ATTR_PLAYER_NAME);
if (cachedName) return cachedName;
const infoElement = pionElement.querySelector(CONFIG.SELECTOR_INFO);
if (infoElement?.textContent) {
const playerName = infoElement.textContent.trim();
pionElement.setAttribute(CONFIG.ATTR_PLAYER_NAME, playerName);
return playerName;
}
} catch (error) { }
return null;
},
isAvatarValid(pionElement) {
const iconElement = pionElement.querySelector(CONFIG.SELECTOR_ICON);
if (!iconElement) return false;
const avatarImg = iconElement.querySelector(`.${CONFIG.CLASS_AVATAR_IMG}`);
if (!avatarImg) return false;
const isAttached = avatarImg.parentElement !== null;
const isVisible = avatarImg.style.display !== 'none' &&
avatarImg.style.visibility !== 'hidden' &&
avatarImg.style.opacity !== '0';
return isAttached && isVisible;
},
getAllPions() {
return document.querySelectorAll(CONFIG.SELECTOR_PIONS);
}
};
// ============================================================
// PIE CHART MANAGER - Gestion des pions multiples
// ============================================================
const PieChartManager = {
debugMode: true, // ✅ Mode debug activé pour diagnostiquer
/**
* Vérifie si un pion est visible
* @param {HTMLElement} pionElement - L'élément pion
* @returns {boolean} - true si visible
*/
isPionVisible(pionElement) {
try {
const style = window.getComputedStyle(pionElement);
return style.display !== 'none' &&
style.visibility !== 'hidden' &&
style.opacity !== '0' &&
pionElement.offsetParent !== null;
} catch (error) {
return false;
}
},
/**
* Parse les noms des joueurs depuis .info_a_afficher
* @param {HTMLElement} pionContainer - Le conteneur .icon_perso
* @returns {Array<string>} - Liste des noms de joueurs
*/
parsePlayerNames(pionContainer) {
try {
const infoElement = pionContainer.querySelector('.info_a_afficher');
if (!infoElement) return [];
// Les noms sont séparés par <br /> ou <br> dans le HTML
const innerHTML = infoElement.innerHTML;
const names = innerHTML.split(/<br\s*\/?>/i)
.map(name => name.trim())
.filter(name => name.length > 0);
return names;
} catch (error) {
console.error('[PMP DEBUG] Erreur parsePlayerNames:', error);
return [];
}
},
/**
* Détecte les pions multiples (UN .icon_perso avec plusieurs .le_icon_perso dedans)
* @returns {Array} - Tableau d'objets avec les pions multiples
*/
detectMultiplePions() {
const multiplePions = [];
const allPions = DOMUtils.getAllPions();
console.log('[PMP DEBUG] 🔍 Détection des pions multiples...');
console.log('[PMP DEBUG] Total .icon_perso trouvés:', allPions.length);
let visibleCount = 0;
let multipleCount = 0;
allPions.forEach(pionContainer => {
// Vérifier si le pion est visible
if (!this.isPionVisible(pionContainer)) {
return;
}
visibleCount++;
// ✅ CORRECTION : Compter les .le_icon_perso à l'intérieur
const iconElements = pionContainer.querySelectorAll('.le_icon_perso');
const playerCount = iconElements.length;
if (playerCount >= 2) {
// ✅ Ce pion contient plusieurs joueurs !
multipleCount++;
// Parser les noms
const playerNames = this.parsePlayerNames(pionContainer);
if (this.debugMode) {
console.log(`[PMP DEBUG] 🥧 Pion multiple détecté: ${playerCount} joueurs`);
console.log(`[PMP DEBUG] Noms: ${playerNames.join(', ')}`);
console.log(`[PMP DEBUG] Container:`, pionContainer);
}
// Vérifier que le nombre de noms correspond au nombre de .le_icon_perso
if (playerNames.length !== playerCount) {
console.warn(`[PMP DEBUG] ⚠️ Mismatch: ${playerCount} .le_icon_perso mais ${playerNames.length} noms`);
}
multiplePions.push({
container: pionContainer,
iconElements: Array.from(iconElements),
playerNames: playerNames,
count: playerCount
});
}
});
console.log('[PMP DEBUG] Pions visibles:', visibleCount);
console.log('[PMP DEBUG] Pions multiples détectés:', multipleCount);
return multiplePions;
},
/**
* Calcule les angles de début et fin pour chaque portion du pie chart
* @param {number} count - Nombre de joueurs
* @returns {Array} - Tableau d'objets {startAngle, endAngle}
*/
calculateAngles(count) {
const anglePerSlice = 360 / count;
const angles = [];
for (let i = 0; i < count; i++) {
const startAngle = i * anglePerSlice - 90; // -90 pour commencer en haut
const endAngle = (i + 1) * anglePerSlice - 90;
angles.push({ startAngle, endAngle });
}
return angles;
},
/**
* Convertit des degrés en radians
* @param {number} degrees - Angle en degrés
* @returns {number} - Angle en radians
*/
degreesToRadians(degrees) {
return degrees * (Math.PI / 180);
},
/**
* Génère le path SVG pour une portion de pie chart
* @param {number} startAngle - Angle de début (en degrés)
* @param {number} endAngle - Angle de fin (en degrés)
* @param {number} radius - Rayon du cercle
* @param {number} cx - Centre X
* @param {number} cy - Centre Y
* @returns {string} - Path SVG
*/
generatePiePath(startAngle, endAngle, radius = 50, cx = 50, cy = 50) {
const startRad = this.degreesToRadians(startAngle);
const endRad = this.degreesToRadians(endAngle);
const x1 = cx + radius * Math.cos(startRad);
const y1 = cy + radius * Math.sin(startRad);
const x2 = cx + radius * Math.cos(endRad);
const y2 = cy + radius * Math.sin(endRad);
const largeArcFlag = endAngle - startAngle > 180 ? 1 : 0;
return `M ${cx},${cy} L ${x1},${y1} A ${radius},${radius} 0 ${largeArcFlag},1 ${x2},${y2} Z`;
},
/**
* Extrait les données de chaque joueur et les trie par priorité
* @param {Object} multiplePionData - Objet {container, iconElements, playerNames, count}
* @returns {Array} - Liste triée des joueurs avec leurs données
*/
extractAndSortPlayers(multiplePionData) {
const { iconElements, playerNames } = multiplePionData;
const playersData = iconElements.map((iconElement, index) => {
const playerName = playerNames[index] || `Joueur ${index + 1}`;
// Détecter l'action depuis les classes de .le_icon_perso
let action = null;
const actionClasses = [
'en_combat', 'encombat',
'recherche', 'fouille',
'repos',
'cacher',
'scruter',
'soin',
'travail',
'destruction',
'reparation',
'aucune', 'noaction',
'ko'
];
for (const actionClass of actionClasses) {
if (iconElement.classList.contains(actionClass)) {
action = actionClass;
break;
}
}
// Vérifier si le joueur est connecté
const isConnected = iconElement.classList.contains(CONFIG.CLASS_CONNECTED);
// Emoji d'action
const emoji = ActionDetector.getActionEmoji(action) || '';
// URL de l'avatar
const avatarUrl = Utils.buildAvatarUrl(playerName);
// Calculer la priorité
let priority = 0;
if (action === 'en_combat' || action === 'encombat') {
priority = 3; // Combat = priorité la plus haute
} else if (isConnected) {
priority = 2; // Connecté
} else {
priority = 1; // Déconnecté
}
return {
iconElement,
playerName,
action,
isConnected,
emoji,
avatarUrl,
priority
};
});
// Trier par priorité décroissante
return playersData.sort((a, b) => b.priority - a.priority);
},
/**
* Crée le SVG du pie chart avec les avatars
* @param {Array} playersData - Données des joueurs triés
* @param {HTMLElement} container - Conteneur où insérer le pie chart
* @returns {SVGElement} - Élément SVG créé
*/
createPieChart(playersData, container) {
const count = playersData.length;
const angles = this.calculateAngles(count);
// Créer le SVG
// 🎨 TAILLE : 50px choisie pour être proportionnée aux pions vanilla (environ 70% de leur taille)
// Cette taille permet d'avoir un pie chart lisible sans être trop imposant
const svg = document.createElementNS('http://www.w3.org/2000/svg', 'svg');
svg.setAttribute('viewBox', '0 0 100 100');
svg.setAttribute('class', 'pie-chart-svg');
svg.style.cssText = `
width: 50px;
height: 50px;
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
z-index: ${CONFIG.Z_INDEX_AVATAR};
pointer-events: none;
`;
// Créer le groupe defs pour les clipPaths
const defs = document.createElementNS('http://www.w3.org/2000/svg', 'defs');
svg.appendChild(defs);
// Créer chaque portion avec son avatar
playersData.forEach((player, index) => {
const { startAngle, endAngle } = angles[index];
const sliceId = `slice-${Date.now()}-${index}`;
// Créer le clipPath pour cette portion
const clipPath = document.createElementNS('http://www.w3.org/2000/svg', 'clipPath');
clipPath.setAttribute('id', sliceId);
const clipPathPath = document.createElementNS('http://www.w3.org/2000/svg', 'path');
clipPathPath.setAttribute('d', this.generatePiePath(startAngle, endAngle));
clipPath.appendChild(clipPathPath);
defs.appendChild(clipPath);
// Créer l'image de l'avatar avec le clipPath
const image = document.createElementNS('http://www.w3.org/2000/svg', 'image');
image.setAttributeNS('http://www.w3.org/1999/xlink', 'href', player.avatarUrl);
image.setAttribute('x', '0');
image.setAttribute('y', '0');
image.setAttribute('width', '100');
image.setAttribute('height', '100');
image.setAttribute('clip-path', `url(#${sliceId})`);
image.setAttribute('preserveAspectRatio', 'xMidYMid slice');
svg.appendChild(image);
// Créer la bordure blanche de la portion
const borderPath = document.createElementNS('http://www.w3.org/2000/svg', 'path');
borderPath.setAttribute('d', this.generatePiePath(startAngle, endAngle));
borderPath.setAttribute('fill', 'none');
borderPath.setAttribute('stroke', 'white');
borderPath.setAttribute('stroke-width', '2');
svg.appendChild(borderPath);
});
// Créer le cercle central transparent pour la zone de survol
const centerCircle = document.createElementNS('http://www.w3.org/2000/svg', 'circle');
centerCircle.setAttribute('cx', '50');
centerCircle.setAttribute('cy', '50');
centerCircle.setAttribute('r', '15');
centerCircle.setAttribute('fill', 'rgba(0, 0, 0, 0.7)');
centerCircle.setAttribute('class', 'pie-chart-center');
centerCircle.style.pointerEvents = 'auto';
centerCircle.style.cursor = 'pointer';
svg.appendChild(centerCircle);
// Ajouter le texte au centre (nombre de joueurs)
const text = document.createElementNS('http://www.w3.org/2000/svg', 'text');
text.setAttribute('x', '50');
text.setAttribute('y', '50');
text.setAttribute('text-anchor', 'middle');
text.setAttribute('dominant-baseline', 'middle');
text.setAttribute('fill', 'white');
text.setAttribute('font-size', '14');
text.setAttribute('font-weight', 'bold');
text.setAttribute('class', 'pie-chart-count');
text.textContent = count;
text.style.pointerEvents = 'none';
svg.appendChild(text);
// Attacher les événements de survol
this.attachHoverEvents(centerCircle, playersData, container);
return svg;
},
/**
* Crée le popup avec la liste des joueurs
* @param {Array} playersData - Données des joueurs
* @returns {HTMLElement} - Élément popup
*/
createPopup(playersData) {
const popup = document.createElement('div');
popup.className = 'pie-chart-popup';
popup.style.cssText = `
position: absolute;
bottom: 100%;
left: 50%;
transform: translateX(-50%) translateY(-10px);
background: rgba(26, 26, 26, 0.98);
border: 1px solid #4a9eff;
border-radius: 8px;
padding: 12px;
min-width: 200px;
box-shadow: 0 4px 16px rgba(0, 0, 0, 0.5);
z-index: ${CONFIG.Z_INDEX_PANEL};
opacity: 0;
transition: opacity 0.2s ease, transform 0.2s ease;
pointer-events: none;
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
`;
// Header
const header = document.createElement('div');
header.style.cssText = `
color: #4a9eff;
font-weight: 600;
font-size: 13px;
margin-bottom: 8px;
padding-bottom: 8px;
border-bottom: 1px solid rgba(74, 158, 255, 0.3);
`;
header.textContent = `👥 ${playersData.length} joueurs sur la case`;
popup.appendChild(header);
// Liste des joueurs
playersData.forEach(player => {
const playerRow = document.createElement('div');
playerRow.style.cssText = `
display: flex;
align-items: center;
gap: 8px;
padding: 6px;
margin: 4px 0;
background: rgba(42, 42, 42, 0.5);
border-radius: 4px;
`;
// Avatar
const avatar = document.createElement('img');
avatar.src = player.avatarUrl;
avatar.style.cssText = `
width: 32px;
height: 32px;
border-radius: 50%;
border: 2px solid ${player.isConnected ? '#4ade80' : '#ffffff'};
object-fit: cover;
`;
playerRow.appendChild(avatar);
// Nom
const name = document.createElement('span');
name.textContent = player.playerName;
name.style.cssText = `
flex: 1;
color: #ffffff;
font-size: 13px;
font-weight: 500;
`;
playerRow.appendChild(name);
// Emoji d'action
if (player.emoji) {
const emoji = document.createElement('span');
emoji.textContent = player.emoji;
emoji.style.cssText = `
font-size: 16px;
`;
playerRow.appendChild(emoji);
}
popup.appendChild(playerRow);
});
return popup;
},
/**
* Attache les événements de survol au centre du pie chart
* @param {SVGElement} centerCircle - Cercle central du pie chart
* @param {Array} playersData - Données des joueurs
* @param {HTMLElement} container - Conteneur du pie chart
*/
attachHoverEvents(centerCircle, playersData, container) {
let popup = null;
let hoverTimeout = null;
centerCircle.addEventListener('mouseenter', () => {
hoverTimeout = setTimeout(() => {
// Créer le popup
popup = this.createPopup(playersData);
container.appendChild(popup);
// Animer l'apparition
setTimeout(() => {
popup.style.opacity = '1';
popup.style.transform = 'translateX(-50%) translateY(-5px)';
}, 10);
}, 300); // Délai de 300ms
});
centerCircle.addEventListener('mouseleave', () => {
if (hoverTimeout) {
clearTimeout(hoverTimeout);
hoverTimeout = null;
}
if (popup) {
popup.style.opacity = '0';
popup.style.transform = 'translateX(-50%) translateY(-10px)';
setTimeout(() => {
if (popup && popup.parentElement) {
popup.remove();
}
popup = null;
}, 200);
}
});
},
/**
* Applique le pie chart aux pions multiples
*/
applyPieCharts() {
console.log('[PMP DEBUG] 🥧 Application des pie charts...');
const multiplePions = this.detectMultiplePions();
if (multiplePions.length === 0) {
console.log('[PMP DEBUG] ✅ Aucun pion multiple détecté');
return;
}
console.log(`[PMP DEBUG] 🎯 ${multiplePions.length} pion(s) multiple(s) à traiter`);
let pieChartCreatedCount = 0;
let pieChartFailedCount = 0;
multiplePions.forEach((pionData, index) => {
const { container, iconElements, playerNames, count } = pionData;
console.log(`[PMP DEBUG] 📍 Traitement pion ${index + 1}/${multiplePions.length}`);
console.log(`[PMP DEBUG] ${count} joueurs: ${playerNames.join(', ')}`);
// Extraire et trier les joueurs par priorité
const sortedPlayers = this.extractAndSortPlayers(pionData);
console.log('[PMP DEBUG] ✅ Joueurs triés par priorité:', sortedPlayers.map(p => p.playerName));
// Supprimer tous les avatars existants sur tous les .le_icon_perso
iconElements.forEach((iconEl, idx) => {
const existingAvatar = iconEl.querySelector(`.${CONFIG.CLASS_AVATAR_IMG}`);
if (existingAvatar) {
existingAvatar.remove();
console.log(`[PMP DEBUG] 🗑️ Avatar existant supprimé sur .le_icon_perso ${idx + 1}`);
}
// Supprimer tout pie chart existant
const existingPieChart = iconEl.querySelector('.pie-chart-svg');
if (existingPieChart) {
existingPieChart.remove();
console.log(`[PMP DEBUG] 🗑️ Pie chart existant supprimé sur .le_icon_perso ${idx + 1}`);
}
});
// IMPORTANT : Créer le pie chart sur le PREMIER .le_icon_perso seulement
const mainIconElement = iconElements[0];
// FIX v0.5.2 : S'assurer que le conteneur a position: relative pour le centrage
const computedStyle = window.getComputedStyle(mainIconElement);
console.log('[PMP DEBUG] 📍 Position du conteneur AVANT:', computedStyle.position);
// Force position: relative si ce n'est pas déjà le cas
if (computedStyle.position === 'static') {
mainIconElement.style.position = 'relative';
console.log('[PMP DEBUG] ✅ Position forcée à relative');
}
// Créer et insérer le pie chart
try {
const pieChart = this.createPieChart(sortedPlayers, mainIconElement);
console.log('[PMP DEBUG] ✅ Pie chart créé:', pieChart);
mainIconElement.appendChild(pieChart);
console.log('[PMP DEBUG] ✅ Pie chart inséré dans le DOM');
// FIX v0.5.2 : Logs de diagnostic du positionnement
const pieChartStyle = window.getComputedStyle(pieChart);
const containerRect = mainIconElement.getBoundingClientRect();
const pieChartRect = pieChart.getBoundingClientRect();
console.log('[PMP DEBUG] 📊 Diagnostic du positionnement:');
console.log('[PMP DEBUG] Conteneur (.le_icon_perso):', {
position: computedStyle.position,
width: `${containerRect.width}px`,
height: `${containerRect.height}px`,
top: `${containerRect.top}px`,
left: `${containerRect.left}px`
});
console.log('[PMP DEBUG] Pie chart (SVG):', {
position: pieChartStyle.position,
width: pieChartStyle.width,
height: pieChartStyle.height,
top: pieChartStyle.top,
left: pieChartStyle.left,
transform: pieChartStyle.transform,
zIndex: pieChartStyle.zIndex
});
console.log('[PMP DEBUG] Rect du pie chart:', {
top: `${pieChartRect.top}px`,
left: `${pieChartRect.left}px`,
width: `${pieChartRect.width}px`,
height: `${pieChartRect.height}px`
});
console.log('[PMP DEBUG] ✅ Centrage théorique: top: 50%, left: 50%, transform: translate(-50%, -50%)');
// ✅ CORRECTION : Cacher les pions vanilla (.le_icon_perso) de tous les autres iconElements
// Le pie chart est sur le premier .le_icon_perso, on cache les autres
let hiddenPionsCount = 0;
iconElements.forEach((iconEl, idx) => {
if (idx > 0) { // Garder le premier visible (celui avec le pie chart)
iconEl.style.setProperty('display', 'none', 'important');
hiddenPionsCount++;
console.log(`[PMP DEBUG] 👻 Pion vanilla ${idx + 1} caché`);
}
});
console.log(`[PMP DEBUG] ✅ ${hiddenPionsCount} pion(s) vanilla caché(s)`);
// Vérifier que le pie chart est bien visible
const insertedPieChart = mainIconElement.querySelector('.pie-chart-svg');
if (insertedPieChart) {
const style = window.getComputedStyle(insertedPieChart);
console.log('[PMP DEBUG] 📊 Pie chart visible:', {
display: style.display,
visibility: style.visibility,
opacity: style.opacity,
width: style.width,
height: style.height,
position: style.position,
zIndex: style.zIndex
});
pieChartCreatedCount++;
} else {
console.log('[PMP DEBUG] ❌ Pie chart non trouvé après insertion !');
pieChartFailedCount++;
}
// Marquer le container comme traité
container.setAttribute('data-pie-chart-applied', 'true');
console.log('[PMP DEBUG] ✅ Container marqué comme traité');
} catch (error) {
console.error('[PMP DEBUG] ❌ Erreur lors de la création du pie chart:', error);
pieChartFailedCount++;
}
});
console.log(`[PMP DEBUG] 📊 Résumé: ${pieChartCreatedCount} pie chart(s) créé(s), ${pieChartFailedCount} échec(s)`);
}
};
// ============================================================
// Chargement des images
// ============================================================
const ImageLoader = {
async checkImageExists(url, playerName) {
if (state.avatarUrlCache.has(playerName)) {
return state.avatarUrlCache.get(playerName).exists;
}
return new Promise((resolve) => {
const img = new Image();
img.onload = () => {
state.avatarUrlCache.set(playerName, { url, exists: true });
resolve(true);
};
img.onerror = () => {
state.avatarUrlCache.set(playerName, { url, exists: false });
resolve(false);
};
img.src = url;
});
}
};
// ============================================================
// Style des avatars
// ============================================================
const AvatarStyler = {
createAvatarImage(avatarUrl, playerName) {
const img = document.createElement('img');
img.className = CONFIG.CLASS_AVATAR_IMG;
img.src = avatarUrl;
img.alt = playerName;
img.setAttribute('loading', 'eager');
img.setAttribute('decoding', 'sync');
return img;
},
applyAvatarStyles(avatarImg, borderColor) {
const styles = {
width: '20px',
height: '20px',
'object-fit': 'cover',
'border-radius': '50%',
border: `3px solid ${borderColor}`,
'box-shadow': '0 2px 8px rgba(0, 0, 0, 0.3)',
position: 'absolute',
top: '50%',
left: '50%',
transform: 'translate(-50%, -50%)',
'z-index': String(CONFIG.Z_INDEX_AVATAR),
'pointer-events': 'none',
display: 'block',
visibility: 'visible',
opacity: '1',
transition: 'border-color 0.3s ease',
animation: 'none'
};
Object.entries(styles).forEach(([property, value]) => {
avatarImg.style.setProperty(property, value, 'important');
});
},
getBorderColor(pionElement) {
const iconElement = pionElement.querySelector(CONFIG.SELECTOR_ICON);
if (!iconElement) return Storage.getColorForStatus(false);
// ✅ APPROCHE 1 : Suppression des couleurs d'actions
// Ne plus utiliser les couleurs basées sur les actions (combat, repos, fouille, etc.)
// Conserver UNIQUEMENT les couleurs connecté/déconnecté
// ❌ SUPPRIMÉ : Couleurs par action
// const action = ActionDetector.getPlayerAction(pionElement);
// if (action) {
// const actionColor = ActionDetector.getActionColor(action);
// // return actionColor;
// }
// ✅ TOUJOURS utiliser les couleurs connecté/déconnecté
const isConnected = iconElement.classList.contains(CONFIG.CLASS_CONNECTED);
const colorStatus = Storage.getColorForStatus(isConnected); return colorStatus;
}
};
// ============================================================
// Application des avatars
// ============================================================
const AvatarManager = {
async applyCustomAvatar(pionElement, force = false) {
try {
if (!Storage.loadAvatarEnabled()) return;
const cachedStatus = state.avatarCache.get(pionElement);
if (!force && cachedStatus === CONFIG.STATUS_SUCCESS && DOMUtils.isAvatarValid(pionElement)) {
// Même si l'avatar est valide, on doit mettre à jour la bordure
const iconElement = pionElement.querySelector(CONFIG.SELECTOR_ICON);
const avatarImg = iconElement?.querySelector(`.${CONFIG.CLASS_AVATAR_IMG}`);
if (avatarImg) {
const borderColor = AvatarStyler.getBorderColor(pionElement);
avatarImg.style.setProperty('border', `3px solid ${borderColor}`, 'important');
}
return;
}
if (!force && cachedStatus === CONFIG.STATUS_FAILED) return;
const playerName = DOMUtils.getPlayerNameFromPion(pionElement);
if (!playerName) {
state.avatarCache.set(pionElement, CONFIG.STATUS_FAILED);
return;
}
const avatarUrl = Utils.buildAvatarUrl(playerName);
if (!state.avatarUrlCache.has(playerName) || force) {
const exists = await ImageLoader.checkImageExists(avatarUrl, playerName);
if (!exists) {
state.avatarCache.set(pionElement, CONFIG.STATUS_FAILED);
pionElement.setAttribute(CONFIG.ATTR_AVATAR_STATUS, CONFIG.STATUS_FAILED);
return;
}
} else if (!state.avatarUrlCache.get(playerName).exists) {
return;
}
const iconElement = pionElement.querySelector(CONFIG.SELECTOR_ICON);
if (!iconElement) return;
let avatarImg = iconElement.querySelector(`.${CONFIG.CLASS_AVATAR_IMG}`);
if (!avatarImg) {
avatarImg = AvatarStyler.createAvatarImage(avatarUrl, playerName);
if (iconElement.firstChild) {
iconElement.insertBefore(avatarImg, iconElement.firstChild);
} else {
iconElement.appendChild(avatarImg);
}
} else if (!DOMUtils.isAvatarValid(pionElement)) {
avatarImg.src = avatarUrl;
avatarImg.alt = playerName;
}
const borderColor = AvatarStyler.getBorderColor(pionElement);
AvatarStyler.applyAvatarStyles(avatarImg, borderColor);
if (!avatarImg.complete || avatarImg.naturalHeight === 0) {
avatarImg.src = avatarImg.src;
}
pionElement.setAttribute(CONFIG.ATTR_AVATAR_STATUS, CONFIG.STATUS_SUCCESS);
pionElement.setAttribute(CONFIG.ATTR_PLAYER_NAME, playerName);
state.avatarCache.set(pionElement, CONFIG.STATUS_SUCCESS);
// Gérer l'emoji d'action
const action = ActionDetector.getPlayerAction(pionElement);
const currentAction = pionElement.getAttribute(CONFIG.ATTR_CURRENT_ACTION);
// Mettre à jour l'emoji si l'action a changé ou si force est activé
if (force || action !== currentAction) {
ActionDetector.updateActionEmoji(iconElement, action);
pionElement.setAttribute(CONFIG.ATTR_CURRENT_ACTION, action || ''); }
} catch (error) { state.avatarCache.set(pionElement, CONFIG.STATUS_FAILED);
}
},
removeCustomAvatars() {
document.querySelectorAll(`.${CONFIG.CLASS_AVATAR_IMG}`).forEach(img => img.remove());
document.querySelectorAll(`.${CONFIG.CLASS_ACTION_EMOJI}`).forEach(emoji => emoji.remove());
DOMUtils.getAllPions().forEach(pion => {
pion.removeAttribute(CONFIG.ATTR_AVATAR_STATUS);
pion.removeAttribute(CONFIG.ATTR_PLAYER_NAME);
pion.removeAttribute(CONFIG.ATTR_CURRENT_ACTION);
});
state.avatarCache.clear(); },
async applyAvatarsToAllPions(force = false) {
if (!Storage.loadAvatarEnabled()) {
this.removeCustomAvatars();
return;
}
const pions = DOMUtils.getAllPions();
Utils.debugLog(`🔍 ${pions.length} pion(s) trouvé(s)`);
// ✅ ÉTAPE 1 : Appliquer les avatars normaux à tous les pions
for (const pion of pions) {
if (!force && DOMUtils.isAvatarValid(pion)) {
// Mettre à jour uniquement la bordure
const iconElement = pion.querySelector(CONFIG.SELECTOR_ICON);
const avatarImg = iconElement?.querySelector(`.${CONFIG.CLASS_AVATAR_IMG}`);
if (avatarImg) {
const borderColor = AvatarStyler.getBorderColor(pion);
avatarImg.style.setProperty('border', `3px solid ${borderColor}`, 'important');
}
continue;
}
await this.applyCustomAvatar(pion, force);
}
// ✅ ÉTAPE 2 : Détecter et appliquer les pie charts aux pions multiples
PieChartManager.applyPieCharts();
},
reapplyAvatarsSync(force = false) {
if (!Storage.loadAvatarEnabled()) return;
// ✅ ÉTAPE 1 : Réappliquer les avatars normaux
DOMUtils.getAllPions().forEach(pion => {
if (!force && DOMUtils.isAvatarValid(pion)) {
// Mettre à jour uniquement la bordure
const iconElement = pion.querySelector(CONFIG.SELECTOR_ICON);
const avatarImg = iconElement?.querySelector(`.${CONFIG.CLASS_AVATAR_IMG}`);
if (avatarImg) {
const borderColor = AvatarStyler.getBorderColor(pion);
avatarImg.style.setProperty('border', `3px solid ${borderColor}`, 'important');
}
return;
}
this.applyCustomAvatar(pion, force);
});
// ✅ ÉTAPE 2 : Appliquer les pie charts aux pions multiples
PieChartManager.applyPieCharts();
},
// ✅ CORRECTION BUG 1 : Fonction de rafraîchissement complet
refreshAll() {
// Forcer la réapplication de tous les avatars
const pions = DOMUtils.getAllPions();
let refreshedCount = 0;
pions.forEach(pion => {
const iconElement = pion.querySelector(CONFIG.SELECTOR_ICON);
const avatarImg = iconElement?.querySelector(`.${CONFIG.CLASS_AVATAR_IMG}`);
if (avatarImg) {
// Récupérer la nouvelle couleur
const borderColor = AvatarStyler.getBorderColor(pion);
// Appliquer la couleur avec l'opacité actuelle
avatarImg.style.setProperty('border', `3px solid ${borderColor}`, 'important');
avatarImg.style.setProperty('box-shadow', `0 2px 8px ${borderColor}`, 'important');
refreshedCount++;
}
});
Utils.debugLog(`✅ ${refreshedCount} avatar(s) rafraîchi(s)`);
}
};
// ============================================================
// Système de Réapplication rapide
// ============================================================
const ReapplicationSystem = {
ultraFastReapplication() {
const now = Date.now();
if (now - state.lastReapplyTime < CONFIG.RAF_THROTTLE) {
state.reapplyAnimationFrameId = requestAnimationFrame(() => this.ultraFastReapplication());
return;
}
state.lastReapplyTime = now;
if (Storage.loadAvatarEnabled()) {
AvatarManager.reapplyAvatarsSync(false);
}
state.reapplyAnimationFrameId = requestAnimationFrame(() => this.ultraFastReapplication());
},
start() {
state.stopReapplication();
state.reapplyIntervalId = setInterval(() => {
if (Storage.loadAvatarEnabled()) {
AvatarManager.reapplyAvatarsSync(false);
}
}, CONFIG.REAPPLY_INTERVAL);
this.ultraFastReapplication();
},
stop() {
state.stopReapplication(); }
};
// ============================================================
// Systeme de taille & styles
// ============================================================
const SizingSystem = {
applyAvatarSize(size) {
const scale = size / 100;
let styleElement = document.getElementById('dreadcast-avatar-resize-style');
if (!styleElement) {
styleElement = document.createElement('style');
styleElement.id = 'dreadcast-avatar-resize-style';
document.head.appendChild(styleElement);
}
const emojiSize = Storage.loadEmojiSize();
styleElement.textContent = `
.personnages .icon_perso {
transform: scale(${scale}) !important;
transform-origin: center center !important;
}
.personnages .icon_perso .le_icon_perso {
transform: scale(1) !important;
position: relative !important;
}
.personnages .icon_perso {
z-index: auto !important;
}
.${CONFIG.CLASS_AVATAR_IMG} {
pointer-events: none !important;
width: 70px !important;
height: 70px !important;
object-fit: cover !important;
border-radius: 50% !important;
border: 2px solid rgba(255, 255, 255, 0.8) !important;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.3) !important;
position: absolute !important;
top: 50% !important;
left: 50% !important;
transform: translate(-50%, -50%) !important;
z-index: ${CONFIG.Z_INDEX_AVATAR} !important;
display: block !important;
visibility: visible !important;
opacity: 1 !important;
transition: border-color 0.3s ease !important;
animation: none !important;
}
.personnages .icon_perso .le_icon_perso > * {
position: relative !important;
}
.personnages .icon_perso .le_icon_perso > .${CONFIG.CLASS_AVATAR_IMG} {
z-index: ${CONFIG.Z_INDEX_AVATAR} !important;
}
.personnages .icon_perso .le_icon_perso > svg,
.personnages .icon_perso .le_icon_perso > use {
z-index: 1 !important;
}
.${CONFIG.CLASS_ACTION_EMOJI} {
position: absolute !important;
top: -5px !important;
right: -15px !important;
font-size: ${emojiSize}px !important;
border-radius: 50% !important;
width: ${emojiSize * 0.56}px !important;
height: ${emojiSize * 0.56}px !important;
display: flex !important;
align-items: center !important;
justify-content: center !important;
box-shadow: 0 2px 6px rgba(0, 0, 0, 0.3) !important;
z-index: ${CONFIG.Z_INDEX_EMOJI} !important;
pointer-events: none !important;
border: 2px solid rgba(0, 0, 0, 0.1) !important;
transition: opacity 0.2s ease !important;
}
`;
},
applyEmojiSize(size) {
// Cette fonction met à jour uniquement la taille des emojis
// Elle réutilise applyAvatarSize qui utilise déjà Storage.loadEmojiSize()
const currentAvatarSize = Storage.loadAvatarSize();
this.applyAvatarSize(currentAvatarSize); }
};
// ============================================================
// Composants UI
// ============================================================
const UIComponents = {
// ✅ CORRECTION BUG 3 : Amélioration du drag & drop
createDraggableBehavior(element, handle) { let pos1 = 0, pos2 = 0, pos3 = 0, pos4 = 0;
let isDragging = false;
handle.onmousedown = (e) => {
e.preventDefault();
e.stopPropagation();
isDragging = true;
// Changer le curseur
handle.style.cursor = 'grabbing';
document.body.style.userSelect = 'none';
const rect = element.getBoundingClientRect();
element.style.transform = 'none';
element.style.top = `${rect.top}px`;
element.style.left = `${rect.left}px`;
pos3 = e.clientX;
pos4 = e.clientY;
document.onmouseup = closeDrag;
document.onmousemove = elementDrag; };
function elementDrag(e) {
if (!isDragging) return;
e.preventDefault();
pos1 = pos3 - e.clientX;
pos2 = pos4 - e.clientY;
pos3 = e.clientX;
pos4 = e.clientY;
// Calculer la nouvelle position
const newTop = element.offsetTop - pos2;
const newLeft = element.offsetLeft - pos1;
// Limites de l'écran
const maxX = window.innerWidth - element.offsetWidth;
const maxY = window.innerHeight - element.offsetHeight;
element.style.top = `${Math.max(0, Math.min(newTop, maxY))}px`;
element.style.left = `${Math.max(0, Math.min(newLeft, maxX))}px`;
}
function closeDrag(e) {
if (e) {
e.stopPropagation();
}
isDragging = false;
handle.style.cursor = 'move';
document.body.style.userSelect = '';
document.onmouseup = null;
document.onmousemove = null; }
},
// ✅ CORRECTION BUG 1 : Création du panel HTML
createConfigPanel() {
const overlay = document.createElement('div');
overlay.id = 'dreadcast-avatar-config-panel';
overlay.style.cssText = `
display: none !important;
position: fixed !important;
top: 0 !important;
left: 0 !important;
width: 100% !important;
height: 100% !important;
background: rgba(0, 0, 0, 0.6) !important;
z-index: ${CONFIG.Z_INDEX_OVERLAY} !important;
`;
const panel = document.createElement('div');
panel.id = 'pmp-settings-menu';
panel.style.cssText = `
position: fixed !important;
top: 50% !important;
left: 50% !important;
transform: translate(-50%, -50%) !important;
z-index: ${CONFIG.Z_INDEX_PANEL} !important;
width: 480px !important;
max-height: 90vh !important;
background: #1a1a1a !important;
border: 1px solid #3a3a3a !important;
border-radius: 12px !important;
box-shadow: 0 8px 32px rgba(0, 0, 0, 0.5) !important;
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif !important;
color: #ffffff !important;
display: flex !important;
flex-direction: column !important;
animation: fadeIn 0.2s ease !important;
`;
panel.innerHTML = `
<style>
@keyframes fadeIn {
from { opacity: 0; transform: translate(-50%, -48%); }
to { opacity: 1; transform: translate(-50%, -50%); }
}
#pmp-settings-menu .pmp-content::-webkit-scrollbar {
width: 8px !important;
}
#pmp-settings-menu .pmp-content::-webkit-scrollbar-track {
background: transparent !important;
}
#pmp-settings-menu .pmp-content::-webkit-scrollbar-thumb {
background: #333333 !important;
border-radius: 4px !important;
}
#pmp-settings-menu .pmp-content::-webkit-scrollbar-thumb:hover {
background: #4a9eff !important;
}
#pmp-settings-menu .pmp-slider::-webkit-slider-thumb {
-webkit-appearance: none !important;
width: 18px !important;
height: 18px !important;
background: #4a9eff !important;
border-radius: 50% !important;
cursor: pointer !important;
transition: all 0.2s ease !important;
}
#pmp-settings-menu .pmp-slider::-webkit-slider-thumb:hover {
transform: scale(1.2) !important;
box-shadow: 0 0 0 4px rgba(74, 158, 255, 0.2) !important;
}
#pmp-settings-menu .pmp-slider::-moz-range-thumb {
width: 18px !important;
height: 18px !important;
background: #4a9eff !important;
border: none !important;
border-radius: 50% !important;
cursor: pointer !important;
}
@media (max-width: 600px) {
#pmp-settings-menu {
width: 90vw !important;
max-width: 400px !important;
}
#pmp-settings-menu .pmp-option {
flex-direction: column !important;
align-items: flex-start !important;
gap: 12px !important;
}
#pmp-settings-menu .pmp-slider-container {
width: 100% !important;
}
}
</style>
<!-- Header -->
<div style="display: flex !important; align-items: center !important; justify-content: space-between !important; padding: 20px 24px !important; border-bottom: 1px solid #3a3a3a !important; cursor: move !important; user-select: none !important;" class="pmp-header-drag">
<div>
<span style="margin: 0 !important; font-size: 20px !important; font-weight: 600 !important;">⚙️ PimpMyPion</span>
<span style="margin-left: 8px !important; padding: 4px 8px !important; background: #2a2a2a !important; border-radius: 6px !important; font-size: 12px !important; font-weight: 500 !important; color: #a0a0a0 !important;">v0.5.2</span>
</div>
<button id="avatar-close-btn" style="width: 32px !important; height: 32px !important; padding: 0 !important; background: transparent !important; border: none !important; border-radius: 6px !important; font-size: 20px !important; color: #a0a0a0 !important; cursor: pointer !important; transition: all 0.2s ease !important;">✕</button>
</div>
<!-- Content -->
<div class="pmp-content" style="flex: 1 !important; overflow-y: auto !important; padding: 8px !important;">
<!-- Section Affichage -->
<div style="margin-bottom: 8px !important; background: #2a2a2a !important; border-radius: 10px !important; overflow: hidden !important;">
<h3 style="margin: 0 !important; padding: 16px 20px !important; font-size: 14px !important; font-weight: 600 !important; text-transform: uppercase !important; letter-spacing: 0.5px !important; color: #a0a0a0 !important; background: #1a1a1a !important;">Affichage</h3>
<div style="padding: 8px !important;">
<div style="display: flex !important; align-items: center !important; justify-content: space-between !important; padding: 16px !important; margin-bottom: 4px !important; background: #1a1a1a !important; border-radius: 8px !important; transition: background 0.2s ease !important;">
<div style="display: flex !important; align-items: center !important; gap: 12px !important; flex: 1 !important;">
<span style="font-size: 24px !important;">🖼️</span>
<div style="display: flex !important; flex-direction: column !important; gap: 4px !important;">
<span style="font-size: 15px !important; font-weight: 500 !important;">Avatars</span>
<span style="font-size: 13px !important; color: #a0a0a0 !important;">Afficher les avatars des joueurs</span>
</div>
</div>
<label style="position: relative !important; display: inline-block !important; width: 48px !important; height: 28px !important; cursor: pointer !important;">
<input type="checkbox" id="avatar-enabled-checkbox" checked style="opacity: 0 !important; width: 0 !important; height: 0 !important;">
<span class="pmp-toggle-slider" style="position: absolute !important; top: 0 !important; left: 0 !important; right: 0 !important; bottom: 0 !important; background: #333333 !important; border-radius: 14px !important; transition: all 0.3s ease !important;"></span>
</label>
</div>
<div style="display: flex !important; align-items: center !important; justify-content: space-between !important; padding: 16px !important; background: #1a1a1a !important; border-radius: 8px !important;">
<div style="display: flex !important; align-items: center !important; gap: 12px !important; flex: 1 !important;">
<span style="font-size: 24px !important;">😀</span>
<div style="display: flex !important; flex-direction: column !important; gap: 4px !important;">
<span style="font-size: 15px !important; font-weight: 500 !important;">Emojis</span>
<span style="font-size: 13px !important; color: #a0a0a0 !important;">Afficher les icônes d'action</span>
</div>
</div>
<label style="position: relative !important; display: inline-block !important; width: 48px !important; height: 28px !important; cursor: pointer !important;">
<input type="checkbox" id="emoji-enabled-checkbox" checked style="opacity: 0 !important; width: 0 !important; height: 0 !important;">
<span class="pmp-toggle-slider" style="position: absolute !important; top: 0 !important; left: 0 !important; right: 0 !important; bottom: 0 !important; background: #333333 !important; border-radius: 14px !important; transition: all 0.3s ease !important;"></span>
</label>
</div>
</div>
</div>
<!-- Section Couleurs -->
<div style="margin-bottom: 8px !important; background: #2a2a2a !important; border-radius: 10px !important; overflow: hidden !important;">
<h3 style="margin: 0 !important; padding: 16px 20px !important; font-size: 14px !important; font-weight: 600 !important; text-transform: uppercase !important; letter-spacing: 0.5px !important; color: #a0a0a0 !important; background: #1a1a1a !important;">Couleurs</h3>
<div style="padding: 8px !important;">
<div style="display: flex !important; align-items: center !important; justify-content: space-between !important; padding: 16px !important; margin-bottom: 4px !important; background: #1a1a1a !important; border-radius: 8px !important;">
<div style="display: flex !important; align-items: center !important; gap: 12px !important; flex: 1 !important;">
<span style="font-size: 24px !important;">🟢</span>
<div style="display: flex !important; flex-direction: column !important; gap: 4px !important;">
<span style="font-size: 15px !important; font-weight: 500 !important;">Connecté</span>
<span style="font-size: 13px !important; color: #a0a0a0 !important;">Couleur des joueurs connectés</span>
</div>
</div>
<input type="color" id="color-connected" value="#4ade80" style="width: 48px !important; height: 48px !important; padding: 0 !important; border: 2px solid #3a3a3a !important; border-radius: 8px !important; cursor: pointer !important; transition: all 0.2s ease !important;">
</div>
<div style="display: flex !important; align-items: center !important; justify-content: space-between !important; padding: 16px !important; margin-bottom: 4px !important; background: #1a1a1a !important; border-radius: 8px !important;">
<div style="display: flex !important; align-items: center !important; gap: 12px !important; flex: 1 !important;">
<span style="font-size: 24px !important;">⚪</span>
<div style="display: flex !important; flex-direction: column !important; gap: 4px !important;">
<span style="font-size: 15px !important; font-weight: 500 !important;">Déconnecté</span>
<span style="font-size: 13px !important; color: #a0a0a0 !important;">Couleur des joueurs déconnectés</span>
</div>
</div>
<input type="color" id="color-disconnected" value="#ffffff" style="width: 48px !important; height: 48px !important; padding: 0 !important; border: 2px solid #3a3a3a !important; border-radius: 8px !important; cursor: pointer !important; transition: all 0.2s ease !important;">
</div>
<div style="display: flex !important; align-items: center !important; justify-content: space-between !important; padding: 16px !important; background: #1a1a1a !important; border-radius: 8px !important;">
<div style="display: flex !important; align-items: center !important; gap: 12px !important; flex: 1 !important;">
<span style="font-size: 24px !important;">👁️</span>
<div style="display: flex !important; flex-direction: column !important; gap: 4px !important;">
<span style="font-size: 15px !important; font-weight: 500 !important;">Opacité</span>
<span style="font-size: 13px !important; color: #a0a0a0 !important;">Transparence des couleurs</span>
</div>
</div>
<div style="display: flex !important; align-items: center !important; gap: 12px !important; min-width: 180px !important;">
<input type="range" id="color-opacity-slider" min="0" max="100" value="100" class="pmp-slider" style="flex: 1 !important; height: 6px !important; background: #333333 !important; border-radius: 3px !important; outline: none !important; -webkit-appearance: none !important; cursor: pointer !important;">
<span id="color-opacity-value" style="min-width: 50px !important; text-align: right !important; font-size: 14px !important; font-weight: 500 !important;">100%</span>
</div>
</div>
</div>
</div>
<!-- Section Tailles -->
<div style="margin-bottom: 8px !important; background: #2a2a2a !important; border-radius: 10px !important; overflow: hidden !important;">
<h3 style="margin: 0 !important; padding: 16px 20px !important; font-size: 14px !important; font-weight: 600 !important; text-transform: uppercase !important; letter-spacing: 0.5px !important; color: #a0a0a0 !important; background: #1a1a1a !important;">Tailles</h3>
<div style="padding: 8px !important;">
<div style="display: flex !important; align-items: center !important; justify-content: space-between !important; padding: 16px !important; margin-bottom: 4px !important; background: #1a1a1a !important; border-radius: 8px !important;">
<div style="display: flex !important; align-items: center !important; gap: 12px !important; flex: 1 !important;">
<span style="font-size: 24px !important;">📏</span>
<div style="display: flex !important; flex-direction: column !important; gap: 4px !important;">
<span style="font-size: 15px !important; font-weight: 500 !important;">Pions</span>
<span style="font-size: 13px !important; color: #a0a0a0 !important;">Taille des avatars</span>
</div>
</div>
<div style="display: flex !important; align-items: center !important; gap: 12px !important; min-width: 180px !important;">
<input type="range" id="avatar-size-slider" min="75" max="125" value="100" class="pmp-slider" style="flex: 1 !important; height: 6px !important; background: #333333 !important; border-radius: 3px !important; outline: none !important; -webkit-appearance: none !important; cursor: pointer !important;">
<span id="avatar-size-value" style="min-width: 50px !important; text-align: right !important; font-size: 14px !important; font-weight: 500 !important;">100%</span>
</div>
</div>
<div style="display: flex !important; align-items: center !important; justify-content: space-between !important; padding: 16px !important; background: #1a1a1a !important; border-radius: 8px !important;">
<div style="display: flex !important; align-items: center !important; gap: 12px !important; flex: 1 !important;">
<span style="font-size: 24px !important;">🎯</span>
<div style="display: flex !important; flex-direction: column !important; gap: 4px !important;">
<span style="font-size: 15px !important; font-weight: 500 !important;">Icônes</span>
<span style="font-size: 13px !important; color: #a0a0a0 !important;">Taille des emojis d'action</span>
</div>
</div>
<div style="display: flex !important; align-items: center !important; gap: 12px !important; min-width: 180px !important;">
<input type="range" id="emoji-size-slider" min="12" max="28" value="18" class="pmp-slider" style="flex: 1 !important; height: 6px !important; background: #333333 !important; border-radius: 3px !important; outline: none !important; -webkit-appearance: none !important; cursor: pointer !important;">
<span id="emoji-size-value" style="min-width: 50px !important; text-align: right !important; font-size: 14px !important; font-weight: 500 !important;">18px</span>
</div>
</div>
</div>
</div>
</div>
<!-- Footer -->
<div style="padding: 16px 24px !important; border-top: 1px solid #3a3a3a !important; display: flex !important; justify-content: center !important;">
<button id="avatar-reset-btn" style="padding: 10px 20px !important; border: none !important; border-radius: 8px !important; font-size: 14px !important; font-weight: 500 !important; cursor: pointer !important; transition: all 0.2s ease !important; background: #2a2a2a !important; color: #ffffff !important;">
🔄 Réinitialiser
</button>
</div>
`;
// Ajouter le CSS pour les toggles
const style = document.createElement('style');
style.textContent = `
.pmp-toggle-slider:before {
content: '' !important;
position: absolute !important;
height: 20px !important;
width: 20px !important;
left: 4px !important;
bottom: 4px !important;
background: white !important;
border-radius: 50% !important;
transition: all 0.3s ease !important;
}
input:checked + .pmp-toggle-slider {
background: #4a9eff !important;
}
input:checked + .pmp-toggle-slider:before {
transform: translateX(20px) !important;
}
#avatar-close-btn:hover {
background: #333333 !important;
color: #ffffff !important;
}
#avatar-reset-btn:hover {
background: #333333 !important;
transform: translateY(-1px) !important;
}
`;
document.head.appendChild(style);
overlay.appendChild(panel);
// Fermer en cliquant sur l'overlay
overlay.addEventListener('click', (e) => {
if (e.target === overlay) {
overlay.style.display = 'none';
}
});
document.body.appendChild(overlay);
// Attacher les événements
setTimeout(() => {
this.attachPanelEvents();
}, CONFIG.EVENT_ATTACH_DELAY);
return overlay;
},
// ✅ CORRECTION BUG 2 : Fonction pour attacher les event listeners
attachPanelEvents() {
const panel = document.getElementById('dreadcast-avatar-config-panel');
if (!panel) { return;
}
// Récupérer tous les éléments
const menuPanel = document.getElementById('pmp-settings-menu');
const headDiv = panel.querySelector('.pmp-header-drag');
const closeBtn = document.getElementById('avatar-close-btn');
const slider = document.getElementById('avatar-size-slider');
const valueDisplay = document.getElementById('avatar-size-value');
const resetBtn = document.getElementById('avatar-reset-btn');
const avatarCheckbox = document.getElementById('avatar-enabled-checkbox');
const emojiCheckbox = document.getElementById('emoji-enabled-checkbox');
const emojiSizeSlider = document.getElementById('emoji-size-slider');
const emojiSizeValue = document.getElementById('emoji-size-value');
const colorOpacitySlider = document.getElementById('color-opacity-slider');
const colorOpacityValue = document.getElementById('color-opacity-value');
const colorConnected = document.getElementById('color-connected');
const colorDisconnected = document.getElementById('color-disconnected');
// ===== DRAG & DROP =====
if (menuPanel && headDiv) {
this.createDraggableBehavior(menuPanel, headDiv);
}
// ===== BOUTON FERMER =====
if (closeBtn) {
closeBtn.addEventListener('click', () => {
panel.style.display = 'none';
Utils.debugLog('🔧 [DEBUG] Panel fermé (bouton X)');
}); } else { }
// ===== CHECKBOX AVATARS =====
if (avatarCheckbox) {
avatarCheckbox.checked = Storage.loadAvatarEnabled();
avatarCheckbox.addEventListener('change', () => {
const enabled = avatarCheckbox.checked;
Storage.saveAvatarEnabled(enabled);
if (enabled) {
AvatarManager.applyAvatarsToAllPions(true);
ReapplicationSystem.start();
} else {
AvatarManager.removeCustomAvatars();
ReapplicationSystem.stop();
} }); } else { }
// ===== CHECKBOX EMOJIS =====
if (emojiCheckbox) {
emojiCheckbox.checked = Storage.loadEmojiEnabled();
emojiCheckbox.addEventListener('change', () => {
const enabled = emojiCheckbox.checked;
Storage.saveEmojiEnabled(enabled);
AvatarManager.applyAvatarsToAllPions(true); }); } else { }
// ===== SLIDER TAILLE PIONS =====
if (slider && valueDisplay) {
slider.value = Storage.loadAvatarSize();
valueDisplay.textContent = `${slider.value}%`;
slider.addEventListener('input', () => {
const size = parseInt(slider.value, 10);
valueDisplay.textContent = `${size}%`;
if (!CombatDetector.isInCombat) {
Storage.saveAvatarSize(size);
SizingSystem.applyAvatarSize(size);
} }); } else { }
// ===== SLIDER TAILLE EMOJIS =====
if (emojiSizeSlider && emojiSizeValue) {
emojiSizeSlider.value = Storage.loadEmojiSize();
emojiSizeValue.textContent = `${emojiSizeSlider.value}px`;
emojiSizeSlider.addEventListener('input', () => {
const size = parseInt(emojiSizeSlider.value, 10);
emojiSizeValue.textContent = `${size}px`;
Storage.saveEmojiSize(size);
SizingSystem.applyEmojiSize(size); }); } else { }
// ===== SLIDER OPACITÉ =====
if (colorOpacitySlider && colorOpacityValue) {
colorOpacitySlider.value = Storage.loadColorOpacity();
colorOpacityValue.textContent = `${colorOpacitySlider.value}%`;
colorOpacitySlider.addEventListener('input', () => {
const opacity = parseInt(colorOpacitySlider.value, 10);
colorOpacityValue.textContent = `${opacity}%`;
Storage.saveColorOpacity(opacity);
// Rafraîchir tous les avatars
AvatarManager.refreshAll(); }); } else { }
// ===== COLOR PICKERS =====
if (colorConnected) {
const customColors = Storage.loadCustomColors();
colorConnected.value = customColors['connected'] || CONFIG.COLOR_CONNECTED;
colorConnected.addEventListener('change', () => {
const customColors = Storage.loadCustomColors();
customColors['connected'] = colorConnected.value;
Storage.saveCustomColors(customColors);
AvatarManager.refreshAll(); }); } else { }
if (colorDisconnected) {
const customColors = Storage.loadCustomColors();
colorDisconnected.value = customColors['disconnected'] || CONFIG.COLOR_DISCONNECTED;
colorDisconnected.addEventListener('change', () => {
const customColors = Storage.loadCustomColors();
customColors['disconnected'] = colorDisconnected.value;
Storage.saveCustomColors(customColors);
AvatarManager.refreshAll(); }); } else { }
// ===== BOUTON RESET =====
if (resetBtn) {
resetBtn.addEventListener('click', () => {
// Réinitialiser les valeurs
const defaultSize = CONFIG.DEFAULT_SIZE;
const defaultEmojiSize = CONFIG.DEFAULT_EMOJI_SIZE;
const defaultOpacity = CONFIG.DEFAULT_COLOR_OPACITY;
Storage.saveAvatarSize(defaultSize);
Storage.saveEmojiSize(defaultEmojiSize);
Storage.saveColorOpacity(defaultOpacity);
Storage.saveCustomColors({});
// Mettre à jour l'interface
if (slider && valueDisplay) {
slider.value = defaultSize;
valueDisplay.textContent = `${defaultSize}%`;
}
if (emojiSizeSlider && emojiSizeValue) {
emojiSizeSlider.value = defaultEmojiSize;
emojiSizeValue.textContent = `${defaultEmojiSize}px`;
}
if (colorOpacitySlider && colorOpacityValue) {
colorOpacitySlider.value = defaultOpacity;
colorOpacityValue.textContent = `${defaultOpacity}%`;
}
if (colorConnected) {
colorConnected.value = CONFIG.COLOR_CONNECTED;
}
if (colorDisconnected) {
colorDisconnected.value = CONFIG.COLOR_DISCONNECTED;
}
// Appliquer les changements
SizingSystem.applyAvatarSize(defaultSize);
SizingSystem.applyEmojiSize(defaultEmojiSize);
AvatarManager.refreshAll(); }); } else { } },
openConfigPanel() {
let panel = document.getElementById('dreadcast-avatar-config-panel');
if (!panel) { panel = this.createConfigPanel(); }
if (panel) {
panel.style.display = 'block'; } else { }
}
};
// ============================================================
// Intégration du menu
// ============================================================
const MenuIntegration = {
addMenuOption() {
const checkMenu = setInterval(() => {
const parametresMenu = document.querySelector(CONFIG.SELECTOR_SETTINGS_MENU);
if (parametresMenu) {
clearInterval(checkMenu);
if (document.getElementById('avatar-resize-menu-option')) { return;
}
const menuOption = document.createElement('li');
menuOption.id = 'avatar-resize-menu-option';
menuOption.className = 'link couleur2';
menuOption.textContent = '🥧 PmP v0.5.2';
menuOption.style.cursor = 'pointer';
menuOption.addEventListener('click', (e) => {
e.preventDefault();
e.stopPropagation(); UIComponents.openConfigPanel();
}, true);
const lastSeparator = parametresMenu.querySelector('.separator:last-of-type');
if (lastSeparator) {
parametresMenu.insertBefore(menuOption, lastSeparator); } else {
parametresMenu.appendChild(menuOption); } }
}, CONFIG.MENU_CHECK_INTERVAL);
setTimeout(() => {
clearInterval(checkMenu); }, CONFIG.MENU_CHECK_TIMEOUT);
}
};
// ============================================================
// DOM OBSERVER
// ============================================================
const DOMObserver = {
observe() {
const targetNode = document.querySelector('.personnages');
if (!targetNode) return;
const observer = new MutationObserver((mutations) => {
const currentSize = Storage.loadAvatarSize();
if (CombatDetector.isInCombat) {
SizingSystem.applyAvatarSize(100);
} else {
SizingSystem.applyAvatarSize(currentSize);
}
mutations.forEach(mutation => {
mutation.addedNodes.forEach(node => {
if (node.nodeType === 1 && node.classList?.contains('icon_perso')) { AvatarManager.applyCustomAvatar(node, true);
}
});
if (mutation.type === 'childList' || mutation.type === 'attributes') {
const target = mutation.target;
const pionElement = target.classList?.contains('icon_perso')
? target
: target.closest('.icon_perso');
if (pionElement && !DOMUtils.isAvatarValid(pionElement)) { AvatarManager.applyCustomAvatar(pionElement, true);
}
}
});
});
observer.observe(targetNode, {
childList: true,
subtree: true,
attributes: true,
attributeFilter: ['class'],
attributeOldValue: true,
characterData: true,
characterDataOldValue: true
}); }
};
// ============================================================
// Initialisation
// ============================================================
function init() {
const savedSize = Storage.loadAvatarSize();
const savedEmojiSize = Storage.loadEmojiSize();
SizingSystem.applyAvatarSize(savedSize);
SizingSystem.applyEmojiSize(savedEmojiSize);
MenuIntegration.addMenuOption();
DOMObserver.observe();
CombatDetector.start();
setTimeout(() => { AvatarManager.applyAvatarsToAllPions(true);
if (Storage.loadAvatarEnabled()) {
ReapplicationSystem.start();
}
}, CONFIG.INIT_DELAY);
setTimeout(() => { AvatarManager.applyAvatarsToAllPions(true);
}, CONFIG.SECONDARY_DELAY);
}
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', init);
} else {
init();
}
})();