Bricol'tool

Vue : recherche monstres/lieux | Possessions : tableau agrégé avec stats décomposées

Na nainštalovanie skriptu si budete musieť nainštalovať rozšírenie, ako napríklad Tampermonkey, Greasemonkey alebo Violentmonkey.

Na inštaláciu tohto skriptu je potrebné nainštalovať rozšírenie, ako napríklad Tampermonkey.

Na nainštalovanie skriptu si budete musieť nainštalovať rozšírenie, ako napríklad Tampermonkey, % alebo Violentmonkey.

Na nainštalovanie skriptu si budete musieť nainštalovať rozšírenie, ako napríklad Tampermonkey alebo Userscripts.

Na inštaláciu tohto skriptu je potrebné nainštalovať rozšírenie, ako napríklad Tampermonkey.

Na inštaláciu tohto skriptu je potrebné nainštalovať rozšírenie správcu používateľských skriptov.

(Už mám správcu používateľských skriptov, nechajte ma ho nainštalovať!)

Advertisement:

Na inštaláciu tohto štýlu je potrebné nainštalovať rozšírenie, ako napríklad Stylus.

Na inštaláciu tohto štýlu je potrebné nainštalovať rozšírenie, ako napríklad Stylus.

Na inštaláciu tohto štýlu je potrebné nainštalovať rozšírenie, ako napríklad Stylus.

Na inštaláciu tohto štýlu je potrebné nainštalovať rozšírenie správcu používateľských štýlov.

Na inštaláciu tohto štýlu je potrebné nainštalovať rozšírenie správcu používateľských štýlov.

Na inštaláciu tohto štýlu je potrebné nainštalovať rozšírenie správcu používateľských štýlov.

(Už mám správcu používateľských štýlov, nechajte ma ho nainštalovať!)

Advertisement:

// ==UserScript==
// @name         Bricol'tool
// @namespace    mountyhall-tools
// @version      3.9
// @description  Vue : recherche monstres/lieux | Possessions : tableau agrégé avec stats décomposées
// @match        http://trolls.ratibus.net/*/vue.php*
// @match        https://it.mh.raistlin.fr/mountyhall/vue.php*
// @match        http://trolls.ratibus.net/*/possessions.php*
// @author       Cageux avec ClaudeIA
// @grant        none
// ==/UserScript==

