Dreadcast - PimpMyPion

Ajoute un slider pour contrôler la taille des pions + affiche les avatars personnalisés des joueurs

您需要先安裝使用者腳本管理器擴展,如 TampermonkeyGreasemonkeyViolentmonkey 之後才能安裝該腳本。

您需要先安裝使用者腳本管理器擴充功能,如 TampermonkeyViolentmonkey 後才能安裝該腳本。

您需要先安裝使用者腳本管理器擴充功能,如 TampermonkeyViolentmonkey 後才能安裝該腳本。

您需要先安裝使用者腳本管理器擴充功能,如 TampermonkeyUserscripts 後才能安裝該腳本。

你需要先安裝一款使用者腳本管理器擴展,比如 Tampermonkey,才能安裝此腳本

您需要先安裝使用者腳本管理器擴充功能後才能安裝該腳本。

(我已經安裝了使用者腳本管理器,讓我安裝!)

你需要先安裝一款使用者樣式管理器擴展,比如 Stylus,才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展,比如 Stylus,才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展,比如 Stylus,才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展後才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展後才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展後才能安裝此樣式

(我已經安裝了使用者樣式管理器,讓我安裝!)

// ==UserScript==
// @name         Dreadcast - PimpMyPion
// @namespace    http://tampermonkey.net/
// @version      0.4.4
// @description  Ajoute un slider pour contrôler la taille des pions + affiche les avatars personnalisés des joueurs
// @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',

    // Paramètres de taille(en %)
    DEFAULT_SIZE: 50,
    MIN_SIZE: 50,
    MAX_SIZE: 200,

    // URLs & chemins
    AVATAR_BASE_URL: 'https://www.dreadcast.net/images/avatars/',

    // Timing (millisecondes)
    REAPPLY_INTERVAL: 50,
    RAF_THROTTLE: 0, // 16 = ~60 FPS ; 0 = no limit (trop gourmand en CPU? voir retours des gens)
    INIT_DELAY: 2000,
    SECONDARY_DELAY: 5000,
    MENU_CHECK_INTERVAL: 500,
    MENU_CHECK_TIMEOUT: 10000,
    EVENT_ATTACH_DELAY: 100,

    // Z-indices
    Z_INDEX_AVATAR: 999,
    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',

    // Classes CSS
    CLASS_AVATAR_IMG: 'custom-avatar-img',
    CLASS_CONNECTED: 'connecte',

    // Data attributes
    ATTR_AVATAR_STATUS: 'data-avatar-applied',
    ATTR_PLAYER_NAME: 'data-player-name',

    // Status avatars
    STATUS_SUCCESS: 'success',
    STATUS_FAILED: 'failed',

    // Couleurs
    COLOR_CONNECTED: '#4ade80',
    COLOR_DISCONNECTED: '#9ca3af',

    // Divers
    DEBUG_MODE: false
  });

  // ============================================================
  // Gestion des état
  // ============================================================
  class AvatarState {
    constructor() {
      this.avatarCache = new Map();
      this.avatarUrlCache = new Map();
      this.reapplyIntervalId = null;
      this.reapplyAnimationFrameId = null;
      this.lastReapplyTime = 0;
    }

    clearCaches() {
      this.avatarCache.clear();
      this.avatarUrlCache.clear();
    }

    stopReapplication() {
      if (this.reapplyIntervalId) {
        clearInterval(this.reapplyIntervalId);
        this.reapplyIntervalId = null;
      }
      if (this.reapplyAnimationFrameId) {
        cancelAnimationFrame(this.reapplyAnimationFrameId);
        this.reapplyAnimationFrameId = null;
      }
    }
  }

  const state = new AvatarState();

  // ============================================================
  // 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);
      };
    }
  };

  // ============================================================
  // 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));
      Utils.debugLog('✅ Taille sauvegardée:', `${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));
      Utils.debugLog('✅ Affichage des avatars:', enabled ? 'activé' : 'désactivé');
    }
  };

  // ============================================================
  // 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) {
        Utils.debugLog('❌ Erreur extraction nom joueur:', 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);
    }
  };

  // ============================================================
  // 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: '40px',
        height: '40px',
        '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: 'none',
        animation: 'none'
      };

      Object.entries(styles).forEach(([property, value]) => {
        avatarImg.style.setProperty(property, value, 'important');
      });
    },

    getBorderColor(iconElement) {
      return iconElement.classList.contains(CONFIG.CLASS_CONNECTED)
        ? CONFIG.COLOR_CONNECTED
        : CONFIG.COLOR_DISCONNECTED;
    }
  };

  // ============================================================
  // 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)) {
          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(iconElement);
        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);

      } catch (error) {
        Utils.debugLog('❌ Erreur application avatar:', error);
        state.avatarCache.set(pionElement, CONFIG.STATUS_FAILED);
      }
    },

    removeCustomAvatars() {
      document.querySelectorAll(`.${CONFIG.CLASS_AVATAR_IMG}`).forEach(img => img.remove());

      DOMUtils.getAllPions().forEach(pion => {
        pion.removeAttribute(CONFIG.ATTR_AVATAR_STATUS);
        pion.removeAttribute(CONFIG.ATTR_PLAYER_NAME);
      });

      state.avatarCache.clear();
      Utils.debugLog('✅ Avatars supprimés');
    },

    async applyAvatarsToAllPions(force = false) {
      if (!Storage.loadAvatarEnabled()) {
        this.removeCustomAvatars();
        return;
      }

      const pions = DOMUtils.getAllPions();
      Utils.debugLog(`🔍 ${pions.length} pion(s) trouvé(s)`);

      for (const pion of pions) {
        if (!force && DOMUtils.isAvatarValid(pion)) continue;
        await this.applyCustomAvatar(pion, force);
      }
    },

    reapplyAvatarsSync(force = false) {
      if (!Storage.loadAvatarEnabled()) return;

      DOMUtils.getAllPions().forEach(pion => {
        if (!force && DOMUtils.isAvatarValid(pion)) return;
        this.applyCustomAvatar(pion, force);
      });
    }
  };

  // ============================================================
  // Système de Réapplication rapide (réapplique les avatar quand DOM change -quand on bouge en jeu par exemple-)
  // Version rapide --> 60FPS, pour éviter au max de voir le "clignotement" des pions)
  // ============================================================
  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();
      Utils.debugLog('🔄 Démarrage système de réapplication');

      state.reapplyIntervalId = setInterval(() => {
        if (Storage.loadAvatarEnabled()) {
          AvatarManager.reapplyAvatarsSync(false);
        }
      }, CONFIG.REAPPLY_INTERVAL);

      this.ultraFastReapplication();
    },

    stop() {
      state.stopReapplication();
      Utils.debugLog('⏹️ Arrêt système de réapplication');
    }
  };

  // ============================================================
  // 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);
      }

      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: none !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;
        }
      `;
    }
  };

  // ============================================================
  // Composants UI
  // ============================================================
  const UIComponents = {
    createDraggableBehavior(element, handle) {
      let pos1 = 0, pos2 = 0, pos3 = 0, pos4 = 0;
      let isDragging = false;

      handle.onmousedown = (e) => {
        e.preventDefault();
        e.stopPropagation();

        isDragging = true;

        // Désactiver le transform et fixer les positions AVANT de commencer le drag
        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) {
        e.preventDefault();
        pos1 = pos3 - e.clientX;
        pos2 = pos4 - e.clientY;
        pos3 = e.clientX;
        pos4 = e.clientY;
        element.style.top = `${element.offsetTop - pos2}px`;
        element.style.left = `${element.offsetLeft - pos1}px`;
      }

      function closeDrag(e) {
        // Empêcher la propagation du mouseup vers l'overlay
        if (e) {
          e.stopPropagation();
        }
        isDragging = false;
        document.onmouseup = null;
        document.onmousemove = null;
      }
    },

    attachPanelEvents() {
      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 panel = document.getElementById('dreadcast-avatar-config-panel');

      if (!closeBtn || !slider || !valueDisplay || !resetBtn || !avatarCheckbox) {
        Utils.debugLog('❌ Éléments du panneau introuvables');
        return;
      }

      const currentSize = Storage.loadAvatarSize();
      const avatarsEnabled = Storage.loadAvatarEnabled();

      slider.value = currentSize;
      valueDisplay.textContent = `${currentSize}%`;
      avatarCheckbox.checked = avatarsEnabled;

      closeBtn.addEventListener('click', (e) => {
        e.stopPropagation();
        panel.style.display = 'none';
      });

      slider.addEventListener('input', function () {
        const size = parseInt(this.value, 10);
        valueDisplay.textContent = `${size}%`;
        SizingSystem.applyAvatarSize(size);
        Storage.saveAvatarSize(size);
      });

      avatarCheckbox.addEventListener('change', function () {
        const enabled = this.checked;
        Storage.saveAvatarEnabled(enabled);

        if (enabled) {
          Utils.debugLog('🖼️ Activation des avatars...');
          ReapplicationSystem.start();
          AvatarManager.applyAvatarsToAllPions(true);
        } else {
          Utils.debugLog('🚫 Désactivation des avatars...');
          ReapplicationSystem.stop();
          AvatarManager.removeCustomAvatars();
        }
      });

      resetBtn.addEventListener('click', () => {
        slider.value = CONFIG.DEFAULT_SIZE;
        valueDisplay.textContent = `${CONFIG.DEFAULT_SIZE}%`;
        avatarCheckbox.checked = true;

        SizingSystem.applyAvatarSize(CONFIG.DEFAULT_SIZE);
        Storage.saveAvatarSize(CONFIG.DEFAULT_SIZE);
        Storage.saveAvatarEnabled(true);

        state.clearCaches();
        ReapplicationSystem.start();
        AvatarManager.applyAvatarsToAllPions(true);
      });

      const dataBox = panel.querySelector('.dataBox');
      const head = panel.querySelector('.head');
      if (dataBox && head) {
        this.createDraggableBehavior(dataBox, head);
      }
    },

    createConfigPanel() {
      let panel = document.getElementById('dreadcast-avatar-config-panel');
      if (panel) return panel;

      panel = document.createElement('div');
      panel.id = 'dreadcast-avatar-config-panel';
      panel.style.cssText = `
        position: fixed !important;
        top: 0 !important;
        left: 0 !important;
        width: 100% !important;
        height: 100% !important;
        background-color: rgba(0, 0, 0, 0.7) !important;
        z-index: ${CONFIG.Z_INDEX_OVERLAY} !important;
        display: none !important;
      `;

      panel.innerHTML = `
        <div class="dataBox" style="position: fixed !important; top: 50% !important; left: 50% !important; transform: translate(-50%, -50%) !important; z-index: ${CONFIG.Z_INDEX_PANEL} !important; width: 500px !important; background: #ffffff !important; border-radius: 12px !important; box-shadow: 0 8px 32px rgba(0, 0, 0, 0.3) !important;">
          <relative>
            <div class="head" style="background: linear-gradient(135deg, #667eea 0%, #764ba2 100%) !important; padding: 20px !important; cursor: move !important; position: relative !important;">
              <div class="title" style="color: #ffffff !important; font-size: 18px !important; font-weight: 600 !important; text-align: center !important;">🎀 PmP</div>
              <div title="Fermer" class="close" id="avatar-close-btn" style="position: absolute !important; top: 15px !important; right: 15px !important; color: #ffffff !important; cursor: pointer !important; font-size: 24px !important; background: rgba(255, 255, 255, 0.2) !important; border-radius: 6px !important; width: 32px !important; height: 32px !important; display: flex !important; align-items: center !important; justify-content: center !important;">✕</div>
            </div>
            <div class="content" style="padding: 30px !important; background: #ffffff !important;">
              <div style="margin-bottom: 25px !important; padding: 18px !important; background: linear-gradient(135deg, #f6f8fb 0%, #eef2f7 100%) !important; border-left: 4px solid #667eea !important; border-radius: 8px !important;">
                <label style="display: flex !important; align-items: center !important; cursor: pointer !important;">
                  <input type="checkbox" id="avatar-enabled-checkbox" style="width: 20px !important; height: 20px !important; margin-right: 12px !important; cursor: pointer !important; accent-color: #667eea !important;">
                  <span style="font-size: 15px !important; color: #2d3748 !important; font-weight: 500 !important;">🖼️ Afficher les avatars des joueurs</span>
                </label>
                <div style="margin-top: 10px !important; font-size: 12px !important; color: #718096 !important; padding-left: 32px !important;">Remplace l'icône par défaut par l'avatar réel de chaque joueur</div>
              </div>

              <div style="margin-bottom: 25px !important;">
                <label style="display: block !important; margin-bottom: 15px !important; font-size: 15px !important; color: #2d3748 !important; font-weight: 500 !important;">
                  📏 Taille des pions : <span id="avatar-size-value" style="color: #667eea !important; font-weight: 700 !important;">100%</span>
                </label>
                <input type="range" id="avatar-size-slider" min="${CONFIG.MIN_SIZE}" max="${CONFIG.MAX_SIZE}" value="100" style="width: 100% !important; height: 6px !important; background: linear-gradient(to right, #e2e8f0, #667eea) !important; border-radius: 10px !important; cursor: pointer !important;">
                <style>
                  #avatar-size-slider::-webkit-slider-thumb {
                    -webkit-appearance: none !important;
                    width: 20px !important;
                    height: 20px !important;
                    background: linear-gradient(135deg, #667eea 0%, #764ba2 100%) !important;
                    border-radius: 50% !important;
                    cursor: pointer !important;
                    box-shadow: 0 2px 6px rgba(102, 126, 234, 0.4) !important;
                  }
                  #avatar-size-slider::-webkit-slider-thumb:hover {
                    transform: scale(1.2) !important;
                  }
                  #avatar-size-slider::-moz-range-thumb {
                    width: 20px !important;
                    height: 20px !important;
                    background: linear-gradient(135deg, #667eea 0%, #764ba2 100%) !important;
                    border: none !important;
                    border-radius: 50% !important;
                    cursor: pointer !important;
                  }
                  #avatar-close-btn:hover {
                    background: rgba(255, 255, 255, 0.3) !important;
                    transform: rotate(90deg) !important;
                  }
                  #avatar-reset-btn:hover {
                    transform: translateY(-2px) !important;
                    box-shadow: 0 6px 20px rgba(102, 126, 234, 0.4) !important;
                  }
                </style>
              </div>

              <div style="text-align: center !important;">
                <button id="avatar-reset-btn" style="padding: 12px 30px !important; background: linear-gradient(135deg, #667eea 0%, #764ba2 100%) !important; color: #ffffff !important; border: none !important; border-radius: 8px !important; cursor: pointer !important; font-size: 14px !important; font-weight: 600 !important;">🔄 Réinitialiser</button>
              </div>
            </div>
          </relative>
        </div>
      `;

      panel.addEventListener('click', (e) => {
        if (e.target === panel) {
          panel.style.display = 'none';
        }
      });

      const appendPanel = () => {
        if (document.body) {
          document.body.appendChild(panel);
          setTimeout(() => this.attachPanelEvents(), CONFIG.EVENT_ATTACH_DELAY);
        } else {
          setTimeout(appendPanel, CONFIG.MENU_CHECK_INTERVAL);
        }
      };

      appendPanel();
      return panel;
    },

    openConfigPanel() {
      let panel = document.getElementById('dreadcast-avatar-config-panel');
      if (!panel) {
        panel = this.createConfigPanel();
      }
      if (panel) {
        panel.style.display = 'block';
        Utils.debugLog('✅ Panneau ouvert');
      }
    }
  };

  // ============================================================
  // 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.4.4';
          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);
          }

          Utils.debugLog('✅ Option menu ajoutée');
        }
      }, 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) => {
        Utils.debugLog('🔍 Changement DOM détecté:', mutations.length);

        const currentSize = Storage.loadAvatarSize();
        SizingSystem.applyAvatarSize(currentSize);

        mutations.forEach(mutation => {
          mutation.addedNodes.forEach(node => {
            if (node.nodeType === 1 && node.classList?.contains('icon_perso')) {
              Utils.debugLog('🆕 Nouveau pion détecté');
              AvatarManager.applyCustomAvatar(node, true);
            }
          });

          if (mutation.type === 'childList') {
            const target = mutation.target;
            const pionElement = target.classList?.contains('icon_perso')
              ? target
              : target.closest('.icon_perso');

            if (pionElement && !DOMUtils.isAvatarValid(pionElement)) {
              Utils.debugLog('⚠️ Pion modifié, réapplication...');
              AvatarManager.applyCustomAvatar(pionElement, true);
            }
          }
        });
      });

      observer.observe(targetNode, {
        childList: true,
        subtree: true,
        attributes: true,
        attributeOldValue: true,
        characterData: true,
        characterDataOldValue: true
      });

      Utils.debugLog('✅ MutationObserver activé');
    }
  };

  // ============================================================
  // Initialisation
  // ============================================================
  function init() {
    Utils.debugLog('⚡ PimpMyPion - Initialisation');

    const savedSize = Storage.loadAvatarSize();
    SizingSystem.applyAvatarSize(savedSize);
    Utils.debugLog('✅ Taille appliquée:', `${savedSize}%`);

    MenuIntegration.addMenuOption();
    DOMObserver.observe();

    setTimeout(() => {
      Utils.debugLog('🖼️ Application initiale des avatars');
      AvatarManager.applyAvatarsToAllPions(true);

      if (Storage.loadAvatarEnabled()) {
        ReapplicationSystem.start();
      }
    }, CONFIG.INIT_DELAY);

    setTimeout(() => {
      Utils.debugLog('🔄 Réapplication de sécurité');
      AvatarManager.applyAvatarsToAllPions(true);
    }, CONFIG.SECONDARY_DELAY);
  }

  // Démarrer initialization
  if (document.readyState === 'loading') {
    document.addEventListener('DOMContentLoaded', init);
  } else {
    init();
  }

})();