(function () {
  'use strict';

  // ══════════════════════════════════════════════════════════════
  // COMMUN — utilitaires
  // ══════════════════════════════════════════════════════════════

  function escHtml(str) {
    return String(str).replace(/&/g,'&amp;').replace(/</g,'&lt;').replace(/>/g,'&gt;');
  }

  function makeDraggable(el, handle) {
    let mx = 0, my = 0;
    handle.addEventListener('mousedown', e => {
      if (e.target.dataset.nodrag) return;
      e.preventDefault();
      // Ancrer la position courante en left/top avant tout mouvement
      // (le panneau peut être positionné via right: initialement)
      const r = el.getBoundingClientRect();
      el.style.left  = r.left + 'px';
      el.style.top   = r.top  + 'px';
      el.style.right = 'auto';
      mx = e.clientX; my = e.clientY;
      document.addEventListener('mousemove', drag);
      document.addEventListener('mouseup', stop);
    });
    function drag(e) {
      const dx = e.clientX - mx;
      const dy = e.clientY - my;
      mx = e.clientX; my = e.clientY;
      el.style.left = (el.offsetLeft + dx) + 'px';
      el.style.top  = (el.offsetTop  + dy) + 'px';
    }
    function stop() {
      document.removeEventListener('mousemove', drag);
      document.removeEventListener('mouseup', stop);
    }
  }

  function makeResizable(el) {
    const MIN_W = 400, MIN_H = 200;
    let resizing = false, startX, startY, startW, startH, edge;
    el.addEventListener('mousemove', e => {
      if (resizing) return;
      const r = el.getBoundingClientRect();
      const onR = e.clientX > r.right  - 8 && e.clientX <= r.right;
      const onB = e.clientY > r.bottom - 8 && e.clientY <= r.bottom;
      el.style.cursor = (onR && onB) ? 'nwse-resize' : onR ? 'ew-resize' : onB ? 'ns-resize' : '';
    });
    el.addEventListener('mousedown', e => {
      const r = el.getBoundingClientRect();
      const onR = e.clientX > r.right  - 8 && e.clientX <= r.right;
      const onB = e.clientY > r.bottom - 8 && e.clientY <= r.bottom;
      if (!onR && !onB) return;
      resizing = true;
      edge = (onR && onB) ? 'corner' : onR ? 'right' : 'bottom';
      startX = e.clientX; startY = e.clientY;
      startW = el.offsetWidth; startH = el.offsetHeight;
      e.preventDefault();
      document.addEventListener('mousemove', onMove);
      document.addEventListener('mouseup', onUp);
    });
    function onMove(e) {
      if (!resizing) return;
      if (edge === 'right'  || edge === 'corner') el.style.width     = Math.max(MIN_W, startW + e.clientX - startX) + 'px';
      if (edge === 'bottom' || edge === 'corner') el.style.maxHeight = Math.max(MIN_H, startH + e.clientY - startY) + 'px';
    }
    function onUp() {
      resizing = false; el.style.cursor = '';
      document.removeEventListener('mousemove', onMove);
      document.removeEventListener('mouseup', onUp);
    }
  }


  // Redimensionnement des colonnes par glisser le bord droit du <th>
  function makeColumnsResizable(table) {
    table.querySelectorAll('thead tr:last-child th, thead th[rowspan]').forEach(th => {
      const handle = document.createElement('div');
      handle.style.cssText = `position:absolute;right:0;top:0;bottom:0;width:5px;cursor:col-resize;z-index:2;`;
      handle.dataset.nodrag = '1';
      th.style.position = 'relative';
      th.appendChild(handle);

      let startX, startW;
      handle.addEventListener('mousedown', e => {
        e.stopPropagation();
        e.preventDefault();
        startX = e.clientX;
        startW = th.offsetWidth;
        document.addEventListener('mousemove', onMove);
        document.addEventListener('mouseup', onUp);
      });
      function onMove(e) {
        const newW = Math.max(30, startW + e.clientX - startX);
        th.style.width = newW + 'px';
        th.style.minWidth = newW + 'px';
        th.style.maxWidth = newW + 'px';
      }
      function onUp() {
        document.removeEventListener('mousemove', onMove);
        document.removeEventListener('mouseup', onUp);
      }
    });
  }

  // ══════════════════════════════════════════════════════════════
  // MODULE VUE — monstres + lieux
  // ══════════════════════════════════════════════════════════════

  const FAMILLES = {
    'Insecte': [
      'Ankheg','Anoploure Purpurin','Araignée Géante','Aragnarok du Chaos',
      'Coccicruelle','Essaim Cratérien','Essaim Sanguinaire','Foudroyeur',
      'Labeilleux','Limace Géante','Mante Fulcreuse','Mille-Pattes Géant',
      "Nuage d'Insectes","Nuée de Vermine",'Pititabeille','Scarabée Géant',
      'Scorpion Géant','Strige','Thri-kreen',
    ],
    'Humanoïde': [
      'Ashashin','Boggart','Caillouteux','Champi-Glouton','Ettin',
      'Flagelleur Mental','Furgolin','Géant de Pierre','Géant des Gouffres',
      'Gnoll','Goblin','Goblours',"Golem d'Argile",'Golem de Chair',
      'Golem de Fer','Golem de Pierre','Golem de Cuir','Golem de Métal','Golem de Mithril',
      'Golem de Papier','Gremlins','Homme-Lézard','Hurleur',
      'Kobold','Loup-Garou','Lutin','Méduse','Mégacéphale','Minotaure',
      'Ogre','Orque','Ours-Garou','Raquettou','Rat-Garou','Rocketeux',
      'Sorcière','Sphinx','Tigre-Garou','Titan','Veskan du Chaos','Yéti','Yuan-ti',
    ],
    'Monstre': [
      'Amibe Géante','Anaconda des Catacombes','Basilisk','Behir','Bondin',
      "Bouj'Dla","Bouj'Dla Placide",'Bulette','Carnosaure','Chimère',
      'Chonchon','Cockatrice','Crasc','Crasc Médius','Crasc Maexus',
      'Cube Gélatineux','Djinn','Effrit','Esprit-Follet','Familier',
      'Feu Follet','Fungus Géant','Fungus Violet','Gargouille','Gorgone',
      'Grouilleux','Grylle','Harpie','Hydre','Lézard Géant','Manticore','Mimique',
      'Monstre Rouilleur',"Mouch'oo","Mouch'oo Majestueux",'Naga',
      'Ombre de Roches','Phoenix','Plante Carnivore','Poulpe-Garou','Slaad',
      'Tertre Errant','Trancheur','Tutoki','Ver Carnivore Géant','Ver Carnivore','Vouivre','Worg',
    ],
    'Animal': [
      'Chauve-Souris Géante','Cheval à Dents de Sabre','Dindon',"Geck'oo",
      "Geck'oo majestueux",'Glouton','Gnu','Gowap','Lapin Blanc',
      'Rat Géant','Sagouin','Tubercule Tueur',
    ],
    'Mort-Vivant': [
      'Ame-en-peine','Banshee','Capitan','Croquemitaine','Ectoplasme',
      'Fantôme','Goule','Liche','Mohrg','Momie','Nécrochore','Nécromant',
      'Nécrophage','Ombre','Spectre','Squelette','Vampire','Zombie','Zobi de',
    ],
    'Démon': [
      'Abishaii Bleu','Abishaii Noir','Abishaii Rouge','Abishaii Vert',
      'Barghest','Behemoth','Chevalier du Chaos','Daemonite','Diablotin',
      "Elementaire d'Air","Elementaire d'Eau",'Elementaire de Feu',
      'Elementaire de Terre','Elementaire du Chaos','Elementaire Magmatique',
      'Erinyes','Fumeux','Gritche','Hellrot','Incube','Marilith',
      'Molosse Satanique','Palefroi Infernal','Pseudo-Dragon','Shai',
      'Succube','Xorn',
    ],
  };

  const FAMILLE_COULEUR = {
    'Insecte':    '#7bc67e',
    'Humanoïde':  '#64b5f6',
    'Monstre':    '#ff8a65',
    'Animal':     '#a5d6a7',
    'Mort-Vivant':'#b0bec5',
    'Démon':      '#ce93d8',
    'Inconnu':    '#888888',
  };

  const LIEU_CATEGORIES = {
    'Portail':   { couleur: '#f0c040', mots: ['Portail de Téléportation', 'Sortie de Portail'] },
    'Tanière':   { couleur: '#c084fc', mots: ['Grande Tanière', 'Tanière'] },
    'Météorite': { couleur: '#94a3b8', mots: ['Trou de Météorite', 'Geyser', 'Puits'] },
    'Services':  { couleur: '#34d399', mots: ['Antre du trépanateur', 'Armurerie', 'Auberge',
                   "Boutique de l'Envouteur", 'Matérioll', 'Office des Assignations', 'Lice',
                   'Tanneur', 'Tripotroll', 'Sépulcre', "Boutique d'Enchantement", 'Gowapier',
                   'Forge', 'Maison des Ancêtres', 'Téléporteur', 'Refuge'] },
    'Monstres':  { couleur: '#fb923c', mots: ['Cocon', 'Nid', 'Portail Démoniaque', 'Couvoir'] },
    'Raccourcis':{ couleur: '#38bdf8', mots: ['Campement', 'Tombe', 'Grotte', 'Lac souterrain',
                   'Caverne', 'Gouffre', 'Rocher', 'Cahute', 'Sanctuaire'] },
  };

  const LIEU_CAT_COULEUR = Object.fromEntries(
    Object.entries(LIEU_CATEGORIES).map(([k, v]) => [k, v.couleur])
  );

  function isGrandeTaniere(nomLieu) {
    return /^.+\s+\(\d+\)$/.test(nomLieu.trim());
  }

  function getLieuCategorie(nomLieu) {
    if (isGrandeTaniere(nomLieu)) return 'Tanière';
    const n = nomLieu.toLowerCase().normalize('NFD').replace(/[\u0300-\u036f]/g, '');
    for (const [cat, { mots }] of Object.entries(LIEU_CATEGORIES)) {
      for (const mot of mots) {
        const m = mot.toLowerCase().normalize('NFD').replace(/[\u0300-\u036f]/g, '');
        if (n.startsWith(m) || n.includes(m)) return cat;
      }
    }
    return 'Autre';
  }

  function normalise(str) {
    return str.toLowerCase().normalize('NFD').replace(/[\u0300-\u036f]/g, '').replace(/[^a-z0-9]/g, '');
  }

  const NOM_VERS_FAMILLE = {};
  for (const [famille, liste] of Object.entries(FAMILLES)) {
    for (const nom of liste) NOM_VERS_FAMILLE[normalise(nom)] = famille;
  }

  function getFamille(nomJeu) {
    const n = normalise(nomJeu);
    if (NOM_VERS_FAMILLE[n]) return NOM_VERS_FAMILLE[n];
    for (const [cle, f] of Object.entries(NOM_VERS_FAMILLE)) { if (n.startsWith(cle)) return f; }
    for (const [cle, f] of Object.entries(NOM_VERS_FAMILLE)) { if (n.includes(cle)) return f; }
    return 'Inconnu';
  }

  function decodeHTML(str) {
    const t = document.createElement('textarea'); t.innerHTML = str; return t.value;
  }

  function parseMonstres() {
    const monstres = [];
    if (typeof detailsVue === 'undefined') return monstres;
    for (const key in detailsVue) {
      const html = detailsVue[key];
      if (!html.includes('EBV(')) continue;
      const m = key.match(/^(-?\d+);(-?\d+);(\d+)$/);
      if (!m) continue;
      const x = +m[1], y = +m[2];
      for (const bloc of html.split(/<b>N=/)) {
        const nM = bloc.match(/^(-?\d+)<\/b>/);
        if (!nM) continue;
        const n = +nM[1];
        const re = /EMV\((\d+)\).*?EBV\('((?:[^'\\]|\\.)*)'[^)]*\).*?<\/a>\s*(\d+)(?:\s*\(\d+\s*PX\))?/g;
        let r;
        while ((r = re.exec(bloc)) !== null) {
          const nom = r[2].replace(/\\'/g, "'");
          monstres.push({ x, y, n, id: r[1], nom, niveau: +r[3], famille: getFamille(nom) });
        }
      }
    }
    return monstres;
  }

  function parseLieux() {
    const lieux = [];
    if (typeof detailsVueLieux === 'undefined') return lieux;
    for (const key in detailsVueLieux) {
      const html = detailsVueLieux[key];
      const m = key.match(/^(-?\d+);(-?\d+)$/);
      if (!m) continue;
      const x = +m[1], y = +m[2];
      for (const bloc of html.split(/<b>N=/)) {
        const nM = bloc.match(/^(-?\d+)<\/b>/);
        if (!nM) continue;
        const n = +nM[1];
        const lieuM = bloc.match(/<i>(\d+)\s+([^<]+)<\/i>/);
        if (!lieuM) continue;
        const nom = decodeHTML(lieuM[2].trim()).replace(/\\'/g, "'");
        lieux.push({ x, y, n, id: lieuM[1], nom });
      }
    }
    return lieux;
  }

  function initVue() {
    const monstres = parseMonstres();
    const lieux    = parseLieux();

    let familleFiltree = 'Toutes';
    let triMonstre     = 'niveau';
    let triDesc        = true;
    let onglet         = 'monstres';
    let catLieuFiltre  = 'Toutes';
    let lieuTriDesc    = true;

    const famillesPresentes = ['Toutes', ...Object.keys(FAMILLE_COULEUR).filter(f =>
      f !== 'Inconnu' && monstres.some(m => m.famille === f)
    )];
    if (monstres.some(m => m.famille === 'Inconnu')) famillesPresentes.push('Inconnu');

    const panel = document.createElement('div');
    panel.id = 'mh-search-panel';
    panel.style.cssText = `position:fixed;top:60px;right:12px;width:350px;background:#1a1a2e;border:1px solid #e94560;border-radius:6px;font-family:'Courier New',monospace;font-size:14px;color:#eee;z-index:9999;box-shadow:0 4px 20px rgba(233,69,96,0.3);overflow:hidden;`;

    const familleButtonsHTML = famillesPresentes.map(f => {
      const c = FAMILLE_COULEUR[f] || '#e94560';
      return `<button class="mh-famille-btn" data-famille="${f}" style="background:${f==='Toutes'?c:'#1a1a2e'};color:${f==='Toutes'?'#1a1a2e':c};border:1px solid ${c};border-radius:3px;padding:2px 6px;margin:2px;cursor:pointer;font-size:14px;font-family:inherit;">${f}</button>`;
    }).join('');

    panel.innerHTML = `
      <div id="mh-search-header" style="background:#e94560;color:#fff;padding:6px 10px;font-weight:bold;font-size:14px;cursor:move;display:flex;justify-content:space-between;align-items:center;user-select:none;">
        🔎 Recherche
        <span id="mh-search-toggle" data-nodrag="1" style="cursor:pointer;font-size:16px;line-height:1;">−</span>
      </div>
      <div id="mh-search-body" style="padding:8px;">
        <div style="display:flex;gap:4px;margin-bottom:8px;">
          <button id="mh-tab-monstres" style="flex:1;padding:4px;border-radius:4px 4px 0 0;border:1px solid #e94560;border-bottom:none;background:#e94560;color:#fff;cursor:pointer;font-family:inherit;font-size:13px;font-weight:bold;">☠ Monstres (${monstres.length})</button>
          <button id="mh-tab-lieux" style="flex:1;padding:4px;border-radius:4px 4px 0 0;border:1px solid #444;border-bottom:none;background:#1a1a2e;color:#aaa;cursor:pointer;font-family:inherit;font-size:13px;">📍 Lieux (${lieux.length})</button>
        </div>
        <input id="mh-search-input" type="text" placeholder="Nom ou ID…" style="width:100%;box-sizing:border-box;background:#0f3460;border:1px solid #e94560;color:#fff;padding:5px 8px;border-radius:4px;font-family:inherit;font-size:14px;outline:none;">
        <div id="mh-ctrl-monstres">
          <div style="margin-top:6px;line-height:1.8;">${familleButtonsHTML}</div>
          <div style="margin-top:6px;display:flex;align-items:center;gap:4px;">
            <span style="color:#aaa;font-size:12px;" id="mh-search-count">${monstres.length} monstre(s)</span>
            <button id="mh-sort-btn" style="margin-left:auto;background:#ffd166;color:#1a1a2e;border:1px solid #ffd166;border-radius:3px;padding:2px 8px;cursor:pointer;font-size:11px;font-family:inherit;">Niv ▼</button>
            <button id="mh-sort-alpha-btn" style="background:#0f3460;color:#aef;border:1px solid #aef;border-radius:3px;padding:2px 8px;cursor:pointer;font-size:11px;font-family:inherit;">A–Z</button>
          </div>
        </div>
        <div id="mh-ctrl-lieux" style="display:none;">
          <div style="margin-top:6px;line-height:1.8;" id="mh-lieu-cat-btns"></div>
          <div style="margin-top:6px;display:flex;align-items:center;gap:4px;">
            <span style="color:#aaa;font-size:12px;" id="mh-lieux-count">${lieux.length} lieu(x)</span>
            <button id="mh-lieu-sort-alpha-btn" style="margin-left:auto;background:#0f3460;color:#aef;border:1px solid #aef;border-radius:3px;padding:2px 8px;cursor:pointer;font-size:11px;font-family:inherit;">A–Z ▲</button>
          </div>
        </div>
        <div id="mh-search-results" style="margin-top:6px;max-height:300px;overflow-y:auto;scrollbar-width:thin;scrollbar-color:#e94560 #1a1a2e;"></div>
      </div>`;

    document.body.appendChild(panel);

    const catsBtnsEl = panel.querySelector('#mh-lieu-cat-btns');
    const catsPresentes = ['Toutes', ...Object.keys(LIEU_CAT_COULEUR).filter(c => lieux.some(l => getLieuCategorie(l.nom) === c))];
    if (lieux.some(l => getLieuCategorie(l.nom) === 'Autre')) catsPresentes.push('Autre');
    catsBtnsEl.innerHTML = catsPresentes.map(c => {
      const col = LIEU_CAT_COULEUR[c] || '#94a3b8';
      const actif = c === 'Toutes';
      return `<button class="mh-lieu-cat-btn" data-cat="${c}" style="background:${actif?col:'#1a1a2e'};color:${actif?'#1a1a2e':col};border:1px solid ${col};border-radius:3px;padding:2px 6px;margin:2px;cursor:pointer;font-size:13px;font-family:inherit;">${c}</button>`;
    }).join('');

    const input            = panel.querySelector('#mh-search-input');
    const results          = panel.querySelector('#mh-search-results');
    const countEl          = panel.querySelector('#mh-search-count');
    const lieuxCount       = panel.querySelector('#mh-lieux-count');
    const sortBtn          = panel.querySelector('#mh-sort-btn');
    const sortAlphaBtn     = panel.querySelector('#mh-sort-alpha-btn');
    const lieuSortAlphaBtn = panel.querySelector('#mh-lieu-sort-alpha-btn');
    const tabM             = panel.querySelector('#mh-tab-monstres');
    const tabL             = panel.querySelector('#mh-tab-lieux');
    const ctrlM            = panel.querySelector('#mh-ctrl-monstres');
    const ctrlL            = panel.querySelector('#mh-ctrl-lieux');

    function setOnglet(o) {
      onglet = o;
      if (o === 'monstres') {
        tabM.style.background = '#e94560'; tabM.style.color = '#fff'; tabM.style.borderColor = '#e94560';
        tabL.style.background = '#1a1a2e'; tabL.style.color = '#aaa'; tabL.style.borderColor = '#444';
        ctrlM.style.display = ''; ctrlL.style.display = 'none';
        input.placeholder = 'Nom ou ID de monstre…';
      } else {
        tabL.style.background = '#f0c040'; tabL.style.color = '#1a1a2e'; tabL.style.borderColor = '#f0c040';
        tabM.style.background = '#1a1a2e'; tabM.style.color = '#aaa'; tabM.style.borderColor = '#444';
        ctrlL.style.display = ''; ctrlM.style.display = 'none';
        input.placeholder = 'Nom ou ID de lieu…';
      }
      input.value = ''; refresh();
    }

    tabM.addEventListener('click', () => setOnglet('monstres'));
    tabL.addEventListener('click', () => setOnglet('lieux'));

    function updateSortButtons() {
      if (triMonstre === 'niveau') {
        sortBtn.style.background = '#ffd166'; sortBtn.style.color = '#1a1a2e';
        sortAlphaBtn.style.background = '#0f3460'; sortAlphaBtn.style.color = '#aef';
        sortBtn.textContent = triDesc ? 'Niv ▼' : 'Niv ▲';
      } else {
        sortAlphaBtn.style.background = '#aef'; sortAlphaBtn.style.color = '#1a1a2e';
        sortBtn.style.background = '#0f3460'; sortBtn.style.color = '#ffd166';
        sortAlphaBtn.textContent = triDesc ? 'A–Z ▲' : 'Z–A ▼';
      }
    }

    sortBtn.addEventListener('click', () => {
      if (triMonstre === 'niveau') triDesc = !triDesc; else { triMonstre = 'niveau'; triDesc = true; }
      updateSortButtons(); refresh();
    });
    sortAlphaBtn.addEventListener('click', () => {
      if (triMonstre === 'alpha') triDesc = !triDesc; else { triMonstre = 'alpha'; triDesc = true; }
      updateSortButtons(); refresh();
    });
    lieuSortAlphaBtn.addEventListener('click', () => {
      lieuTriDesc = !lieuTriDesc;
      lieuSortAlphaBtn.textContent = lieuTriDesc ? 'A–Z ▲' : 'Z–A ▼';
      refresh();
    });

    function getFilteredMonstres() {
      const q = input.value.trim().toLowerCase();
      let liste = monstres.slice();
      if (familleFiltree !== 'Toutes') liste = liste.filter(m => m.famille === familleFiltree);
      if (q) liste = liste.filter(m => m.nom.toLowerCase().includes(q) || m.id.toString().includes(q));
      if (triMonstre === 'niveau') liste.sort((a,b) => triDesc ? b.niveau-a.niveau : a.niveau-b.niveau);
      else liste.sort((a,b) => triDesc ? a.nom.localeCompare(b.nom,'fr') : b.nom.localeCompare(a.nom,'fr'));
      return liste;
    }

    function renderMonstres(liste) {
      if (!liste.length) { results.innerHTML = '<div style="color:#888;padding:4px 0;">Aucun résultat.</div>'; return; }
      results.innerHTML = liste.map(m => `
        <div style="border-bottom:1px solid #0f3460;padding:6px 2px;cursor:pointer;line-height:1.6;" data-id="${m.id}" class="mh-result-row">
          <div style="color:#e94560;font-weight:bold;font-size:14px;">${m.nom}</div>
          <div style="color:#aef;font-size:14px;">X=${m.x} &nbsp;Y=${m.y} &nbsp;N=${m.n} &nbsp;&nbsp;<span style="color:#ffd166;">Niv.${m.niveau}</span></div>
          <div style="color:#888;font-size:13px;">ID:${m.id}</div>
        </div>`).join('');
      results.querySelectorAll('.mh-result-row').forEach(row => {
        row.addEventListener('mouseenter', () => row.style.background = '#0f3460');
        row.addEventListener('mouseleave', () => row.style.background = '');
        row.addEventListener('click', () => { if (typeof EMV === 'function') EMV(row.dataset.id); });
      });
    }

    function getFilteredLieux() {
      const q = input.value.trim().toLowerCase();
      let liste = lieux.slice();
      if (catLieuFiltre !== 'Toutes') liste = liste.filter(l => getLieuCategorie(l.nom) === catLieuFiltre);
      if (q) liste = liste.filter(l => l.nom.toLowerCase().includes(q) || l.id.toString().includes(q));
      liste.sort((a,b) => lieuTriDesc ? a.nom.localeCompare(b.nom,'fr') : b.nom.localeCompare(a.nom,'fr'));
      return liste;
    }

    function renderLieux(liste) {
      if (!liste.length) { results.innerHTML = '<div style="color:#888;padding:4px 0;">Aucun résultat.</div>'; return; }
      results.innerHTML = liste.map(l => {
        const cat = getLieuCategorie(l.nom);
        const c = LIEU_CAT_COULEUR[cat] || '#94a3b8';
        return `<div style="border-bottom:1px solid #0f3460;padding:6px 2px;line-height:1.6;">
          <div style="color:${c};font-weight:bold;font-size:14px;">${l.nom}</div>
          <div style="color:#aef;font-size:14px;">X=${l.x} &nbsp;Y=${l.y} &nbsp;N=${l.n}</div>
          <div style="color:#888;font-size:13px;">ID:${l.id}</div>
        </div>`;
      }).join('');
    }

    function refresh() {
      if (onglet === 'monstres') {
        const liste = getFilteredMonstres();
        countEl.textContent = `${liste.length} / ${monstres.length} monstre(s)`;
        renderMonstres(liste);
      } else {
        const liste = getFilteredLieux();
        lieuxCount.textContent = `${liste.length} / ${lieux.length} lieu(x)`;
        renderLieux(liste);
      }
    }

    input.addEventListener('input', refresh);

    panel.querySelectorAll('.mh-lieu-cat-btn').forEach(btn => {
      btn.addEventListener('click', () => {
        catLieuFiltre = btn.dataset.cat;
        panel.querySelectorAll('.mh-lieu-cat-btn').forEach(b => {
          const c = b.dataset.cat, col = LIEU_CAT_COULEUR[c] || '#94a3b8', actif = c === catLieuFiltre;
          b.style.background = actif ? col : '#1a1a2e'; b.style.color = actif ? '#1a1a2e' : col;
        });
        refresh();
      });
    });

    panel.querySelectorAll('.mh-famille-btn').forEach(btn => {
      btn.addEventListener('click', () => {
        familleFiltree = btn.dataset.famille;
        panel.querySelectorAll('.mh-famille-btn').forEach(b => {
          const f = b.dataset.famille, c = FAMILLE_COULEUR[f] || '#e94560', actif = f === familleFiltree;
          b.style.background = actif ? c : '#1a1a2e'; b.style.color = actif ? '#1a1a2e' : c;
        });
        refresh();
      });
    });

    updateSortButtons(); refresh();

    const toggle = panel.querySelector('#mh-search-toggle');
    const body   = panel.querySelector('#mh-search-body');
    toggle.addEventListener('click', () => {
      body.style.display = body.style.display === 'none' ? '' : 'none';
      toggle.textContent = body.style.display === 'none' ? '+' : '−';
    });

    makeDraggable(panel, panel.querySelector('#mh-search-header'));
    makeResizable(panel);
  }

  // ══════════════════════════════════════════════════════════════
  // MODULE POSSESSIONS — tableau agrégé
  // ══════════════════════════════════════════════════════════════

  const TEMPLATES = [
    "de l'Aigle", "des Béhémoths", "des Cyclopes", "des Enragés",
    "de Feu", "des Mages", "de l'Orage", "de l'Ours",
    "du Pic", "du Rat", "de Résistance", "de la Salamandre",
    "du Temps", "de la Terre", "du Sable", "des Vampires",
    "des Duellistes", "des Champions", "des Anciens",
    "en Mithril",
  ].sort((a, b) => b.length - a.length);

  function extraireTemplates(enchant) {
    if (!enchant) return [];
    const trouvés = [];
    for (const tpl of TEMPLATES) {
      if (enchant.toLowerCase().includes(tpl.toLowerCase())) trouvés.push(tpl);
    }
    return trouvés;
  }

  const STATS_M = {
    'ATT':    ['att',  'attM'],
    'ESQ':    ['esq',  'esqM'],
    'DEG':    ['deg',  'degM'],
    'REG':    ['reg',  'regM'],
    'VUE':    ['vue',  'vueM'],
    'PV':     ['pv',   'pvM'],
    'Armure': ['arm',  'armM'],
  };
  const STATS_PCT_CONNUES = new Set(['RM', 'MM', 'Concentration']);

  function parseVal(str) {
    if (str === null || str === undefined) return null;
    const n = parseFloat(str.replace(',', '.'));
    return isNaN(n) ? null : n;
  }

  function parseStats(statsStr) {
    const s = {
      att: null, attM: null, esq: null, esqM: null,
      deg: null, degM: null, reg: null, regM: null,
      vue: null, vueM: null, pv:  null, pvM:  null,
      tour: null, arm: null, armM: null, rm: null, mm: null, pourcent: '',
    };
    if (!statsStr) return s;
    const segments = statsStr.split(/\s*\|\s*/);
    for (const seg of segments) {
      const colonIdx = seg.indexOf(':');
      if (colonIdx === -1) continue;
      const cle    = seg.slice(0, colonIdx).trim();
      const valStr = seg.slice(colonIdx + 1).trim();
      if (STATS_M[cle]) {
        const [k, kM] = STATS_M[cle];
        if (valStr.includes('\\')) {
          const parts = valStr.split('\\');
          s[k]  = parseVal(parts[0]);
          s[kM] = parseVal(parts[1]);
        } else { s[k] = parseVal(valStr); }
        continue;
      }
      if (cle === 'TOUR') {
        const m = valStr.match(/([+-]?\d+)\s*min/);
        s.tour = m ? parseVal(m[1]) : parseVal(valStr.replace(/[^0-9+\-]/g, '') || null);
        continue;
      }
      if (cle === 'RM') { s.rm = parseVal(valStr.replace('%', '').trim()); continue; }
      if (cle === 'MM') { s.mm = parseVal(valStr.replace('%', '').trim()); continue; }
      if (cle === 'Concentration') continue;
      if (valStr.includes('%') && !STATS_PCT_CONNUES.has(cle)) {
        const entry = `${cle} : ${valStr}`;
        s.pourcent = s.pourcent ? s.pourcent + ' | ' + entry : entry;
      }
    }
    return s;
  }

  function parsePossessions() {
    const items = [];
    const rows = document.querySelectorAll('table tr');
    let currentTroll = '?', currentSection = '?';

    rows.forEach(row => {
      const th = row.querySelector('th');
      if (th) {
        const text = th.textContent.trim();
        const gM = text.match(/Gowap\(s\)\s+de\s+(.+?)(?:\s{2,}|$)/);
        const tM = text.match(/Tani[eè]re\(s\)\s+de\s+(.+?)(?:\s{2,}|$)/);
        if (gM) { currentTroll = gM[1].trim(); currentSection = 'Gowap'; }
        if (tM) { currentTroll = tM[1].trim(); currentSection = 'Tanière'; }
        return;
      }
      const td = row.querySelector('td');
      if (!td) return;

      td.querySelectorAll('ul.gowapList, ul.taniereList').forEach(containerList => {
        Array.from(containerList.children).filter(c => c.tagName === 'LI').forEach(gowapLi => {
          const gowapText = gowapLi.textContent.trim().split('\n')[0].trim();
          let containerLabel = '';
          if (currentSection === 'Gowap') {
            const m = gowapText.match(/Gowap\s*:\s*(\d+)\s*\(X\s*=\s*(-?\d+),\s*Y\s*=\s*(-?\d+),\s*N\s*=\s*(-?\d+)\)/);
            containerLabel = m ? `Gowap ${m[1]} (${m[2]},${m[3]},${m[4]})` : gowapText.slice(0, 40);
          } else {
            const m = gowapText.match(/Tani[eè]re\s*:\s*(\d+)\s*\(X\s*=\s*(-?\d+),\s*Y\s*=\s*(-?\d+),\s*N\s*=\s*(-?\d+)\)/);
            containerLabel = m ? `Tanière ${m[1]} (${m[2]},${m[3]},${m[4]})` : gowapText.slice(0, 40);
          }

          let equipUl = gowapLi.nextElementSibling;
          if (!equipUl || (!equipUl.classList.contains('gowapEquipementList') && !equipUl.classList.contains('taniereEquipementList'))) return;

          equipUl.querySelectorAll('li').forEach(li => {
            const raw = li.textContent.trim();
            const idM = raw.match(/^\[(\d+)\]/);
            if (!idM) return;
            const id = idM[1];
            const afterId = raw.slice(idM[0].length).trim();
            const colonIdx = afterId.indexOf(':');
            if (colonIdx === -1) return;
            const type    = afterId.slice(0, colonIdx).trim();
            const rest    = afterId.slice(colonIdx + 1).trim();

            let nom = '', enchant = '';
            const nomSpan = li.querySelector('strong');
            if (nomSpan) {
              const beforeStrong = li.childNodes[0] ? li.childNodes[0].textContent : '';
              const cpos = beforeStrong.indexOf(':');
              nom = cpos !== -1 ? beforeStrong.slice(cpos + 1).trim() : beforeStrong.trim();
              enchant = nomSpan.textContent.trim();
            } else {
              const cpos = rest.indexOf('(');
              nom = cpos !== -1 ? rest.slice(0, cpos).trim() : rest.trim();
            }

            const statsMatch = rest.match(/\(([^)]*)\)[^(]*$/);
            const statsRaw = statsMatch ? statsMatch[1] : '';
            const poidsM = raw.match(/,\s*(\d+)\s*min\s*$/);
            const poids = poidsM ? +poidsM[1] : 0;
            const nomComplet = enchant ? `${nom} ${enchant}` : nom;
            const templates = extraireTemplates(enchant);
            const mithril   = enchant.toLowerCase().includes('en mithril');
            const stats     = parseStats(statsRaw);

            const poidsStr  = poids > 0 ? `, ${poids} min` : '';
            const statsStr2 = statsRaw ? ` (${statsRaw})` : '';
            const nomCopie  = enchant ? `${nom} ${enchant}` : nom;
            const clipText  = `[${id}] ${type} : ${nomCopie}${statsStr2}${poidsStr}`;

            if (!type) return;
            items.push({
              id, type,
              nom: nomComplet.trim() || '(sans nom)',
              nomBase: nom.trim(),
              enchant: enchant.trim(),
              templates,
              nbTemplates: templates.filter(t => t.toLowerCase() !== 'en mithril').length,
              mithril,
              statsRaw,
              clipText,
              ...stats,
              troll: currentTroll,
              localisation: `${currentSection} → ${currentTroll} → ${containerLabel}`,
              section: currentSection,
              poids,
            });
          });
        });
      });
    });
    return items;
  }

  const STAT_COLS = [
    { key: 'att',      label: 'ATT'   },
    { key: 'attM',     label: 'ATT+M' },
    { key: 'esq',      label: 'ESQ'   },
    { key: 'esqM',     label: 'ESQ+M' },
    { key: 'deg',      label: 'DEG'   },
    { key: 'degM',     label: 'DEG+M' },
    { key: 'reg',      label: 'REG'   },
    { key: 'regM',     label: 'REG+M' },
    { key: 'vue',      label: 'VUE'   },
    { key: 'vueM',     label: 'VUE+M' },
    { key: 'pv',       label: 'PV'    },
    { key: 'pvM',      label: 'PV+M'  },
    { key: 'tour',     label: 'TOUR'  },
    { key: 'arm',      label: 'ARM'   },
    { key: 'armM',     label: 'ARM+M' },
    { key: 'rm',       label: 'RM%'   },
    { key: 'mm',       label: 'MM%'   },
    { key: 'pourcent', label: '%'     },
  ];

  function fmtStat(val) {
    if (val === null || val === undefined || val === '') return '';
    if (typeof val === 'number') return (val >= 0 ? '+' : '') + val;
    return val;
  }

  function statColor(val) {
    if (typeof val !== 'number') return '';
    if (val > 0) return 'color:#a5d6a7';
    if (val < 0) return 'color:#f48771';
    return 'color:#888';
  }

  function showToast(msg) {
    let t = document.getElementById('mh-toast');
    if (!t) {
      t = document.createElement('div');
      t.id = 'mh-toast';
      t.style.cssText = `position:fixed;bottom:24px;left:50%;transform:translateX(-50%);
        background:#0f3460;color:#a5d6a7;border:1px solid #a5d6a7;border-radius:6px;
        padding:7px 18px;font-family:'Courier New',monospace;font-size:13px;
        z-index:99999;pointer-events:none;opacity:0;transition:opacity .2s;white-space:nowrap;`;
      document.body.appendChild(t);
    }
    t.textContent = msg;
    t.style.opacity = '1';
    clearTimeout(t._timer);
    t._timer = setTimeout(() => { t.style.opacity = '0'; }, 1800);
  }

  function initPossessions() {
    const items = parsePossessions();
    if (!items.length) return;

    const templatesPresents = ['(tous)', ...TEMPLATES.filter(t =>
      t.toLowerCase() !== 'en mithril' &&
      items.some(i => i.templates.some(it => it.toLowerCase() === t.toLowerCase()))
    )];

    const style = document.createElement('style');
    style.textContent = `
      #mh-poss-panel {
        position:fixed; top:60px; right:12px; width:98vw; max-width:1600px; max-height:90vh;
        background:#1a1a2e; border:1px solid #e94560; border-radius:6px;
        font-family:'Courier New',monospace; font-size:14px; color:#eee;
        z-index:9999; box-shadow:0 4px 20px rgba(233,69,96,0.3);
        display:flex; flex-direction:column; overflow:hidden;
      }
      #mh-poss-header {
        background:#e94560; color:#fff; padding:6px 10px; font-weight:bold; font-size:14px;
        cursor:move; display:flex; justify-content:space-between; align-items:center;
        user-select:none; flex-shrink:0;
      }
      #mh-poss-body { padding:8px; overflow:hidden; display:flex; flex-direction:column; flex:1; min-height:0; }
      #mh-poss-filters { display:flex; gap:6px; flex-wrap:wrap; margin-bottom:6px; flex-shrink:0; align-items:center; }
      #mh-poss-filters input, #mh-poss-filters select {
        background:#0f3460; color:#fff; border:1px solid #e94560;
        padding:4px 7px; border-radius:4px; font-family:inherit; font-size:14px; outline:none;
      }
      #mh-poss-filters input[type="text"] { width:180px; }
      #mh-poss-filters label {
        display:flex; align-items:center; gap:5px; cursor:pointer;
        color:#f0d080; font-size:14px; font-weight:bold; user-select:none;
        background:#0f3460; border:2px solid #f0d080; border-radius:4px;
        padding:3px 9px; transition:background .15s;
      }
      #mh-poss-filters label:has(input:checked) { background:#f0d080; color:#1a1a2e; }
      #mh-poss-filters label input[type="checkbox"] { accent-color:#f0d080; width:15px; height:15px; cursor:pointer; }
      #mh-poss-stats { color:#aaa; font-size:13px; margin-bottom:4px; flex-shrink:0; }
      #mh-poss-table-wrap { overflow:auto; flex:1; min-height:0; scrollbar-width:thin; scrollbar-color:#e94560 #1a1a2e; }
      #mh-poss-table { border-collapse:collapse; font-size:14px; table-layout:fixed; width:max-content; min-width:100%; }
      #mh-poss-table th {
        background:#0f3460; color:#aef; padding:5px 6px; text-align:left;
        border-bottom:1px solid #e94560; border-right:1px solid #1a1a2e;
        cursor:pointer; user-select:none;
        position:sticky; top:0; z-index:2; font-size:13px;
        white-space:nowrap; overflow:hidden; text-overflow:ellipsis;
      }
      #mh-poss-table th:hover { background:#1a3a70; }
      #mh-poss-table th.sa::after { content:' ▲'; }
      #mh-poss-table th.sd::after { content:' ▼'; }
      #mh-poss-table thead tr.group-row th {
        background:#0a2040; color:#7ad; font-size:12px; text-align:center;
        border-bottom:none; cursor:default; padding:2px 4px; z-index:3;
      }
      #mh-poss-table thead tr.group-row th[rowspan] {
        background:#0f3460; color:#aef; cursor:pointer;
        vertical-align:bottom; padding-bottom:5px;
      }
      #mh-poss-table td {
        padding:4px 6px; border-bottom:1px solid #0f3460; border-right:1px solid #0a1830;
        vertical-align:top; font-size:14px; overflow:hidden; text-overflow:ellipsis;
      }
      #mh-poss-table tr:hover td { background:#0f3460; }
      .col-id   { width:72px;  min-width:50px; }
      .col-type { width:110px; min-width:60px; white-space:nowrap; }
      .col-nom  { width:200px; min-width:80px; white-space:normal; }
      .col-stat { width:52px;  min-width:32px; text-align:right; white-space:nowrap; }
      .col-pct  { width:160px; min-width:60px; white-space:normal; }
      .col-poids{ width:52px;  min-width:36px; text-align:right; white-space:nowrap; }
      .col-loc  { width:220px; min-width:80px; white-space:normal; }
      .pi   { color:#888; font-size:13px; }
      .pn   { color:#fff; font-weight:bold; }
      .pn.is-mithril { color:#f0d080; }
      .pn-clickable { cursor:pointer; }
      .pn-clickable:hover .pn-name { text-decoration:underline; text-decoration-style:dotted; }
      .pt   { color:#ffd166; }
      .pl   { color:#888; font-size:13px; }
      .ppoids { color:#a5d6a7; }
      .ptpl { color:#ce93d8; font-size:12px; }
      .pmithril-badge { display:inline-block; font-size:11px; font-weight:bold; background:#f0d080; color:#1a1a2e; border-radius:3px; padding:1px 4px; margin-left:3px; vertical-align:middle; }
      .pstat { color:#ccc; }
      .ppct  { color:#c8a0f0; }
      .stat-grp-sep { border-left:2px solid #e94560 !important; }
      /* Bouton MAJ */
      #poss-maj-all {
        background:#1a1a2e; color:#e94560; border:2px solid #e94560;
        border-radius:4px; padding:3px 10px; font-family:inherit; font-size:14px;
        font-weight:bold; cursor:pointer; margin-left:auto; transition:background .15s;
      }
      #poss-maj-all:hover:not(:disabled) { background:#e94560; color:#fff; }
      #poss-maj-all:disabled { opacity:.45; cursor:not-allowed; }
      /* Bandeau statut MAJ */
      #mh-maj-status {
        display:none; flex-shrink:0; margin-bottom:6px;
        padding:8px 12px; border-radius:4px; font-size:13px; line-height:1.5;
        background:#0f3460; border:1px solid #e94560; color:#ffd166;
      }
      #mh-maj-status.done  { border-color:#a5d6a7; color:#a5d6a7; }
      #mh-maj-status.error { border-color:#f48771; color:#f48771; }
      #mh-poss-panel.collapsed #mh-poss-body { display:none; }
      #mh-poss-panel::after { content:''; position:absolute; bottom:0; right:0; width:12px; height:12px; border-right:3px solid #e94560; border-bottom:3px solid #e94560; border-radius:0 0 4px 0; pointer-events:none; }
    `;
    document.head.appendChild(style);

    const MATOS_TYPES = new Set([
      'Arme (1 main)', 'Arme (2 mains)', 'Casque', 'Bouclier',
      'Bottes', 'Armure', 'Anneau', 'Talisman',
    ]);

    const allTypes       = [...new Set(items.map(i => i.type).sort())];
    const matosPresents  = allTypes.filter(t => MATOS_TYPES.has(t));
    const autresPresents = allTypes.filter(t => !MATOS_TYPES.has(t));

    function buildTypeSelect() {
      let html = `<option value="(tous)">(tous)</option>`;
      html += `<option value="__MATOS__">⚔ Matos (tous)</option>`;
      if (matosPresents.length) {
        html += `<optgroup label="— Matos —">`;
        html += matosPresents.map(t => `<option value="${escHtml(t)}">&nbsp;&nbsp;${escHtml(t)}</option>`).join('');
        html += `</optgroup>`;
      }
      if (autresPresents.length) {
        html += `<optgroup label="— Autres —">`;
        html += autresPresents.map(t => `<option value="${escHtml(t)}">${escHtml(t)}</option>`).join('');
        html += `</optgroup>`;
      }
      return html;
    }

    const sections  = ['(tous)', 'Gowap', 'Tanière'];
    const nbTplOpts = [
      { val: '(tous)', label: '(tous templates)' },
      { val: '1',      label: 'Simple (×1)' },
      { val: '2',      label: 'Double (×2)' },
      { val: '3',      label: 'Triple (×3)' },
      { val: '4',      label: 'Quadruple (×4+)' },
    ];

    const STAT_GROUPS = [
      { label: 'ATT',  keys: ['att','attM'] },
      { label: 'ESQ',  keys: ['esq','esqM'] },
      { label: 'DEG',  keys: ['deg','degM'] },
      { label: 'REG',  keys: ['reg','regM'] },
      { label: 'VUE',  keys: ['vue','vueM'] },
      { label: 'PV',   keys: ['pv','pvM']   },
      { label: 'TOUR', keys: ['tour']        },
      { label: 'ARM',  keys: ['arm','armM'] },
      { label: 'RM',   keys: ['rm']          },
      { label: 'MM',   keys: ['mm']          },
      { label: '%',    keys: ['pourcent']    },
    ];
    const STAT_GROUP_FIRSTS = new Set(STAT_GROUPS.map(g => g.keys[0]));

    const groupRow1 =
      `<th rowspan="2" data-col="id"   class="col-id"   style="vertical-align:bottom;">ID</th>` +
      `<th rowspan="2" data-col="type" class="col-type" style="vertical-align:bottom;">Type</th>` +
      `<th rowspan="2" data-col="nom"  class="col-nom"  style="vertical-align:bottom;">Nom</th>` +
      STAT_GROUPS.map(g =>
        `<th colspan="${g.keys.length}" style="text-align:center;border-left:2px solid #e94560;">${g.label}</th>`
      ).join('') +
      `<th rowspan="2" data-col="poids"        class="col-poids" style="vertical-align:bottom;">Poids</th>` +
      `<th rowspan="2" data-col="localisation" class="col-loc"   style="vertical-align:bottom;">Localisation</th>`;

    const groupRow2 = STAT_COLS.map(c => {
      const sep   = STAT_GROUP_FIRSTS.has(c.key) ? ' stat-grp-sep' : '';
      const color = c.label.includes('+M') ? 'color:#ce93d8' : '';
      const cls   = c.key === 'pourcent' ? 'col-pct' : 'col-stat';
      return `<th data-col="${c.key}" class="${cls}${sep}" style="${color}">${c.label}</th>`;
    }).join('');

    const panel = document.createElement('div');
    panel.id = 'mh-poss-panel';
    panel.innerHTML = `
      <div id="mh-poss-header">
        📦 Possessions agrégées <span style="font-size:13px;opacity:.7">(${items.length} objets)</span>
        <span id="mh-poss-toggle" data-nodrag="1" style="cursor:pointer;font-size:16px;">−</span>
      </div>
      <div id="mh-poss-body">
        <div id="mh-poss-filters">
          <input id="poss-search" type="text" placeholder="Recherche nom/stats/ID…">
          <select id="poss-type">${buildTypeSelect()}</select>
          <select id="poss-section">${sections.map(s => `<option>${s}</option>`).join('')}</select>
          <select id="poss-template">${templatesPresents.map(t => `<option value="${escHtml(t)}">${escHtml(t)}</option>`).join('')}</select>
          <select id="poss-nbtpl">${nbTplOpts.map(o => `<option value="${o.val}">${o.label}</option>`).join('')}</select>
          <label title="Afficher uniquement les objets en Mithril">
            <input type="checkbox" id="poss-mithril"> ✨ Mithril
          </label>
          <button id="poss-maj-all" data-nodrag="1"
            title="⚠ Attention, à utiliser avec parcimonie">
            🔄 Tout MAJ
          </button>
        </div>
        <div id="mh-maj-status"></div>
        <div id="mh-poss-stats"></div>
        <div id="mh-poss-loading" style="display:none;padding:20px 0;text-align:center;color:#aef;font-size:15px;letter-spacing:1px;">
          ⏳ Chargement en cours…
        </div>
        <div id="mh-poss-table-wrap">
          <table id="mh-poss-table">
            <thead>
              <tr class="group-row">${groupRow1}</tr>
              <tr>${groupRow2}</tr>
            </thead>
            <tbody id="poss-tbody"></tbody>
          </table>
        </div>
      </div>`;
    document.body.appendChild(panel);

    let sortCol = 'type', sortAsc = true;
    const searchEl  = panel.querySelector('#poss-search');
    const typeEl    = panel.querySelector('#poss-type');
    const sectEl    = panel.querySelector('#poss-section');
    const tplEl     = panel.querySelector('#poss-template');
    const nbTplEl   = panel.querySelector('#poss-nbtpl');
    const mithrilEl = panel.querySelector('#poss-mithril');
    const statsEl   = panel.querySelector('#mh-poss-stats');
    const loadingEl = panel.querySelector('#mh-poss-loading');
    const tableWrap = panel.querySelector('#mh-poss-table-wrap');
    const tbody     = panel.querySelector('#poss-tbody');
    const allSortTh = panel.querySelectorAll('#mh-poss-table th[data-col]');
    const majBtn    = panel.querySelector('#poss-maj-all');
    const majStatus = panel.querySelector('#mh-maj-status');

    const MAJ_LS_KEY  = 'bricoultool_maj_ts';
    const MAJ_DELAI   = 24 * 60 * 60 * 1000; // 24h en ms

    function majTempsRestant() {
      try {
        const ts = parseInt(localStorage.getItem(MAJ_LS_KEY) || '0', 10);
        if (!ts) return 0;
        const restant = MAJ_DELAI - (Date.now() - ts);
        return restant > 0 ? restant : 0;
      } catch(e) { return 0; }
    }

    function majAppliquerEtat() {
      const restant = majTempsRestant();
      if (restant > 0) {
        const h = Math.floor(restant / 3600000);
        const m = Math.floor((restant % 3600000) / 60000);
        majBtn.disabled = true;
        majBtn.title = `⏳ Disponible dans ${h}h${m.toString().padStart(2,'0')}`;
        majStatus.className = '';
        majStatus.style.display = 'block';
        majStatus.innerHTML =
          `⏳ Prochaine mise à jour disponible dans <strong>${h}h${m.toString().padStart(2,'0')}</strong>`;
      } else {
        majBtn.disabled = false;
        majBtn.title = '⚠ Attention, à utiliser avec parcimonie';
      }
    }

    // Appliquer l'état au chargement
    majAppliquerEtat();

    // ── Bouton "Tout MAJ" ──────────────────────────────────────
    majBtn.addEventListener('click', async () => {
      // Vérifier à nouveau au clic (sécurité)
      if (majTempsRestant() > 0) { majAppliquerEtat(); return; }

      // Collecter les liens MAJ uniques depuis la page (hors script)
      const seen = new Set();
      const majLinks = [];
      document.querySelectorAll('a[href*="update_"][href*="source=possessions"]').forEach(a => {
        const href = a.getAttribute('href');
        if (!seen.has(href)) { seen.add(href); majLinks.push(href); }
      });

      if (!majLinks.length) {
        majStatus.className = 'error';
        majStatus.style.display = 'block';
        majStatus.textContent = '⚠ Aucun lien de mise à jour trouvé sur la page.';
        return;
      }

      majBtn.disabled = true;
      majStatus.className = '';
      majStatus.style.display = 'block';

      // Construire la base URL (même dossier que la page courante)
      const baseUrl = location.href.replace(/[^/]+(\?.*)?$/, '');

      let done = 0, errors = 0;

      for (const href of majLinks) {
        // Mise à jour du message AVANT la requête
        majStatus.innerHTML =
          `⏳ Mise à jour en cours, cela peut prendre quelques minutes…` +
          `<br><span style="color:#aef;font-size:12px;">` +
          `${done} / ${majLinks.length} traité(s)` +
          (errors > 0 ? ` — <span style="color:#f48771;">${errors} erreur(s)</span>` : '') +
          `</span>`;

        // Construire l'URL absolue
        const url = href.startsWith('http') ? href : baseUrl + href;

        try {
          // fetch + attente de la réponse complète avant de passer au suivant
          const resp = await fetch(url, { credentials: 'include' });
          if (!resp.ok) errors++;
          // Lire le body entier pour s'assurer que le serveur a terminé
          await resp.text();
        } catch (e) {
          errors++;
        }

        done++;
      }

      // Sauvegarder l'horodatage de la MAJ et griser le bouton 24h
      try { localStorage.setItem(MAJ_LS_KEY, Date.now().toString()); } catch(e) {}
      majAppliquerEtat();

      // Message final
      const errMsg = errors > 0
        ? ` — <span style="color:#f48771;">${errors} erreur(s)</span>`
        : '';
      majStatus.className = errors > 0 ? 'error' : 'done';
      majStatus.innerHTML =
        `✅ Mise à jour terminée — ${done} gowap(s)/tanière(s) traité(s)${errMsg}` +
        `<br><span style="font-size:12px;opacity:.8;">Rechargez la page pour voir les nouvelles données. Prochaine MAJ dans 24h.</span>`;
    });

    // ── Tableau ────────────────────────────────────────────────

    function render() {
      loadingEl.style.display = 'block';
      tableWrap.style.visibility = 'hidden';
      statsEl.textContent = '';
      setTimeout(_doRender, 0);
    }

    function _doRender() {
      const q  = searchEl.value.toLowerCase();
      const tv = typeEl.value, sv = sectEl.value;
      const tplv = tplEl.value, nbv = nbTplEl.value;
      const onlyMithril = mithrilEl.checked;

      let list = items.filter(i => {
        if (tv === '__MATOS__' && !MATOS_TYPES.has(i.type)) return false;
        if (tv !== '(tous)' && tv !== '__MATOS__' && i.type !== tv)  return false;
        if (sv !== '(tous)' && i.section !== sv)  return false;
        if (onlyMithril && !i.mithril)             return false;
        if (tplv !== '(tous)' && !i.templates.some(t => t.toLowerCase() === tplv.toLowerCase())) return false;
        if (nbv !== '(tous)') {
          const n = +nbv;
          if (n === 4 ? i.nbTemplates < 4 : i.nbTemplates !== n) return false;
        }
        if (q && !i.nom.toLowerCase().includes(q) &&
                 !i.statsRaw.toLowerCase().includes(q) &&
                 !i.id.includes(q) &&
                 !i.troll.toLowerCase().includes(q) &&
                 !i.localisation.toLowerCase().includes(q) &&
                 !i.pourcent.toLowerCase().includes(q)) return false;
        return true;
      });

      list.sort((a, b) => {
        const va = a[sortCol] ?? null, vb = b[sortCol] ?? null;
        if (va === null && vb === null) return 0;
        if (va === null) return 1;
        if (vb === null) return -1;
        const cmp = typeof va === 'number'
          ? va - vb
          : va.toString().localeCompare(vb.toString(), 'fr');
        return sortAsc ? cmp : -cmp;
      });

      const poidsTotal = list.reduce((s, i) => s + i.poids, 0);
      statsEl.textContent = `${list.length} / ${items.length} objet(s) — ${poidsTotal} min de poids`;

      tbody.innerHTML = list.map(i => {
        const tplsAffich   = i.templates.filter(t => t.toLowerCase() !== 'en mithril');
        const tplHtml      = tplsAffich.length
          ? `<div class="ptpl">${tplsAffich.map(t => escHtml(t)).join(' + ')}</div>` : '';
        const mithrilBadge = i.mithril ? `<span class="pmithril-badge">✨ Mithril</span>` : '';
        const clipEsc      = escHtml(i.clipText);

        const statCells = STAT_COLS.map(c => {
          const val = i[c.key];
          const sep = STAT_GROUP_FIRSTS.has(c.key) ? ' stat-grp-sep' : '';
          if (c.key === 'pourcent') return `<td class="ppct col-pct${sep}">${escHtml(val || '')}</td>`;
          const txt = fmtStat(val);
          const col = statColor(val);
          return `<td class="pstat col-stat${sep}" style="${col}">${escHtml(txt)}</td>`;
        }).join('');

        return `<tr>
          <td class="pi col-id">${escHtml(i.id)}</td>
          <td class="pt col-type">${escHtml(i.type)}</td>
          <td class="${i.mithril ? 'pn is-mithril' : 'pn'} col-nom pn-clickable" data-clip="${clipEsc}" title="Cliquer pour copier">
            <span class="pn-name">${escHtml(i.nomBase)}</span>${mithrilBadge}${tplHtml}
          </td>
          ${statCells}
          <td class="ppoids col-poids">${i.poids > 0 ? i.poids : ''}</td>
          <td class="pl col-loc">${escHtml(i.localisation)}</td>
        </tr>`;
      }).join('');

      tbody.querySelectorAll('.pn-clickable').forEach(td => {
        td.addEventListener('click', () => {
          const txt = td.dataset.clip;
          function doCopy() {
            const ta = document.createElement('textarea');
            ta.value = txt; ta.style.cssText = 'position:fixed;opacity:0;top:0;left:0;';
            document.body.appendChild(ta); ta.focus(); ta.select();
            try { document.execCommand('copy'); } catch(e) {}
            document.body.removeChild(ta);
            showToast('📋 Copié : ' + txt.slice(0, 60) + (txt.length > 60 ? '…' : ''));
          }
          if (navigator.clipboard && navigator.clipboard.writeText) {
            navigator.clipboard.writeText(txt).then(() => {
              showToast('📋 Copié : ' + txt.slice(0, 60) + (txt.length > 60 ? '…' : ''));
            }).catch(doCopy);
          } else { doCopy(); }
        });
      });

      loadingEl.style.display = 'none';
      tableWrap.style.visibility = 'visible';
    }

    [searchEl, typeEl, sectEl, tplEl, nbTplEl].forEach(el => {
      el.addEventListener('input', render);
      el.addEventListener('change', render);
    });
    mithrilEl.addEventListener('change', render);

    allSortTh.forEach(th => {
      th.addEventListener('click', e => {
        if (e.target.dataset.nodrag) return;
        const col = th.dataset.col;
        if (sortCol === col) sortAsc = !sortAsc; else { sortCol = col; sortAsc = true; }
        allSortTh.forEach(h => h.classList.remove('sa', 'sd'));
        th.classList.add(sortAsc ? 'sa' : 'sd');
        render();
      });
    });

    panel.querySelector('#mh-poss-toggle').addEventListener('click', () => {
      panel.classList.toggle('collapsed');
      panel.querySelector('#mh-poss-toggle').textContent = panel.classList.contains('collapsed') ? '+' : '−';
    });

    panel.querySelector('#mh-poss-table th[data-col="type"]').classList.add('sa');
    makeDraggable(panel, panel.querySelector('#mh-poss-header'));
    makeResizable(panel);
    makeColumnsResizable(panel.querySelector('#mh-poss-table'));

    render();
  }

  // ══════════════════════════════════════════════════════════════
  // DISPATCH selon la page
  // ══════════════════════════════════════════════════════════════

  window.addEventListener('load', () => {
    const path = location.pathname;
    if (path.includes('vue.php'))              initVue();
    else if (path.includes('possessions.php')) initPossessions();
  });

})();