Animesss Labyrinth Map

Интерактивная карта лабиринта (совместное прохождение)

スクリプトをインストールするには、Tampermonkey, GreasemonkeyViolentmonkey のような拡張機能のインストールが必要です。

You will need to install an extension such as Tampermonkey to install this script.

スクリプトをインストールするには、TampermonkeyViolentmonkey のような拡張機能のインストールが必要です。

スクリプトをインストールするには、TampermonkeyUserscripts のような拡張機能のインストールが必要です。

このスクリプトをインストールするには、Tampermonkeyなどの拡張機能をインストールする必要があります。

このスクリプトをインストールするには、ユーザースクリプト管理ツールの拡張機能をインストールする必要があります。

(ユーザースクリプト管理ツールは設定済みなのでインストール!)

このスタイルをインストールするには、Stylusなどの拡張機能をインストールする必要があります。

このスタイルをインストールするには、Stylus などの拡張機能をインストールする必要があります。

このスタイルをインストールするには、Stylus tなどの拡張機能をインストールする必要があります。

このスタイルをインストールするには、ユーザースタイル管理用の拡張機能をインストールする必要があります。

このスタイルをインストールするには、ユーザースタイル管理用の拡張機能をインストールする必要があります。

このスタイルをインストールするには、ユーザースタイル管理用の拡張機能をインストールする必要があります。

(ユーザースタイル管理ツールは設定済みなのでインストール!)

このスクリプトの質問や評価の投稿はこちら通報はこちらへお寄せください
// ==UserScript==
// @name         Animesss Labyrinth Map
// @namespace    https://animesss.com/
// @version      4.5.0
// @description  Интерактивная карта лабиринта (совместное прохождение)
// @author       VladLIO
// @license      MIT
// @match        https://animesss.com/labyrinth/
// @match        https://animesss.com/labyrinth/*
// @match        https://animesss.tv/labyrinth/
// @match        https://animesss.tv/labyrinth/*
// @grant        none
// @run-at       document-end
// ==/UserScript==

console.log('[LabMap] v3.6.0 старт');

(function () {
  'use strict';

  // ============================================================
  // КОНСТАНТИ
  // ============================================================
  var WORKER_URL     = 'https://labyrinth-map.lvladddd.workers.dev';
  var SESSION_ID     = 'sess_' + Date.now() + '_' + Math.random().toString(36).slice(2,8);
  var LS_KEY         = 'lm_cloud_map';
  var LS_REFRESH_KEY = 'lm_last_refresh';
  var CELL_SIZE      = 28;
  var ZOOM_IN        = 1.18;
  var ZOOM_OUT       = 0.85;
  var ZOOM_MIN       = 0.2;
  var ZOOM_MAX       = 5;

  // ============================================================
  // ДАНІ КІМНАТ
  // ============================================================
  var COLORS = {
    start:'#1a2a5e', reward:'#0d2e12', penalty:'#2e0a0a',
    quiz:'#0a1550', quiz_result:'#0a1550', puzzle:'#1e0d35',
    jackpot:'#2e2200', reward_card:'#002e28', shield_block:'#2e0a0a',
    empty:'#0a0c18', collection:'#001a38', trap_back:'#2e0505',
    mini_boss:'#2e1000', hard_boss:'#12003a', luck_altar:'#062006',
    luck_altar_result:'#062006', locked_chest:'#2e2000',
    locked_chest_result:'#2e2000', card_trader:'#001230',
    card_trader_result:'#001230', relic_room:'#160030',
    recovery_room:'#002e22', room_trap:'#1a0f00', room_gift:'#1a0f00', personal_mine:'#1a1200', personal_mine_created:'#1a1200', personal_mine_collect:'#1a1200', foreign_mine:'#1a0a00', fatigue:'#1a1000',
    spiritual_teleport:'#160030',
    mimic_chest:'#2a0030', mimic_chest_hit:'#2a0030', mimic_chest_killed:'#2a0030',
    mimic_chest_escape:'#2a0030', mimic_chest_reward:'#2a0030', mimic_chest_back:'#2a0030',
    guardian_user:'#5a3a00', guardian_club:'#1a0040', can_capture:'#003a1a',
    unknown:'#080a12',
    personal_mine:'#1a1200', personal_mine_created:'#1a1200', personal_mine_collect:'#1a1200',
    foreign_mine:'#1a0a00', fatigue:'#1a1000'
  };
  var ICONS = {
    start:'⚑', reward:'+', penalty:'!', quiz:'?', quiz_result:'?',
    puzzle:'🧩', jackpot:'★', reward_card:'🎴', shield_block:'!',
    empty:'·', collection:'🃏', trap_back:'↺', mini_boss:'👹',
    hard_boss:'💀', luck_altar:'🍀', luck_altar_result:'🍀',
    locked_chest:'🔒', locked_chest_result:'🎁', card_trader:'🛒',
    card_trader_result:'🛒', relic_room:'🔮', recovery_room:'✚',
    room_trap:'👤', room_gift:'👤', spiritual_teleport:'🌀',
    mimic_chest:'👅', mimic_chest_hit:'👅', mimic_chest_killed:'👅',
    mimic_chest_escape:'👅', mimic_chest_reward:'👅', mimic_chest_back:'👅',
    guardian_user:'👑', guardian_club:'🛡', can_capture:'⚐',
    personal_mine:'⛏', personal_mine_created:'⛏', personal_mine_collect:'⛏',
    foreign_mine:'⛏', fatigue:'💤'
  };
  var ICOLORS = {
    start:'#9fb4ff', reward:'#ffd66b', penalty:'#ff8b8b',
    quiz:'#8bb4ff', quiz_result:'#8bb4ff', puzzle:'#e0d0ff',
    jackpot:'#ffd66b', reward_card:'#aaffee', shield_block:'#ff8b8b',
    empty:'rgba(255,255,255,.3)', collection:'#8fd3ff', trap_back:'#ff8a8a',
    mini_boss:'#ffbb88', hard_boss:'#cc88ff', luck_altar:'#6ee786',
    luck_altar_result:'#6ee786', locked_chest:'#ffd66b',
    locked_chest_result:'#ffd66b', card_trader:'#8fd3ff',
    card_trader_result:'#8fd3ff', relic_room:'#c59cff',
    recovery_room:'#6ee786', room_trap:'#ffd66b', room_gift:'#ffd66b',
    spiritual_teleport:'#c59cff',
    mimic_chest:'#ff9ecb', mimic_chest_hit:'#ff9ecb', mimic_chest_killed:'#ff9ecb',
    mimic_chest_escape:'#ff9ecb', mimic_chest_reward:'#ff9ecb', mimic_chest_back:'#ff9ecb',
    guardian_user:'#ffd700', guardian_club:'#c59cff', can_capture:'#44ff88',
    personal_mine:'#ffd66b', personal_mine_created:'#6ee786', personal_mine_collect:'#6ee786',
    foreign_mine:'#ff8b8b', fatigue:'#ffd66b'
  };
  var NAMES = {
    start:'Старт', reward:'Награда', penalty:'Штраф',
    quiz:'Викторина', quiz_result:'Викторина', puzzle:'Пазл',
    jackpot:'Джекпот', reward_card:'Карта', shield_block:'Штраф (щит)',
    empty:'Пустая', collection:'Коллекция', trap_back:'Откат',
    mini_boss:'Мини-босс', hard_boss:'Хард-босс', luck_altar:'Алтарь',
    luck_altar_result:'Алтарь', locked_chest:'Сундук',
    locked_chest_result:'Сундук✓', card_trader:'Торговец',
    card_trader_result:'Торговец', relic_room:'Реликвия',
    recovery_room:'Лечение', room_trap:'Комната игрока',
    room_gift:'Комната игрока', spiritual_teleport:'Телепорт',
    mimic_chest:'Мимик', mimic_chest_hit:'Мимик⚔',
    mimic_chest_killed:'Мимик💀', mimic_chest_escape:'Мимик🏃',
    mimic_chest_reward:'Мимик+', mimic_chest_back:'Мимик↺',
    guardian_user:'Занято стражем', guardian_club:'Захвачено клубом',
    can_capture:'Можно захватить', unknown:'Неизвестная',
    personal_mine:'Шахта', personal_mine_created:'Шахта↑', personal_mine_collect:'Шахта✓',
    foreign_mine:'Чужая шахта', fatigue:'Усталость'
  };
  var DESCS = {
    start:'Начало пути лабиринта',
    reward:'Комната с ACC наградой',
    penalty:'Штрафная комната, теряешь ACC',
    shield_block:'Штраф был заблокирован щитом',
    quiz:'Вопрос на знание аниме', quiz_result:'Вопрос на знание аниме',
    puzzle:'Головоломка с выбором пути',
    jackpot:'Редкая удача — крупная награда',
    reward_card:'Можно получить карту аниме',
    empty:'Пустая комната, ничего не происходит',
    collection:'Проверка коллекции карт',
    trap_back:'Ловушка отката — возврат назад',
    mini_boss:'Мини-босс, нужно победить',
    hard_boss:'Сложный босс, нужна карта',
    luck_altar:'Алтарь удачи — случайный эффект',
    luck_altar_result:'Алтарь удачи — случайный эффект',
    locked_chest:'Закрытый сундук с наградой',
    locked_chest_result:'Сундук открыт, получена награда',
    card_trader:'Торговец продаёт карту за ACC',
    card_trader_result:'Торговец продаёт карту за ACC',
    relic_room:'Реликвия — особый предмет',
    recovery_room:'Восстановление — бонус к ходам',
    room_trap:'Здесь ловушка или подарок от другого игрока', personal_mine:'Персональная шахта — добывает ACC', personal_mine_created:'Шахта только что основана', personal_mine_collect:'Добыча собрана из шахты', foreign_mine:'Шахта другого игрока', fatigue:'Усталость от лабиринта',
    room_gift:'Здесь ловушка или подарок от другого игрока',
    spiritual_teleport:'Духовный телепорт — перемещение',
    mimic_chest:'Редкий монстр — сундук-мимик',
    mimic_chest_hit:'Редкий монстр — сундук-мимик',
    mimic_chest_killed:'Сундук-мимик был побеждён',
    mimic_chest_escape:'Игрок сбежал от мимика',
    mimic_chest_reward:'Получена награда от мимика',
    mimic_chest_back:'Мимик вернул игрока назад',
    guardian_user:'Здесь стоит личный страж игрока',
    guardian_club:'Здесь стоит страж клуба',
    can_capture:'Свободная комната — можно поставить стража',
    personal_mine:'Персональная шахта — добывает ACC', personal_mine_created:'Шахта только что основана', personal_mine_collect:'Добыча собрана из шахты',
    foreign_mine:'Шахта другого игрока', fatigue:'Усталость от лабиринта'
  };
  var LEGEND_ITEMS = [
    ['start','Старт'],['reward','Награда'],['penalty','Штраф'],
    ['quiz','Викторина'],['puzzle','Пазл'],['mini_boss','Мини-босс'],
    ['hard_boss','Хард-босс'],['reward_card','Карта'],['luck_altar','Алтарь'],
    ['card_trader','Торговец'],['relic_room','Реликвия'],['collection','Коллекция'],
    ['locked_chest','Сундук'],['room_trap','Игрок'],['mimic_chest','Мимик'],
    ['recovery_room','Лечение'],['spiritual_teleport','Телепорт'],['trap_back','Откат'],
    ['empty','Пусто'],['can_capture','Взять'],['guardian_user','Страж'],
    ['guardian_club','Страж клуба'],
    ['personal_mine','Шахта'],
    ['foreign_mine','Чужая шахта'],
    ['__variable__','🔀 Переменные']
  ];

  // ============================================================
  // СТАН
  // ============================================================
  var cloudMap       = {};
  var roomsCache     = null;
  var fmOpen         = false;
  var fmX = 0, fmY = 0, fmScale = 1;
  var drag           = false, dsx = 0, dsy = 0;
  var rafId          = null;
  var filterSet      = {};
  var showMyPath     = false;
  var ownershipCache = null;
  var highlightRoom  = null;

  var elMbody, elTooltip, elInfo, elSt, elFc, elCvs;

  // ============================================================
  // ХЕЛПЕРИ
  // ============================================================
  function clr(ev)      { return COLORS[ev]  || COLORS.unknown; }
  function ico(ev)      { return ICONS[ev]   || ''; }
  function iclr(ev)     { return ICOLORS[ev] || 'rgba(255,255,255,.7)'; }
  function roomName(ev) { return NAMES[ev]   || ev; }
  function roomDesc(ev) { return DESCS[ev]   || ''; }
  function mapData()    { return window.labyrinthData && window.labyrinthData.mapData; }
  function curPos()     { var d = mapData(); return (d && d.current) || {x:0, y:0}; }

  function formatCoords(x, y) {
    var depth = -y;
    var dir = x < 0 ? 'Западная комната ' + (-x) : x > 0 ? 'Восточная комната ' + x : 'Центральный путь';
    return 'Глубина ' + depth + ' \u2022 ' + dir;
  }

  function parseRoomText(text) {
    var t = text.replace(/[\u2022\u00b7]/g,' ').replace(/\s+/g,' ').trim();
    var dm = t.match(/\d+/g);
    if (!dm || !dm.length) return null;
    var depth = parseInt(dm[0]);
    var y = -depth, x = 0;
    var roomNum = dm[1] ? parseInt(dm[1]) : 1;
    if (t.indexOf('Западная') >= 0 || t.indexOf('западн') >= 0)  x = -roomNum;
    else if (t.indexOf('Восточная') >= 0 || t.indexOf('восточн') >= 0) x = roomNum;
    return {x:x, y:y};
  }

  function invalidateRoomsCache() { roomsCache = null; }

  function updateMapInfo() {
    var rooms = allRooms();
    var keys = Object.keys(rooms);
    var total = keys.length;
    var unknown = keys.filter(function(k){ return rooms[k].event==='unknown'; }).length;
    var d = mapData();
    var mine = d && d.steps ? d.steps.length - 1 : 0;
    var maxDepth = 0;
    keys.forEach(function(k){
      var p = k.split('_');
      var y = Math.abs(+p[1]||0);
      if (y > maxDepth) maxDepth = y;
    });
    var el = function(id){ return document.getElementById(id); };
    if (el('lm-mi-rooms-val'))   el('lm-mi-rooms-val').textContent   = total;
    if (el('lm-mi-depth-val'))   el('lm-mi-depth-val').textContent   = maxDepth;
    // Оновлюємо lm-st
    var st = el('lm-st');
    if (st) st.textContent = 'Облако: '+Object.keys(cloudMap).length+' | Всего: '+total;
  }

  // normalizeEvent — більше не змінює типи, залишаємо для сумісності
  function normalizeEvent(ev) {
    return ev;
  }

  // Змінні кімнати — можуть мати різні типи у різних гравців
  var VARIABLE_EVENTS = {
    room_trap:true, room_gift:true,
    reward:true, reward_card:true,
    locked_chest:true, locked_chest_result:true,
    luck_altar:true, luck_altar_result:true,
    card_trader:true, card_trader_result:true,
    mimic_chest:true, mimic_chest_hit:true, mimic_chest_killed:true,
    mimic_chest_escape:true, mimic_chest_reward:true, mimic_chest_back:true
  };

  function allRooms() {
    if (roomsCache) return roomsCache;
    var r = {}, k;
    for (k in cloudMap) {
      var room = cloudMap[k], ev = room.event;
      // Якщо в хмарі event = room_trap/gift (стара версія без v4) — переносимо в roomObject
      var roomObj = room.room_object || null;
      if (ev === 'room_trap' || ev === 'room_gift') {
        roomObj = ev;
        ev = 'unknown';
      }
      // Чистимо room_trap/gift/room_player з alt_types
      var altTypes = (room.alt_types || []).filter(function(t){
        return t !== 'room_trap' && t !== 'room_gift' && t !== 'room_player' && t !== 'shield_block';
      });
      // Якщо event=unknown але є altTypes — піднімаємо перший alt як основний тип
      if (ev === 'unknown' && altTypes.length > 0) {
        ev = altTypes[0];
        altTypes = altTypes.slice(1);
      }
      r[k] = {event:ev, guardian:room.guardian||null, visits:room.visits||1, altTypes:altTypes, roomObject:roomObj};
    }
    var d = mapData();
    if (d && d.steps) {
      for (var i = 0; i < d.steps.length; i++) {
        var s = d.steps[i], key = s.x+'_'+s.y;
        if (!r[key]) {
          var isRoomEv = (s.event==='room_trap'||s.event==='room_gift');
          r[key] = {event: isRoomEv ? 'unknown' : s.event, guardian:null, visits:1, altTypes:[], roomObject: isRoomEv ? s.event : null};
        } else {
          var cur = r[key];
          if (s.event==='room_trap'||s.event==='room_gift') {
            // Це івент поверх кімнати — зберігаємо окремо
            cur.roomObject = s.event;
          } else if (cur.event==='unknown' && s.event && s.event!=='unknown') {
            cur.event = s.event;
          } else if (cur.event !== s.event && s.event && s.event !== 'unknown') {
            // Якщо тип відрізняється і кімната змінна — додаємо в altTypes
            if (VARIABLE_EVENTS[cur.event] || VARIABLE_EVENTS[s.event]) {
              if (cur.altTypes.indexOf(s.event) === -1 && s.event !== cur.event) {
                cur.altTypes.push(s.event);
              }
            }
          }
        }
      }
    }
    roomsCache = r;
    return r;
  }

  // ============================================================
  // МЕРЕЖА
  // ============================================================
  function loadFromCache() {
    try {
      var raw = localStorage.getItem(LS_KEY);
      if (raw) {
        cloudMap = JSON.parse(raw);
        invalidateRoomsCache();
        drawMini(); if (fmOpen) drawFull();
      }
    } catch(e) { console.warn('[LabMap] кэш:', e); }
  }

  var _cloudLoading = false;
  function loadCloud(onDone) {
    if (_cloudLoading) { if (onDone) onDone(); return; }
    _cloudLoading = true;
    fetch(WORKER_URL+'/map')
      .then(function(r) { if (!r.ok) throw new Error('HTTP ' + r.status); return r.json(); })
      .then(function(d) {
        cloudMap = d.rooms || {};
        invalidateRoomsCache();
        try { localStorage.setItem(LS_KEY, JSON.stringify(cloudMap)); } catch(e) {}
        console.log('[LabMap] Облако:', Object.keys(cloudMap).length, 'комнат');
        _cloudLoading = false;
        updateMapInfo();
        drawMini(); if (fmOpen) drawFull();
        if (onDone) onDone();
      })
      .catch(function(e) { console.warn('[LabMap] loadCloud:', e); if (onDone) onDone(); });
  }

  var _stepsPushing = false;
  function pushSteps(steps, sid) {
    if (_stepsPushing) return Promise.resolve(null);
    _stepsPushing = true;
    return fetch(WORKER_URL+'/update', {
      method:'POST',
      headers:{'Content-Type':'application/json'},
      body:JSON.stringify({steps:steps, session_id:sid||SESSION_ID})
    })
    .then(function(r) { if (!r.ok) throw new Error('HTTP ' + r.status); return r.json(); })
    .then(function(d) { _stepsPushing = false; console.log('[LabMap] push', steps.length, 'total:', d.rooms); return d; })
    .catch(function(e) { _stepsPushing = false; console.warn('[LabMap] push err:', e); });
  }

  // ============================================================
  // OWNERSHIP
  // ============================================================
  function getCurrentRoomOwnership() {
    var cur = curPos();
    var owDiv = document.getElementById('labyrinthOwnership');
    if (!owDiv || window.getComputedStyle(owDiv).display==='none') return [];
    var captUser = document.getElementById('labyrinthCaptureUserBtn');
    var captClub = document.getElementById('labyrinthCaptureClubBtn');
    var tribute  = document.getElementById('labyrinthPayTributeBtn');
    function vis(el) { return el && window.getComputedStyle(el).display!=='none'; }
    var guardian = null;
    if (vis(tribute)) guardian = vis(captClub) ? 'guardian_club' : 'guardian_user';
    else if (vis(captUser)||vis(captClub)) guardian = 'can_capture';
    if (!guardian) return [];
    var d = mapData(), realEvent = 'unknown';
    if (d && d.steps) {
      for (var i = d.steps.length-1; i >= 0; i--) {
        if (d.steps[i].x===cur.x && d.steps[i].y===cur.y) { realEvent=d.steps[i].event; break; }
      }
    }
    return [{x:cur.x, y:cur.y, event:realEvent, guardian:guardian}];
  }

  function fetchOwnership(callback) {
    if (ownershipCache !== null) { callback(ownershipCache); return; }
    var username = window.visitor_name || '';
    if (!username) { callback([]); return; }
    var parser = new DOMParser(), rooms = [];
    fetch('/user/' + encodeURIComponent(username) + '/')
      .then(function(r) { return r.text(); })
      .then(function(html) {
        var doc = parser.parseFromString(html, 'text/html');
        doc.querySelectorAll('.user-labyrinth__item').forEach(function(item) {
          var el = item.querySelector('.user-labyrinth__room');
          if (el) { var c = parseRoomText(el.textContent.trim()); if (c) rooms.push({x:c.x,y:c.y,event:'unknown',guardian:'guardian_user'}); }
        });
        var clubLink = doc.querySelector('.usn__club-item-top a[href*="/clubs/"]');
        if (!clubLink) {
          var all = doc.querySelectorAll('a[href*="/clubs/"]');
          for (var i = 0; i < all.length; i++) {
            if (/\/clubs\/\d+\//.test(all[i].getAttribute('href')||'')) { clubLink=all[i]; break; }
          }
        }
        if (clubLink) {
          return fetch(clubLink.getAttribute('href')).then(function(r){return r.text();})
            .then(function(ch) {
              var cd = parser.parseFromString(ch, 'text/html');
              cd.querySelectorAll('.club-labyrinth__item').forEach(function(item) {
                var el = item.querySelector('.club-labyrinth__room');
                if (el) { var c = parseRoomText(el.textContent.trim()); if (c) rooms.push({x:c.x,y:c.y,event:'unknown',guardian:'guardian_club'}); }
              });
              ownershipCache = rooms; callback(rooms);
            });
        } else { ownershipCache = rooms; callback(rooms); }
      })
      .catch(function(e) { console.warn('[LabMap] fetchOwnership:', e); ownershipCache = []; callback([]); });
  }

  // ============================================================
  // МАЛЮВАННЯ
  // ============================================================
  function drawCell(ctx, px, py, cs, event, isCurrent, guardian, dimmed, hasAlt, roomObject) {
    var r = Math.max(1, cs * 0.12);
    ctx.fillStyle = isCurrent ? '#2a1800' : dimmed ? '#0d0f1a' : clr(event);
    ctx.beginPath();
    if (ctx.roundRect) ctx.roundRect(px+.5, py+.5, cs-1, cs-1, r);
    else ctx.rect(px+.5, py+.5, cs-1, cs-1);
    ctx.fill();
    ctx.strokeStyle = isCurrent ? 'rgba(255,214,107,.6)' : 'rgba(255,255,255,.07)';
    ctx.lineWidth = .5; ctx.stroke();

    if (isCurrent) {
      ctx.shadowColor='#ffd66b'; ctx.shadowBlur=cs*.8;
      ctx.strokeStyle='rgba(255,214,107,.9)'; ctx.lineWidth=1; ctx.stroke();
      ctx.shadowBlur=0;
    }
    if (cs < 8 || dimmed) return;

    var iconTxt = isCurrent ? '⚔' : ico(event);
    if (iconTxt) {
      ctx.font = Math.max(8, cs*.58) + 'px sans-serif';
      ctx.fillStyle = isCurrent ? '#ffd66b' : iclr(event);
      ctx.textAlign='center'; ctx.textBaseline='middle';
      ctx.fillText(iconTxt, px+cs/2, py+cs/2);
    }

    if (guardian && cs >= 8) {
      var gc = guardian==='guardian_user' ? '#ffd700' : guardian==='guardian_club' ? '#c59cff' : '#44ff88';
      ctx.strokeStyle=gc; ctx.lineWidth=Math.max(1,cs*.06);
      ctx.shadowColor=gc; ctx.shadowBlur=cs*.12; ctx.globalAlpha=.55;
      ctx.beginPath();
      if (ctx.roundRect) ctx.roundRect(px+.5,py+.5,cs-1,cs-1,r);
      else ctx.rect(px+.5,py+.5,cs-1,cs-1);
      ctx.stroke(); ctx.shadowBlur=0; ctx.globalAlpha=1;
      if (cs >= 14) {
        var gico = guardian==='guardian_user'?'👑':guardian==='guardian_club'?'🛡':'⚐';
        ctx.font=Math.max(7,cs*.3)+'px sans-serif';
        ctx.fillStyle=gc; ctx.textAlign='right'; ctx.textBaseline='top';
        ctx.fillText(gico, px+cs-1, py+1);
      }
    }

    // Рамка для ловушки/подарку в кімнаті
    if (roomObject && !dimmed && cs >= 6) {
      var oc = roomObject==='room_trap' ? '#ff6b6b' : '#ffd66b';
      ctx.strokeStyle = oc;
      ctx.lineWidth = Math.max(1, cs * 0.07);
      ctx.setLineDash([cs*0.15, cs*0.1]);
      ctx.shadowColor = oc; ctx.shadowBlur = cs * 0.1; ctx.globalAlpha = 0.7;
      ctx.beginPath();
      if (ctx.roundRect) ctx.roundRect(px+.5, py+.5, cs-1, cs-1, r);
      else ctx.rect(px+.5, py+.5, cs-1, cs-1);
      ctx.stroke();
      ctx.setLineDash([]); ctx.shadowBlur = 0; ctx.globalAlpha = 1;
    }

    // Значок в лівому нижньому куті якщо є альтернативні типи
    if (hasAlt && !dimmed && cs >= 10) {
      var asz = Math.max(6, cs * 0.28);
      ctx.font = 'bold ' + asz + 'px sans-serif';
      // Змінна кімната (reward↔reward_card тощо) — фіолетовий ~
      var isVarRoom = event !== 'unknown' && VARIABLE_EVENTS[event];
      ctx.fillStyle = isVarRoom ? 'rgba(192,132,252,0.95)' : 'rgba(255,214,107,0.9)';
      ctx.textAlign = 'left';
      ctx.textBaseline = 'bottom';
      ctx.shadowColor = isVarRoom ? 'rgba(160,80,255,0.6)' : 'rgba(255,160,0,0.6)';
      ctx.shadowBlur = cs * 0.15;
      ctx.fillText(isVarRoom ? '~' : '?', px + 2, py + cs - 1);
      ctx.shadowBlur = 0;
    }
  }

  function drawMini() {
    if (!elCvs) return;
    var wrap = document.getElementById('lm-cvs-wrap');
    if (!wrap) return;
    elCvs.width  = wrap.offsetWidth||400;
    elCvs.height = 200;
    var ctx = elCvs.getContext('2d');
    ctx.clearRect(0,0,elCvs.width,elCvs.height);
    var rooms = allRooms(), keys = Object.keys(rooms);
    if (!keys.length) {
      ctx.fillStyle='rgba(255,255,255,.3)'; ctx.font='12px sans-serif'; ctx.textAlign='center';
      ctx.fillText('Нет данных', elCvs.width/2, elCvs.height/2); return;
    }
    var mx=Infinity,Mx=-Infinity,my=Infinity,My=-Infinity;
    for (var i=0; i<keys.length; i++) {
      var p=keys[i].split('_'),x=+p[0],y=+p[1];
      if(x<mx)mx=x; if(x>Mx)Mx=x; if(y<my)my=y; if(y>My)My=y;
    }
    var pad=8,rx=Math.max(Mx-mx,1),ry=Math.max(My-my,1);
    var cs=Math.max(4,Math.min(Math.floor((elCvs.width-pad*2)/(rx+1)),Math.floor((elCvs.height-pad*2)/(ry+1)),22));
    var ox=pad+(elCvs.width-pad*2-(rx+1)*cs)/2, oy=pad+(elCvs.height-pad*2-(ry+1)*cs)/2;
    var cur=curPos();
    for (var j=0; j<keys.length; j++) {
      var q=keys[j].split('_'),rx2=+q[0],ry2=+q[1],room=rooms[keys[j]];
      drawCell(ctx,ox+(rx2-mx)*cs,oy+(ry2-my)*cs,cs,room.event,rx2===cur.x&&ry2===cur.y,room.guardian,false,room.altTypes&&room.altTypes.length>0,room.roomObject);
    }
    if (elSt) elSt.textContent='Облако: '+Object.keys(cloudMap).length+' | Всего: '+keys.length;
  }

  function openFull() { fmOpen=true; document.getElementById('lm-modal').classList.add('on'); requestAnimationFrame(function(){ if(fmOpen) drawFull(); }); }
  function closeFull() { fmOpen=false; document.getElementById('lm-modal').classList.remove('on'); }
  function centerOn(x,y) {
    if (!elMbody) return;
    var cs=CELL_SIZE*fmScale;
    fmX=elMbody.offsetWidth/2-x*cs; fmY=elMbody.offsetHeight/2-y*cs; drawFull();
  }

  function drawFull() {
    if (!elFc || !elMbody) return;
    elFc.width=elMbody.offsetWidth; elFc.height=elMbody.offsetHeight;
    var ctx=elFc.getContext('2d');
    ctx.clearRect(0,0,elFc.width,elFc.height);
    var rooms=allRooms(), keys=Object.keys(rooms);
    if (!keys.length) return;
    var cs=CELL_SIZE*fmScale, cur=curPos(), hasFilter=Object.keys(filterSet).length>0;

    ctx.strokeStyle='rgba(255,255,255,.04)'; ctx.lineWidth=.5;
    for (var gx=fmX%cs; gx<elFc.width; gx+=cs) { ctx.beginPath(); ctx.moveTo(gx,0); ctx.lineTo(gx,elFc.height); ctx.stroke(); }
    for (var gy=fmY%cs; gy<elFc.height; gy+=cs) { ctx.beginPath(); ctx.moveTo(0,gy); ctx.lineTo(elFc.width,gy); ctx.stroke(); }

    var myPathSet = {};
    if (showMyPath) {
      var dm2=mapData();
      if (dm2&&dm2.steps) { for (var si=0;si<dm2.steps.length;si++) { var ss=dm2.steps[si]; myPathSet[ss.x+'_'+ss.y]=true; } }
    }

    for (var i=0; i<keys.length; i++) {
      var p=keys[i].split('_'),x=+p[0],y=+p[1],room=rooms[keys[i]];
      var px=fmX+x*cs, py=fmY+y*cs;
      if (px<-cs||px>elFc.width+cs||py<-cs||py>elFc.height+cs) continue;
      var isCur=x===cur.x&&y===cur.y;
      var _altClean=(room.altTypes||[]).filter(function(t){return t!=='room_player'&&t!=='room_trap'&&t!=='room_gift';}),_isVar=_altClean.length>0&&room.event!=='unknown'&&VARIABLE_EVENTS[room.event],matchesFilter=filterSet[room.event]||(room.guardian&&filterSet[room.guardian])||(filterSet['__has_alt__']&&_altClean.length>0&&!_isVar)||(filterSet['__variable__']&&_isVar)||(filterSet['room_trap']&&room.roomObject&&(room.roomObject==='room_trap'||room.roomObject==='room_gift'));
      var dimmed=(hasFilter&&!matchesFilter&&!isCur)||(showMyPath&&!myPathSet[keys[i]]&&!isCur);
      drawCell(ctx,px,py,cs,room.event,isCur,room.guardian,dimmed,room.altTypes&&room.altTypes.length>0,room.roomObject);
    }

    if (showMyPath) {
      var dm3=mapData();
      if (dm3&&dm3.steps&&dm3.steps.length>1) {
        ctx.strokeStyle='rgba(255,214,107,.35)'; ctx.lineWidth=Math.max(1,cs*.18); ctx.lineJoin='round';
        ctx.beginPath();
        var f=dm3.steps[0]; ctx.moveTo(fmX+f.x*cs+cs/2,fmY+f.y*cs+cs/2);
        for (var li=1;li<dm3.steps.length;li++) { var ls=dm3.steps[li]; ctx.lineTo(fmX+ls.x*cs+cs/2,fmY+ls.y*cs+cs/2); }
        ctx.stroke();
      }
    }

    if (highlightRoom) {
      if (Date.now() < highlightRoom.until) {
        var hx=fmX+highlightRoom.x*cs, hy=fmY+highlightRoom.y*cs;
        if (hx>-cs&&hx<elFc.width+cs&&hy>-cs&&hy<elFc.height+cs) {
          var pulse=0.4+0.6*Math.abs(Math.sin(Date.now()/200));
          ctx.save();
          ctx.shadowColor='#ffd66b'; ctx.shadowBlur=cs*1.2*pulse;
          ctx.strokeStyle='rgba(255,220,80,'+(0.7+0.3*pulse)+')';
          ctx.lineWidth=Math.max(2,cs*.18);
          ctx.beginPath();
          var hr=Math.max(2,cs*.15);
          if(ctx.roundRect) ctx.roundRect(hx+.5,hy+.5,cs-1,cs-1,hr);
          else ctx.rect(hx+.5,hy+.5,cs-1,cs-1);
          ctx.stroke(); ctx.restore();
          requestAnimationFrame(drawFull);
        }
      } else { highlightRoom=null; }
    }

    if (elInfo) {
      var cx=Math.round((elFc.width/2-fmX)/cs),cy=Math.round((elFc.height/2-fmY)/cs);
      elInfo.textContent=formatCoords(cx,cy)+' | Облако: '+Object.keys(cloudMap).length+' | Всего: '+keys.length;
    }
  }

  // ============================================================
  // КНОПКА ТОП ЛАБИРИНТА
  // ============================================================
  function injectTopBtn() {
    var tabsBtns = document.querySelector('.ncard__tabs-btns');
    if (!tabsBtns || tabsBtns.querySelector('.lm-top-btn')) return;
    var topUrl = 'https://' + location.hostname + '/users_top/';
    var btn = document.createElement('a');
    btn.href = topUrl;
    btn.className = 'ncard__tabs-btn btn c-gap-10 lm-top-btn';
    btn.innerHTML = '<span class="fal fa-trophy"></span>Топ лабиринта';
    tabsBtns.appendChild(btn);
  }

  // ============================================================
  // ІСТОРІЯ — ПАРСИНГ ACCHISTORY
  // ============================================================
  // Ключові слова для розпізнавання лабіринтних записів з acchistory
  // Базується на реальних описах з сайту
  var HISTORY_KEYWORDS = [
    'лабиринт',        // "Штраф в лабиринте", "Награда за викторину в лабиринте" і т.д.
    'лабіринт',        // українська версія
    'лабиринта',       // "Покупка небесного кирпича в магазине лабиринта"
    'испытание дао',   // "Штраф за испытание Дао"
    'алтаря удачи',    // "Награда алтаря удачи"
    'небесный кирпич'  // "Покупка небесного кирпича"
  ];

  var HISTORY_ICONS = [
    { re:/мини.?босс/i,       icon:'👹', color:'#ffbb88' },
    { re:/хард.?босс/i,       icon:'💀', color:'#cc88ff' },
    { re:/викторин/i,          icon:'?',  color:'#8bb4ff' },
    { re:/джекпот/i,           icon:'★',  color:'#ffd66b' },
    { re:/сундук.мимик/i,     icon:'👅', color:'#ff9ecb' },
    { re:/сундук/i,            icon:'🔒', color:'#ffd66b' },
    { re:/реликвия/i,          icon:'🔮', color:'#c59cff' },
    { re:/телепорт/i,          icon:'🌀', color:'#c59cff' },
    { re:/торговец/i,          icon:'🛒', color:'#8fd3ff' },
    { re:/алтарь/i,            icon:'🍀', color:'#6ee786' },
    { re:/страж/i,                  icon:'👑', color:'#ffd700' },
    { re:/коллекци/i,               icon:'🃏', color:'#8fd3ff' },
    { re:/пазл/i,                   icon:'🧩', color:'#e0d0ff' },
    { re:/сундука/i,                icon:'🔒', color:'#ffd66b' },
    { re:/небесный кирпич/i,        icon:'🧱', color:'#8fd3ff' },
    { re:/испытание дао/i,          icon:'☯',  color:'#c59cff' },
    { re:/откат|ловушка/i,          icon:'↺',  color:'#ff8a8a' },
    { re:/штраф.*лабиринт/i,        icon:'!',  color:'#ff8b8b' },
    { re:/штраф.*дао/i,             icon:'!',  color:'#ff8b8b' },
    { re:/награда.*лабиринт/i,      icon:'+',  color:'#ffd66b' },
    { re:/награда.*босс/i,          icon:'+',  color:'#ffd66b' },
    { re:/лабиринт/i,               icon:'⚔', color:'rgba(255,255,255,.45)' }
  ];

  function histIcon(desc) {
    for (var i=0;i<HISTORY_ICONS.length;i++) {
      if (HISTORY_ICONS[i].re.test(desc)) return HISTORY_ICONS[i];
    }
    return { icon:'⚔', color:'rgba(255,255,255,.35)' };
  }

  function isLabRow(desc) {
    var d = desc.toLowerCase();
    for (var i=0;i<HISTORY_KEYWORDS.length;i++) { if (d.indexOf(HISTORY_KEYWORDS[i])!==-1) return true; }
    return false;
  }

  var LS_HIST_KEY = 'lm_hist_cache_v2';
  var LS_HIST_DATE_KEY = 'lm_hist_cache_date';

  // Завантажуємо кеш з localStorage при старті
  var histCache = {};
  var histTotal = null;
  var histCurPage = 1;
  try {
    var _raw = localStorage.getItem(LS_HIST_KEY);
    if (_raw) { histCache = JSON.parse(_raw); histTotal = Object.keys(histCache).length || null; }
  } catch(e) {}

  function parseHistoryHtml(html, pageNum) {
    var parser = new DOMParser();
    var doc = parser.parseFromString(html, 'text/html');
    var rows = [];

    // Реальна структура: .ncard-transactions__table tbody tr
    // Колонки: td[0]=Сумма, td[1]=Баланс, td[2]=Дата, td[3]=Описание
    var trs = doc.querySelectorAll('.ncard-transactions__table tbody tr');
    if (!trs.length) trs = doc.querySelectorAll('table tbody tr');

    var allAmounts = []; // { amt, isLost } — для підрахунку earned/lost
    trs.forEach(function(tr) {
      var tds = tr.querySelectorAll('td');
      if (tds.length < 4) return;

      var amount  = tds[0].textContent.trim();
      var balance = tds[1].textContent.trim();
      var date    = tds[2].textContent.trim();
      var descEl  = tds[3];
      var descClone = descEl.cloneNode(true);
      var cardDiv = descClone.querySelector('.acc-history__card');
      if (cardDiv) cardDiv.parentNode.removeChild(cardDiv);
      var desc = descClone.textContent.replace(/\s+/g,' ').trim();

      var amt = parseFloat(amount.replace(/[^0-9.\-]/g,''));
      if (isNaN(amt)) return;

      var dl = desc.toLowerCase();

      if (amt > 0) {
        // Заработано — всі плюси що відносяться до лабіринту
        if (isLabRow(desc)) {
          allAmounts.push({ amt: amt, isLost: false });
        }
      } else {
        // Потеряно — тільки штрафи, без покупок карт і оновлення вітрини
        var isShutraf = dl.indexOf('штраф') !== -1;
        var isPokupka = dl.indexOf('за покупку') !== -1;
        var isVitrina = dl.indexOf('витрин') !== -1;
        var isKirpich = dl.indexOf('небесный кирпич') !== -1;
        if (isShutraf && !isPokupka && !isVitrina) {
          allAmounts.push({ amt: amt, isLost: true });
        }
      }

      // В список показуємо тільки лабіринтні
      if (!desc || !isLabRow(desc)) return;
      rows.push({ amount: amount, balance: balance, date: date, desc: desc });
    });

    // Визначаємо кількість сторінок через .acc-history__page
    // Реальна пагінація: <a class="acc-history__page" href="/acchistory/page/2/">2</a>
    var totalPages = pageNum;
    var pLinks = doc.querySelectorAll('.acc-history__page');
    pLinks.forEach(function(a) {
      var n = parseInt(a.textContent.trim(), 10);
      if (!isNaN(n) && n > totalPages) totalPages = n;
    });

    return { rows: rows, totalPages: totalPages, allAmounts: allAmounts };
  }

  function loadHistPage(pageNum, onDone) {
    if (histCache[pageNum]) { onDone(histCache[pageNum]); return; }
    var url = pageNum > 1 ? '/acchistory/page/' + pageNum + '/' : '/acchistory/';
    fetch(url, { credentials: 'same-origin' })
      .then(function(r) { return r.text(); })
      .then(function(html) {
        var result = parseHistoryHtml(html, pageNum);
        histCache[pageNum] = result;
        if (histTotal === null || result.totalPages > histTotal) histTotal = result.totalPages;
        // Зберігаємо в localStorage
        try { localStorage.setItem(LS_HIST_KEY, JSON.stringify(histCache)); } catch(e) {}
        onDone(result);
      })
      .catch(function(e) {
        console.warn('[LabMap] history err:', e);
        onDone({ rows: [], totalPages: pageNum });
      });
  }

  // Статистика по личным комнатам (только свои steps)
  // Рахує загальний earned/lost ACC з усіх закешованих сторінок histCache
  function buildAccTotals() {
    var earned = 0, lost = 0;
    Object.keys(histCache).forEach(function(p) {
      var amounts = histCache[p].allAmounts || [];
      amounts.forEach(function(item) {
        if (item.isLost) lost += Math.abs(item.amt);
        else earned += item.amt;
      });
    });
    return { earned: earned, lost: lost };
  }

  function buildStepsStats() {
    var d = mapData();
    if (!d || !d.steps || !d.steps.length) return null;
    var counts = {};
    d.steps.forEach(function(s) { if (s.event==='room_trap'||s.event==='room_gift') return; counts[s.event] = (counts[s.event]||0) + 1; });
    // Убираем start из статистики
    delete counts['start'];
    var total = d.steps.length - 1; // минус стартовая комната
    // Количество откатов и потерянные комнаты (1 откат = -5 комнат)
    var trapCount = counts['trap_back'] || 0;
    var lostRooms = trapCount * 5;
    // Все типы отсортированы по количеству (без обрезания)
    var sorted = Object.keys(counts).sort(function(a,b){ return counts[b]-counts[a]; });
    return { total: total, counts: counts, top: sorted, trapCount: trapCount, lostRooms: lostRooms };
  }

  function renderHistPopup(result, page) {
    var pop = document.getElementById('lm-hpop');
    if (!pop) return;
    var listEl = pop.querySelector('.lm-hist-body');
    if (!listEl) return;

    var totalPages = histTotal || result.totalPages || 1;
    var rows = result.rows;

    // ---- Надпис актуальності ----
    var freshHtml = '';
    var lastDate = localStorage.getItem(LS_HIST_DATE_KEY);
    var pagesLoaded = Object.keys(histCache).length;
    if (lastDate && pagesLoaded > 0) {
      var d = new Date(lastDate);
      var pad = function(n){ return n < 10 ? '0'+n : ''+n; };
      var dateStr = pad(d.getDate())+'.'+pad(d.getMonth()+1)+'.'+d.getFullYear()+' '+pad(d.getHours())+':'+pad(d.getMinutes());
      freshHtml = '<div class="lm-hist-fresh">'+
        '🕐 Актуально на: <b>'+dateStr+'</b> &bull; Стр.: <b>'+pagesLoaded+' из '+(histTotal||totalPages||'?')+'</b>'+
        '</div>';
    } else {
      freshHtml = '<div class="lm-hist-fresh lm-hist-fresh--warn">'+
        '⚠ Нажмите 🔄 Обновить инфу для загрузки всех данных.'+
        '</div>';
    }

    // ---- Блок статистики ----
    var stats = buildStepsStats();
    var statsHtml = '';
    if (stats) {
      var itemsHtml = stats.top.map(function(ev) {
        var pct = Math.round(stats.counts[ev] / stats.total * 100);
        // Для empty iclr повертає rgba прозорий — використовуємо явний колір для бара
        var barColor = ev === 'empty' ? 'rgba(255,255,255,0.25)' : iclr(ev)+'44';
        return '<div class="lm-hs-item">' +
          '<span class="lm-hs-ico" style="background:'+clr(ev)+';color:'+iclr(ev)+'">'+ico(ev)+'</span>'+
          '<span class="lm-hs-name">'+roomName(ev)+'</span>'+
          '<div class="lm-hs-bar-wrap"><div class="lm-hs-bar" style="width:'+pct+'%;background:'+barColor+'"></div></div>'+
          '<span class="lm-hs-cnt">'+stats.counts[ev]+' <span class="lm-hs-pct">('+pct+'%)</span></span>'+
        '</div>';
      }).join('');
      var acc = buildAccTotals();
      var cardsHtml =
        '<div class="lm-sc-grid">'+
          '<div class="lm-sc-card">'+
            '<div class="lm-sc-label">Всего комнат</div>'+
            '<div class="lm-sc-val">'+stats.total+'</div>'+
            '<div class="lm-sc-sub">комнат пройдено</div>'+
          '</div>'+
          '<div class="lm-sc-card">'+
            '<div class="lm-sc-label">Откаты</div>'+
            '<div class="lm-sc-val lm-sc-bad">−'+stats.lostRooms+'</div>'+
            '<div class="lm-sc-sub">комнат потеряно</div>'+
          '</div>'+
          '<div class="lm-sc-card">'+
            '<div class="lm-sc-label">Заработано</div>'+
            '<div class="lm-sc-val lm-sc-good">+'+acc.earned+'</div>'+
            '<div class="lm-sc-sub">АСС за всё время</div>'+
          '</div>'+
          '<div class="lm-sc-card">'+
            '<div class="lm-sc-label">Потеряно</div>'+
            '<div class="lm-sc-val lm-sc-bad">−'+acc.lost+'</div>'+
            '<div class="lm-sc-sub">АСС на штрафах</div>'+
          '</div>'+
        '</div>';
      statsHtml =
        cardsHtml+
        '<div class="lm-hist-stats">'+
          '<div class="lm-hist-stats-ttl">📊 Статистика по комнатам</div>'+
          '<div class="lm-hist-stats-list">'+itemsHtml+'</div>'+
        '</div>';
    }

    // ---- Рядки з acchistory ----
    var rowsHtml = '';
    if (rows.length) {
      rowsHtml = rows.map(function(row) {
        var amt = parseFloat(row.amount.replace(/[^0-9.\-+]/g,''));
        var isPlus  = (!isNaN(amt) && amt > 0) || row.amount.indexOf('+') !== -1;
        var isMinus = (!isNaN(amt) && amt < 0) || row.amount.indexOf('-') !== -1;
        var amtCls  = isPlus ? 'lm-ha-plus' : isMinus ? 'lm-ha-minus' : '';
        var hi = histIcon(row.desc);
        return '<div class="lm-hr">'+
          '<div class="lm-hr-ico" style="background:'+hi.color+'18;border-color:'+hi.color+'44">'+
            '<span style="color:'+hi.color+'">'+hi.icon+'</span>'+
          '</div>'+
          '<div class="lm-hr-main">'+
            '<div class="lm-hr-desc">'+row.desc+'</div>'+
            '<div class="lm-hr-meta">'+row.date+(row.balance?' &bull; Баланс: <b>'+row.balance+'</b> ACC':'')+'</div>'+
          '</div>'+
          '<div class="lm-hr-amt '+amtCls+'">'+row.amount+(row.amount.indexOf('ACC')===-1?' ACC':'')+'</div>'+
        '</div>';
      }).join('');
    } else {
      rowsHtml = '<div class="lm-hist-empty">Записей лабиринта не найдено на странице '+page+'.</div>';
    }

    // ---- Пагінація ----
    var pagHtml = '';
    if (totalPages > 1) {
      // Показуємо до 5 номерів сторінок навколо поточної
      var start = Math.max(1, page-2), end = Math.min(totalPages, page+2);
      var nums = '';
      if (start > 1) nums += '<button class="lm-hp-btn" data-p="1">1</button>'+(start>2?'<span class="lm-hp-dots">…</span>':'');
      for (var pi=start; pi<=end; pi++) {
        nums += '<button class="lm-hp-btn'+(pi===page?' lm-hp-cur':'')+'" data-p="'+pi+'">'+pi+'</button>';
      }
      if (end < totalPages) nums += (end<totalPages-1?'<span class="lm-hp-dots">…</span>':'')+'<button class="lm-hp-btn" data-p="'+totalPages+'">'+totalPages+'</button>';
      pagHtml =
        '<div class="lm-hist-pag">'+
          '<button class="lm-hp-nav" id="lm-hprev" '+(page<=1?'disabled':'')+'>‹ Новее</button>'+
          nums+
          '<button class="lm-hp-nav" id="lm-hnext" '+(page>=totalPages?'disabled':'')+'>Старее ›</button>'+
        '</div>';
    }

    listEl.innerHTML = freshHtml + statsHtml +
      '<div class="lm-hist-rows-head">📜 Записи из лабиринта (страница '+page+')</div>'+
      '<div class="lm-hist-rows">' + rowsHtml + '</div>' +
      pagHtml;

    // Обробники пагінації
    listEl.querySelectorAll('.lm-hp-btn[data-p]').forEach(function(btn) {
      btn.addEventListener('click', function() {
        loadAndRenderHist(parseInt(this.dataset.p, 10));
      });
    });
    var prev = listEl.querySelector('#lm-hprev');
    var next = listEl.querySelector('#lm-hnext');
    if (prev) prev.addEventListener('click', function() { if (page>1) loadAndRenderHist(page-1); });
    if (next) next.addEventListener('click', function() { if (page<totalPages) loadAndRenderHist(page+1); });
  }

  function loadAndRenderHist(page) {
    histCurPage = page;
    var listEl = document.querySelector('#lm-hpop .lm-hist-body');
    if (listEl) listEl.innerHTML = '<div class="lm-spop-loading">Загрузка страницы '+page+'...</div>';
    loadHistPage(page, function(result) { renderHistPopup(result, page); });
  }

  // ============================================================
  // ПОБУДОВА UI
  // ============================================================
  function injectStyles() {
    var style = document.createElement('style');
    style.textContent = [
      '#lm-wrap{margin-bottom:14px;padding:14px 16px;border-radius:20px;background:rgba(255,255,255,.04);border:1px solid rgba(255,255,255,.08);color:#fff;}'+
      '#lm-map-info{display:grid;grid-template-columns:repeat(4,1fr);gap:10px;margin-top:12px;}'+
      '.lm-mi-card{display:flex;align-items:center;gap:10px;padding:12px;border-radius:14px;background:rgba(255,255,255,.05);border:1px solid rgba(255,255,255,.07);}'+
      '.lm-mi-ico{font-size:22px;flex-shrink:0;}'+
      '.lm-mi-val{font-size:20px;font-weight:800;line-height:1.1;color:#fff;}'+
      '.lm-mi-lbl{font-size:11px;color:rgba(255,255,255,.5);margin-top:2px;}'+
      '@media(max-width:640px){#lm-map-info{grid-template-columns:repeat(2,1fr);}}'+
      '#lm-map-info{align-items:center;}'+
      '#lm-btns2{display:flex;gap:8px;flex-wrap:wrap;margin-left:auto;}'+
      '#lm-btns2 button{padding:7px 14px;border:none;border-radius:10px;background:rgba(255,255,255,.08);color:rgba(255,255,255,.85);font-size:12px;font-weight:600;cursor:pointer;}'+
      '#lm-btns2 button:hover{background:rgba(255,255,255,.14);color:#fff;}',
      '#lm-hdr{display:flex;align-items:center;justify-content:space-between;margin-bottom:10px;}',
      '#lm-ttl{font-size:13px;font-weight:800;text-transform:uppercase;letter-spacing:.05em;opacity:.7;}',
      '#lm-btns{display:flex;gap:6px;}',
      '#lm-rbtn{padding:6px 14px;border:none;border-radius:10px;background:#4a5ac7;color:#fff;font-size:13px;font-weight:700;cursor:pointer;}',
      '#lm-rbtn:hover{background:#5b6dd8;}#lm-rbtn.loading{opacity:.6;cursor:not-allowed;}',
      '#lm-pbtn{padding:6px 14px;border:none;border-radius:10px;background:#b02a59;color:#fff;font-size:13px;font-weight:700;cursor:pointer;}',
      '#lm-cvs-wrap{background:#080a14;border-radius:14px;overflow:hidden;height:200px;}',
      '#lm-cvs{display:block;width:100%;height:100%;}',
      '#lm-st{margin-top:6px;font-size:11px;opacity:.5;text-align:right;}',
      '#lm-modal{display:none;position:fixed;inset:0;z-index:999999;background:rgba(0,0,0,.93);flex-direction:column;touch-action:none;overflow:hidden;}',
      '#lm-modal.on{display:flex;}',
      '#lm-mhdr{display:flex;flex-direction:column;gap:6px;padding:10px 16px;background:#13151f;border-bottom:1px solid rgba(255,255,255,.1);flex-shrink:0;}'+
      '#lm-mhdr-top{display:flex;align-items:center;justify-content:space-between;width:100%;}'+
      '#lm-mhdr-btns{display:flex;align-items:center;gap:6px;flex-wrap:wrap;margin-left:auto;}',
      '#lm-mttl{font-size:15px;font-weight:800;flex-grow:1;color:#fff;}',
      // Кнопки хедера
      '#lm-b0,#lm-b1,#lm-bf,#lm-bpath,#lm-bguard,#lm-bclub,#lm-bhist{padding:6px 12px;border-radius:8px;font-size:11px;font-weight:700;cursor:pointer;position:relative;}',
      '#lm-b0{border:none;background:rgba(74,90,199,.25);color:#9fb4ff;border:1px solid rgba(74,90,199,.4);}#lm-b0:hover{background:rgba(74,90,199,.4);color:#fff;}',
      '#lm-b1{border:none;background:rgba(249,168,37,.2);color:#ffd66b;border:1px solid rgba(249,168,37,.4);}#lm-b1:hover{background:rgba(249,168,37,.35);color:#fff;}',
      '#lm-bf{border:none;background:rgba(124,58,237,.2);color:#c59cff;border:1px solid rgba(124,58,237,.4);}#lm-bf:hover{background:rgba(124,58,237,.35);color:#fff;}#lm-bf.active{background:#7c3aed;color:#fff;border-color:#7c3aed;}',
      '#lm-bpath{border:none;background:rgba(5,150,105,.2);color:#6ee786;border:1px solid rgba(5,150,105,.4);}#lm-bpath:hover{background:rgba(5,150,105,.35);color:#fff;}#lm-bpath.active{background:#059669;color:#fff;border-color:#059669;}',
      '#lm-bguard{border:none;background:rgba(255,215,0,.15);color:#ffd700;border:1px solid rgba(255,215,0,.35);}#lm-bguard:hover{background:rgba(255,215,0,.28);color:#fff;}#lm-bguard.active{background:#b8860b;color:#fff;border-color:#b8860b;}',
      '#lm-bclub{border:none;background:rgba(197,156,255,.15);color:#c59cff;border:1px solid rgba(197,156,255,.35);}#lm-bclub:hover{background:rgba(197,156,255,.28);color:#fff;}#lm-bclub.active{background:#6b21a8;color:#fff;border-color:#6b21a8;}',
      '#lm-bhist{border:none;background:rgba(6,182,212,.15);color:#67e8f9;border:1px solid rgba(6,182,212,.35);}#lm-bhist:hover{background:rgba(6,182,212,.28);color:#fff;}#lm-bhist.active{background:#0e7490;color:#fff;border-color:#0e7490;}',
      '#lm-bc{width:36px;height:36px;border:none;border-radius:8px;padding:0;background:#c0392b;color:#fff;font-size:18px;font-weight:700;cursor:pointer;display:flex;align-items:center;justify-content:center;flex-shrink:0;}#lm-bc:hover{background:#e74c3c;}'+
      '#lm-brefresh{height:36px;padding:0 12px;gap:5px;border:none;border-radius:8px;background:rgba(74,90,199,.3);color:#9fb4ff;font-size:12px;font-weight:700;cursor:pointer;display:flex;align-items:center;justify-content:center;flex-shrink:0;border:1px solid rgba(74,90,199,.5);white-space:nowrap;}'+
      '#lm-brefresh:hover{background:rgba(74,90,199,.5);color:#fff;}'+
      '#lm-brefresh.loading{opacity:.6;cursor:not-allowed;}'+
      '#lm-brefresh.loading .lm-ref-ico{display:inline-block;animation:lm-spin 1s linear infinite;}'+
      '@keyframes lm-spin{from{transform:rotate(0deg)}to{transform:rotate(360deg)}}'+
      '#lm-hdr-right{display:flex;align-items:center;gap:6px;}',
      // Загальний стиль попапів
      '.lm-pop{display:none;position:absolute;top:calc(100% + 8px);left:0;z-index:100;background:#1a1d2a;border:1px solid rgba(255,255,255,.15);border-radius:14px;padding:0;box-shadow:0 8px 32px rgba(0,0,0,.6);overflow:hidden;max-width:calc(100vw - 16px);}',
      '.lm-pop.on{display:flex;flex-direction:column;}',
      '#lm-gpop,#lm-cpop{width:min(380px,calc(100vw - 24px));max-height:460px;}',
      '#lm-fpop{width:min(360px,calc(100vw - 24px));padding:12px;}',
      // Попап История
      '#lm-hpop{width:min(520px,calc(100vw - 24px));max-height:min(560px,80vh);}',
      '#lm-hpop .lm-pop-head{display:flex;align-items:center;justify-content:space-between;padding:10px 14px;border-bottom:1px solid rgba(255,255,255,.08);flex-shrink:0;}',
      '#lm-hpop .lm-pop-title{font-size:12px;font-weight:800;color:#fff;text-transform:uppercase;letter-spacing:.04em;}',
      '#lm-hpop .lm-pop-link{font-size:10px;color:rgba(255,255,255,.4);text-decoration:none;}#lm-hpop .lm-pop-link:hover{color:#67e8f9;}'+
      '@media(max-width:600px){.lm-pop.on{position:fixed;top:160px;left:8px;right:8px;width:auto;max-width:none;}}',
      '.lm-hist-body{overflow-y:auto;padding:10px;display:flex;flex-direction:column;gap:8px;}',
      // Блок статистики
      '.lm-sc-grid{display:grid;grid-template-columns:1fr 1fr;gap:8px;margin-bottom:10px;}'+
      '.lm-sc-card{padding:10px 12px;border-radius:12px;background:rgba(255,255,255,.05);border:1px solid rgba(255,255,255,.08);}'+
      '.lm-sc-label{font-size:10px;text-transform:uppercase;letter-spacing:.04em;color:rgba(255,255,255,.45);margin-bottom:4px;}'+
      '.lm-sc-val{font-size:20px;font-weight:800;color:#fff;line-height:1.1;margin-bottom:3px;}'+
      '.lm-sc-good{color:#6ee786;}'+
      '.lm-sc-bad{color:#ff8b8b;}'+
      '.lm-sc-sub{font-size:10px;color:rgba(255,255,255,.4);line-height:1.3;}'+
      '.lm-hist-stats{padding:10px;border-radius:12px;background:rgba(255,255,255,.04);border:1px solid rgba(255,255,255,.07);}',
      '.lm-hist-stats-ttl{font-size:11px;color:rgba(255,255,255,.55);text-transform:uppercase;letter-spacing:.04em;margin-bottom:8px;}',
      '.lm-hist-stats-ttl b{color:#fff;font-size:13px;}',
      '.lm-hist-stats-list{display:flex;flex-direction:column;gap:4px;}',
      '.lm-hs-item{display:flex;align-items:center;gap:8px;padding:5px 0;}',
      '.lm-hs-ico{width:24px;height:24px;border-radius:5px;display:flex;align-items:center;justify-content:center;font-size:13px;flex-shrink:0;}',
      '.lm-hs-name{font-size:12px;color:rgba(255,255,255,.85);width:90px;flex-shrink:0;white-space:nowrap;overflow:hidden;text-overflow:ellipsis;text-transform:none;}',
      '.lm-hs-bar-wrap{flex:1;min-width:30px;height:6px;border-radius:3px;background:rgba(255,255,255,.08);overflow:hidden;}',
      '.lm-hs-bar{height:6px;border-radius:3px;transition:width .3s;}',
      '.lm-hs-cnt{font-size:12px;font-weight:600;color:#fff;min-width:58px;text-align:right;white-space:nowrap;}'+
      '.lm-hs-pct{font-size:10px;font-weight:400;color:rgba(255,255,255,.45);}',
      // Заголовок рядків
      '.lm-hist-fresh{font-size:11px;color:rgba(255,255,255,.45);padding:6px 10px;border-radius:8px;background:rgba(255,255,255,.04);border:1px solid rgba(255,255,255,.07);margin-bottom:8px;line-height:1.5;}'+
      '.lm-hist-fresh b{color:rgba(255,255,255,.8);}'+
      '.lm-hist-fresh--warn{color:#ffd66b;border-color:rgba(255,214,107,.25);background:rgba(255,214,107,.07);}'+
      '.lm-hist-rows-head{font-size:11px;font-weight:700;color:rgba(255,255,255,.5);text-transform:uppercase;letter-spacing:.04em;padding:2px 0;}',
      // Рядки
      '.lm-hist-rows{display:flex;flex-direction:column;gap:5px;}',
      '.lm-hr{display:flex;align-items:center;gap:8px;padding:7px 9px;border-radius:10px;background:rgba(255,255,255,.04);border:1px solid rgba(255,255,255,.06);}',
      '.lm-hr-ico{width:32px;height:32px;border-radius:8px;display:flex;align-items:center;justify-content:center;font-size:15px;flex-shrink:0;border:1px solid transparent;}',
      '.lm-hr-main{flex:1;min-width:0;}',
      '.lm-hr-desc{font-size:12px;font-weight:700;color:#fff;white-space:nowrap;overflow:hidden;text-overflow:ellipsis;}',
      '.lm-hr-meta{font-size:10px;color:rgba(255,255,255,.42);margin-top:1px;}',
      '.lm-hr-meta b{color:rgba(255,255,255,.7);}',
      '.lm-hr-amt{font-size:12px;font-weight:800;flex-shrink:0;white-space:nowrap;}',
      '.lm-ha-plus{color:#6ee786;}.lm-ha-minus{color:#ff8b8b;}',
      '.lm-hist-empty{padding:18px;text-align:center;font-size:12px;color:rgba(255,255,255,.4);}',
      // Пагінація
      '.lm-hist-pag{display:flex;align-items:center;justify-content:center;gap:4px;padding:8px 0 2px;border-top:1px solid rgba(255,255,255,.07);flex-wrap:wrap;}',
      '.lm-hp-nav{padding:4px 10px;border:none;border-radius:7px;background:rgba(6,182,212,.2);color:#67e8f9;font-size:11px;font-weight:700;cursor:pointer;}',
      '.lm-hp-nav:disabled{opacity:.3;cursor:not-allowed;}',
      '.lm-hp-nav:hover:not(:disabled){background:rgba(6,182,212,.38);}',
      '.lm-hp-btn{width:28px;height:28px;border:none;border-radius:6px;background:rgba(255,255,255,.08);color:rgba(255,255,255,.7);font-size:11px;font-weight:700;cursor:pointer;}',
      '.lm-hp-btn:hover{background:rgba(255,255,255,.18);color:#fff;}',
      '.lm-hp-cur{background:rgba(6,182,212,.35)!important;color:#fff!important;}',
      '.lm-hp-dots{font-size:11px;color:rgba(255,255,255,.3);padding:0 3px;}',
      // Стражі попапи
      '.lm-spop-head{display:flex;align-items:center;justify-content:space-between;padding:10px 14px;border-bottom:1px solid rgba(255,255,255,.08);flex-shrink:0;}',
      '.lm-spop-title{font-size:12px;font-weight:800;color:#fff;text-transform:uppercase;letter-spacing:.04em;}',
      '.lm-spop-count{font-size:11px;color:rgba(255,255,255,.5);margin-left:6px;}',
      '.lm-spop-list{overflow-y:auto;padding:8px;}',
      '.lm-si{display:flex;align-items:center;gap:10px;padding:8px 10px;border-radius:10px;cursor:pointer;transition:background .12s;border:1px solid transparent;}',
      '.lm-si:hover{background:rgba(255,255,255,.06);border-color:rgba(255,255,255,.08);}',
      '.lm-si-img{width:38px;height:54px;border-radius:6px;object-fit:cover;flex-shrink:0;}',
      '.lm-si-info{min-width:0;flex:1;}',
      '.lm-si-name{font-size:12px;font-weight:700;color:#fff;white-space:nowrap;overflow:hidden;text-overflow:ellipsis;}',
      '.lm-si-room{font-size:11px;color:rgba(255,255,255,.65);margin-top:2px;}',
      '.lm-si-date{font-size:10px;color:rgba(255,255,255,.4);margin-top:2px;}',
      '.lm-si-nav{font-size:16px;color:rgba(255,255,255,.4);flex-shrink:0;}',
      '.lm-spop-loading{padding:20px;text-align:center;color:rgba(255,255,255,.5);font-size:12px;}',
      // Фільтр
      '#lm-fpop-head{display:flex;align-items:center;justify-content:space-between;margin-bottom:10px;}',
      '#lm-fpop-title{font-size:12px;font-weight:800;color:#fff;text-transform:uppercase;letter-spacing:.04em;}',
      '#lm-fpop-clear{padding:3px 8px;border:none;border-radius:6px;background:#b02a59;color:#fff;font-size:10px;font-weight:700;cursor:pointer;}',
      '#lm-fpop-grid{display:flex;flex-wrap:wrap;gap:5px;}',
      '.lm-fi{display:flex;align-items:center;gap:4px;font-size:11px;color:rgba(255,255,255,.7);cursor:pointer;padding:4px 8px;border-radius:8px;border:1px solid rgba(255,255,255,.08);background:rgba(255,255,255,.04);transition:all .12s;}',
      '.lm-fi:hover{background:rgba(255,255,255,.1);color:#fff;}',
      '.lm-fi.sel{border-color:rgba(255,255,255,.45);background:rgba(255,255,255,.16);color:#fff;font-weight:700;}'+
      '.lm-fi-alt{border-color:rgba(255,214,107,.25)!important;background:rgba(255,214,107,.08)!important;}'+
      '.lm-fi-alt.sel{border-color:rgba(255,214,107,.6)!important;background:rgba(255,214,107,.22)!important;}',
      '.lm-fid{width:14px;height:14px;border-radius:3px;flex-shrink:0;display:flex;align-items:center;justify-content:center;font-size:10px;}',
      // Карта
      '#lm-mbody{flex:1;overflow:hidden;position:relative;cursor:grab;background:#06080f;touch-action:none;}',
      '#lm-mbody.dr{cursor:grabbing;}#lm-fc{position:absolute;top:0;left:0;}',
      '#lm-info{position:absolute;bottom:12px;left:50%;transform:translateX(-50%);padding:5px 14px;border-radius:999px;background:rgba(0,0,0,.8);color:rgba(255,255,255,.65);font-size:11px;pointer-events:none;white-space:nowrap;}',
      '#lm-zbtns{position:absolute;right:12px;top:12px;display:flex;flex-direction:column;gap:5px;}',
      '.lm-zb{width:34px;height:34px;border:none;border-radius:8px;background:rgba(255,255,255,.12);color:#fff;font-size:18px;cursor:pointer;}',
      '#lm-legend-bar{display:flex;flex-wrap:wrap;gap:3px 8px;padding:6px 14px;background:#13151f;border-top:1px solid rgba(255,255,255,.08);flex-shrink:0;align-items:center;}',
      '.lm-lb-item{display:flex;align-items:center;gap:3px;font-size:10px;color:rgba(255,255,255,.6);}',
      '.lm-lb-ico{width:14px;height:14px;border-radius:3px;flex-shrink:0;display:flex;align-items:center;justify-content:center;font-size:9px;}',
      // Тулпіт
      '#lm-tooltip{position:fixed;z-index:9999999;pointer-events:none;background:rgba(10,12,24,.95);border:1px solid rgba(255,255,255,.15);border-radius:10px;padding:8px 12px;color:#fff;font-size:12px;line-height:1.5;display:none;min-width:210px;max-width:260px;box-shadow:0 4px 16px rgba(0,0,0,.5);}',
      '#lm-tooltip b{display:block;font-size:13px;margin-bottom:2px;}',
      '#lm-tooltip .tt-desc{color:rgba(255,255,255,.75);font-size:11px;margin-bottom:3px;}',
      '#lm-tooltip .tt-guard{color:#ffd700;font-size:11px;margin-bottom:2px;font-weight:600;}',
      '#lm-tooltip .tt-coord{color:rgba(255,255,255,.5);font-size:11px;white-space:nowrap;}'+
      '#lm-tooltip .tt-alt{color:#ffd66b;font-size:11px;margin-bottom:2px;}'+
      '#lm-tooltip .tt-obj{font-size:11px;font-weight:700;margin-bottom:3px;padding:2px 6px;border-radius:4px;}'+
      '#lm-tooltip .tt-trap{color:#ff8b8b;background:rgba(255,100,100,.12);}'+
      '#lm-tooltip .tt-gift{color:#ffd66b;background:rgba(255,214,107,.12);}'+
      '#lm-tooltip .tt-var{color:#c084fc;background:rgba(192,132,252,.1);}',
      // Підтвердження
      '#lm-confirm{display:none;position:fixed;inset:0;z-index:9999999;background:rgba(0,0,0,.7);align-items:center;justify-content:center;}',
      '#lm-confirm.on{display:flex;}',
      '#lm-cbox{background:#1a1d2a;border:1px solid rgba(255,255,255,.15);border-radius:18px;padding:24px 28px;max-width:340px;width:90%;color:#fff;}',
      '#lm-cbox h3{margin:0 0 10px;font-size:16px;font-weight:800;}',
      '#lm-cbox p{margin:0 0 8px;font-size:13px;color:rgba(255,255,255,.7);line-height:1.5;}',
      '#lm-cbox .warn{color:#ffd66b;font-size:12px;margin-bottom:8px;}',
      '#lm-cbox .time{font-size:12px;color:rgba(255,255,255,.5);margin-bottom:16px;}',
      '#lm-cbtns{display:flex;gap:10px;}',
      '#lm-cyes{flex:1;padding:10px;border:none;border-radius:10px;background:#4a5ac7;color:#fff;font-size:14px;font-weight:700;cursor:pointer;}',
      '#lm-cno{flex:1;padding:10px;border:none;border-radius:10px;background:rgba(255,255,255,.08);color:#fff;font-size:14px;font-weight:700;cursor:pointer;}'+
      '#lm-boost-confirm{display:none;position:fixed;inset:0;z-index:9999999;background:rgba(0,0,0,.75);align-items:center;justify-content:center;}'+
      '#lm-boost-confirm.on{display:flex;}'+
      '#lm-boost-box{background:linear-gradient(135deg,#1a1d2a,#0f1220);border:1px solid rgba(255,255,255,.15);border-radius:20px;padding:28px;max-width:320px;width:90%;color:#fff;text-align:center;}'+
      '#lm-boost-box .lm-boost-conf-ico{font-size:42px;margin-bottom:12px;line-height:1;}'+
      '#lm-boost-box .lm-boost-conf-title{font-size:18px;font-weight:800;margin-bottom:8px;}'+
      '#lm-boost-box .lm-boost-conf-desc{font-size:13px;color:rgba(255,255,255,.65);margin-bottom:20px;line-height:1.5;}'+
      '#lm-boost-box .lm-boost-conf-btns{display:flex;gap:10px;}'+
      '#lm-boost-conf-yes{flex:1;padding:12px;border:none;border-radius:12px;background:#4a5ac7;color:#fff;font-size:14px;font-weight:700;cursor:pointer;}'+
      '#lm-boost-conf-yes:hover{background:#5b6dd8;}'+
      '#lm-boost-conf-no{flex:1;padding:12px;border:none;border-radius:12px;background:rgba(255,255,255,.08);color:#fff;font-size:14px;font-weight:700;cursor:pointer;}'+
      '#lm-boost-conf-no:hover{background:rgba(255,255,255,.15);}'+
      '#lm-toast{position:fixed;top:14px;left:50%;transform:translateX(-50%);z-index:99999999;pointer-events:none;display:flex;flex-direction:column;align-items:center;gap:6px;min-width:280px;max-width:min(480px,90vw);}'+
      '.lm-toast-msg{width:100%;padding:10px 16px;border-radius:12px;background:linear-gradient(135deg,#1a2a4a,#0d1a30);border:1px solid rgba(74,144,226,.4);box-shadow:0 4px 20px rgba(0,0,0,.5);color:#fff;font-size:13px;font-weight:600;line-height:1.4;opacity:0;transform:translateY(-12px);transition:opacity .25s,transform .25s;}'+
      '.lm-toast-msg.on{opacity:1;transform:translateY(0);}'+
      '.lm-toast-msg b{color:#67e8f9;display:block;margin-bottom:2px;}'+
      '.lm-toast-bar{height:3px;border-radius:999px;background:rgba(255,255,255,.15);margin-top:4px;overflow:hidden;}'+
      '.lm-toast-bar-fill{height:3px;border-radius:999px;background:linear-gradient(90deg,#4a90e2,#67e8f9);width:0%;transition:width .4s ease;}'+
      '#lm-confirm2{display:none;position:fixed;inset:0;z-index:9999999;background:rgba(0,0,0,.7);align-items:center;justify-content:center;}'+
      '#lm-confirm2.on{display:flex;}'+
      '#lm-cbox2{background:#1a1d2a;border:1px solid rgba(255,255,255,.15);border-radius:18px;padding:24px 28px;max-width:340px;width:90%;color:#fff;}'+
      '#lm-cbox2 h3{margin:0 0 10px;font-size:16px;font-weight:800;}'+
      '#lm-cbox2 p{margin:0 0 8px;font-size:13px;color:rgba(255,255,255,.7);line-height:1.5;}'+
      '#lm-cbox2 .warn{color:#ffd66b;font-size:12px;margin-bottom:8px;}'+
      '#lm-cbox2 .time{font-size:12px;color:rgba(255,255,255,.5);margin-bottom:16px;}'+
      '#lm-cbtns2{display:flex;gap:10px;}'+
      '#lm-cyes2{flex:1;padding:10px;border:none;border-radius:10px;background:#4a5ac7;color:#fff;font-size:14px;font-weight:700;cursor:pointer;}'+
      '#lm-cno2{flex:1;padding:10px;border:none;border-radius:10px;background:rgba(255,255,255,.08);color:#fff;font-size:14px;font-weight:700;cursor:pointer;}',
    ].join('');
    document.head.appendChild(style);
  }

  function buildDOM() {
    // Міні-карта
    var wrap = document.createElement('div'); wrap.id='lm-wrap';
    wrap.innerHTML=
      '<div id="lm-hdr">'+
        '<div id="lm-ttl">🗺 Карта лабиринта</div>'+
        '<div id="lm-btns">'+
          '<button id="lm-rbtn">🔄 Обновить карту</button>'+
          '<button id="lm-pbtn">⛶ Полная карта</button>'+
        '</div>'+
      '</div>'+
      '<div id="lm-map-info">'+
        '<div class="lm-mi-card" id="lm-mi-rooms">'+
          '<div class="lm-mi-ico">🗺</div>'+
          '<div class="lm-mi-body">'+
            '<div class="lm-mi-val" id="lm-mi-rooms-val">—</div>'+
            '<div class="lm-mi-lbl">комнат в базе</div>'+
          '</div>'+
        '</div>'+
        '<div class="lm-mi-card" id="lm-mi-depth">'+
          '<div class="lm-mi-ico">⬇</div>'+
          '<div class="lm-mi-body">'+
            '<div class="lm-mi-val" id="lm-mi-depth-val">—</div>'+
            '<div class="lm-mi-lbl">макс. глубина</div>'+
          '</div>'+
        '</div>'+

      '</div>'+
      '<div id="lm-st">Загрузка...</div>';
    // Вставляємо перед ареною (зверху)
    var arenaEl = document.querySelector('.labyrinth__arena');
    var placed = false;
    if (arenaEl) {
      arenaEl.parentNode.insertBefore(wrap, arenaEl);
      placed = true;
    }
    if (!placed) {
      var fallbacks = ['.labyrinth__left', '.animesss-labyrinth'];
      for (var ti=0;ti<fallbacks.length;ti++) { var el=document.querySelector(fallbacks[ti]); if(el){el.parentNode.insertBefore(wrap,el.nextSibling);placed=true;break;} }
    }
    if (!placed) document.body.appendChild(wrap);

    // Модалка
    var modal = document.createElement('div'); modal.id='lm-modal';
    modal.innerHTML=
      '<div id="lm-mhdr">'+
        '<div id="lm-mhdr-top">'+
          '<div id="lm-mttl">🗺 Полная карта лабиринта</div>'+
          '<div id="lm-hdr-right">'+
            '<button id="lm-brefresh"><span class="lm-ref-ico">🔄</span> Обновить инфу</button>'+
            '<button id="lm-bc">×</button>'+
          '</div>'+
        '</div>'+
        '<div id="lm-mhdr-btns">'+
          '<button id="lm-b1">⚔ Моя позиция</button>'+
          '<button id="lm-b0">⚑ Старт</button>'+
          '<button id="lm-bguard">👑 Мои стражи</button>'+
          '<button id="lm-bclub">🛡 Стражи клуба</button>'+
          '<button id="lm-bhist">📜 История</button>'+
          '<button id="lm-bpath">👣 Мой путь</button>'+
          '<button id="lm-bf">⊞ Фильтр</button>'+
        '</div>'+
      '</div>'+
      '<div id="lm-mbody"><canvas id="lm-fc"></canvas>'+
        '<div id="lm-info">Тяни мышью • Скролл для зума</div>'+
        '<div id="lm-zbtns"><button class="lm-zb" id="lm-zi">+</button><button class="lm-zb" id="lm-zo">−</button></div>'+
      '</div>'+
      '<div id="lm-legend-bar">'+
        LEGEND_ITEMS.map(function(i){ return '<div class="lm-lb-item"><div class="lm-lb-ico" style="background:'+clr(i[0])+';color:'+iclr(i[0])+'">'+ico(i[0])+'</div><span>'+i[1]+'</span></div>'; }).join('')+
      '</div>';
    document.body.appendChild(modal);

    // Фільтр попап
    var fpop = document.createElement('div'); fpop.id='lm-fpop'; fpop.className='lm-pop';
    fpop.innerHTML='<div id="lm-fpop-head"><div id="lm-fpop-title">⊞ Фильтр комнат</div><button id="lm-fpop-clear">× Сбросить</button></div><div id="lm-fpop-grid">'+
      LEGEND_ITEMS.map(function(i){ return '<div class="lm-fi" data-ev="'+i[0]+'"><div class="lm-fid" style="background:'+clr(i[0])+';color:'+iclr(i[0])+'">'+ico(i[0])+'</div><span>'+i[1]+'</span></div>'; }).join('')+
      '<div class="lm-fi lm-fi-alt" data-ev="__has_alt__"><div class="lm-fid" style="background:#2a1a4a;color:#ffd66b">👁</div><span>Разные типы</span></div>'+
      '</div>';
    document.getElementById('lm-bf').appendChild(fpop);

    // Стражі попап
    var gpop = document.createElement('div'); gpop.id='lm-gpop'; gpop.className='lm-pop';
    gpop.innerHTML='<div class="lm-spop-head"><div><span class="lm-spop-title">👑 Мои стражи</span><span class="lm-spop-count" id="lm-gcount"></span></div></div><div class="lm-spop-list lm-spop-loading" id="lm-glist">Загрузка...</div>';
    document.getElementById('lm-bguard').appendChild(gpop);

    // Клуб попап
    var cpop = document.createElement('div'); cpop.id='lm-cpop'; cpop.className='lm-pop';
    cpop.innerHTML='<div class="lm-spop-head"><div><span class="lm-spop-title">🛡 Стражи клуба</span><span class="lm-spop-count" id="lm-ccount"></span></div></div><div class="lm-spop-list lm-spop-loading" id="lm-clist">Загрузка...</div>';
    document.getElementById('lm-bclub').appendChild(cpop);

    // История попап
    var hpop = document.createElement('div'); hpop.id='lm-hpop'; hpop.className='lm-pop';
    hpop.innerHTML=
      '<div class="lm-pop-head">'+
        '<span class="lm-pop-title">📜 История лабиринта</span>'+
        '<a class="lm-pop-link" href="/acchistory/" target="_blank">↗ Полная история</a>'+
      '</div>'+
      '<div class="lm-hist-body"><div class="lm-spop-loading">Загрузка...</div></div>';
    document.getElementById('lm-bhist').appendChild(hpop);

    // Тулпіт
    var tt = document.createElement('div'); tt.id='lm-tooltip'; document.body.appendChild(tt);

    // Підтвердження
    // Модалка підтвердження бустів
    var boostConf = document.createElement('div'); boostConf.id='lm-boost-confirm';
    boostConf.innerHTML=
      '<div id="lm-boost-box">'+
        '<div class="lm-boost-conf-ico" id="lm-boost-conf-ico">👁</div>'+
        '<div class="lm-boost-conf-title" id="lm-boost-conf-title">Активировать буст?</div>'+
        '<div class="lm-boost-conf-desc" id="lm-boost-conf-desc"></div>'+
        '<div class="lm-boost-conf-btns">'+
          '<button id="lm-boost-conf-no">Отмена</button>'+
          '<button id="lm-boost-conf-yes">Активировать</button>'+
        '</div>'+
      '</div>';
    document.body.appendChild(boostConf);

    var conf = document.createElement('div'); conf.id='lm-confirm';
    conf.innerHTML='<div id="lm-cbox"><h3>🔄 Обновить карту?</h3><p>Скрипт отправит ваши данные в облако и загрузит свежую карту.</p><p class="warn">⚠ Не обновляйте чаще чем раз в 10–15 минут.</p><div class="time" id="lm-ctime"></div><div id="lm-cbtns"><button id="lm-cno">Отмена</button><button id="lm-cyes">Обновить</button></div></div>';
    document.body.appendChild(conf);

    // Діалог для кнопки "Обновить всё" в повній карті
    var conf2 = document.createElement('div'); conf2.id='lm-confirm2';
    conf2.innerHTML='<div id="lm-cbox2"><h3>🔄 Обновить всю информацию?</h3><p>Скрипт отправит ваш маршрут и стражей в облако, затем загрузит все страницы истории для подсчёта АСС.</p><p class="warn">⚠ Не нажимайте чаще чем раз в 10–15 минут — это создаёт нагрузку на сервер.</p><div class="time" id="lm-ctime2"></div><div id="lm-cbtns2"><button id="lm-cno2">Отмена</button><button id="lm-cyes2">Обновить</button></div></div>';
    document.body.appendChild(conf2);

    // Тостер прогресу
    var toast = document.createElement('div'); toast.id='lm-toast';
    document.body.appendChild(toast);
  }

  function closeAllPopups() {
    ['lm-gpop','lm-cpop','lm-fpop','lm-hpop'].forEach(function(id) {
      var el = document.getElementById(id); if (el) el.classList.remove('on');
    });
    ['lm-bguard','lm-bclub','lm-bf','lm-bhist'].forEach(function(id) {
      var el = document.getElementById(id); if (el) el.classList.remove('active');
    });
  }

  // Коригує позицію попапу щоб не вилазив за правий край екрану
  function fixPopupPos(popEl) {
    // Скидаємо попередню корекцію
    popEl.style.left = '0';
    popEl.style.right = 'auto';
    var rect = popEl.getBoundingClientRect();
    var overflow = rect.right - (window.innerWidth - 8);
    if (overflow > 0) {
      popEl.style.left = (-overflow) + 'px';
    }
  }

  function bindEvents() {
    var conf = document.getElementById('lm-confirm');
    var fpop = document.getElementById('lm-fpop');

    elMbody   = document.getElementById('lm-mbody');
    elTooltip = document.getElementById('lm-tooltip');
    elInfo    = document.getElementById('lm-info');
    elSt      = document.getElementById('lm-st');
    elFc      = document.getElementById('lm-fc');
    elCvs     = document.getElementById('lm-cvs');

    // Refresh
    document.getElementById('lm-rbtn').addEventListener('click', function() {
      // Те саме що кнопка "Обновить инфу" в повноекранній карті
      var conf2 = document.getElementById('lm-confirm2');
      var timeEl2 = document.getElementById('lm-ctime2');
      if (conf2) {
        if (timeEl2) {
          var lastT2 = localStorage.getItem(LS_FULL_REFRESH_KEY);
          if (lastT2) { var diff2=Math.floor((Date.now()-parseInt(lastT2))/60000); timeEl2.textContent=diff2<1?'Последнее: только что':'Последнее: '+diff2+' мин. назад'; }
          else timeEl2.textContent='Ещё не обновляли.';
        }
        conf2.classList.add('on');
      }
    });
    document.getElementById('lm-cno').addEventListener('click', function(){ conf.classList.remove('on'); });
    conf.addEventListener('click', function(e){ if(e.target===conf) conf.classList.remove('on'); });
    document.getElementById('lm-cyes').addEventListener('click', function() {
      conf.classList.remove('on');
      localStorage.setItem(LS_REFRESH_KEY, String(Date.now()));
      doRefresh();
    });

    // Карта
    document.getElementById('lm-pbtn').addEventListener('click', function(){ openFull(); var c=curPos(); centerOn(c.x,c.y); });
    document.getElementById('lm-bc').addEventListener('click', closeFull);



    // Кнопка "Обновить всё" — показує діалог підтвердження
    var LS_FULL_REFRESH_KEY = 'lm_last_full_refresh';
    document.getElementById('lm-brefresh').addEventListener('click', function() {
      var conf2 = document.getElementById('lm-confirm2');
      var timeEl2 = document.getElementById('lm-ctime2');
      if (timeEl2) {
        var lastT = localStorage.getItem(LS_FULL_REFRESH_KEY);
        if (lastT) {
          var diff = Math.floor((Date.now() - parseInt(lastT)) / 60000);
          timeEl2.textContent = diff < 1 ? 'Последнее обновление: только что' : 'Последнее обновление: ' + diff + ' мин. назад';
        } else {
          timeEl2.textContent = 'Вы ещё не обновляли информацию.';
        }
      }
      conf2.classList.add('on');
    });

    // Обробники conf2
    var conf2El = document.getElementById('lm-confirm2');
    document.getElementById('lm-cno2').addEventListener('click', function() { conf2El.classList.remove('on'); });
    conf2El.addEventListener('click', function(e) { if (e.target === conf2El) conf2El.classList.remove('on'); });
    document.getElementById('lm-cyes2').addEventListener('click', function() {
      conf2El.classList.remove('on');
      localStorage.setItem(LS_FULL_REFRESH_KEY, String(Date.now()));
      doFullRefresh();
    });
    document.addEventListener('keydown', function(e){ if(e.key==='Escape') closeFull(); });
    document.getElementById('lm-b0').addEventListener('click', function(){
      if(!fmOpen) openFull(); fmScale=1;
      highlightRoom={x:0,y:0,until:Date.now()+2000}; centerOn(0,0); drawFull();
    });
    document.getElementById('lm-b1').addEventListener('click', function(){
      if(!fmOpen) openFull(); var c=curPos();
      highlightRoom={x:c.x,y:c.y,until:Date.now()+2000}; centerOn(c.x,c.y); drawFull();
    });
    document.getElementById('lm-zi').addEventListener('click', function(){ fmScale=Math.min(fmScale*ZOOM_IN,ZOOM_MAX); drawFull(); });
    document.getElementById('lm-zo').addEventListener('click', function(){ fmScale=Math.max(fmScale*ZOOM_OUT,ZOOM_MIN); drawFull(); });

    // Мой путь
    document.getElementById('lm-bpath').addEventListener('click', function(){
      showMyPath=!showMyPath;
      this.classList.toggle('active',showMyPath);
      this.textContent=showMyPath?'👣 Все игроки':'👣 Мой путь';
      drawFull();
    });

    // Фільтр
    document.getElementById('lm-bf').addEventListener('click', function(e){
      e.stopPropagation();
      var wasOpen = fpop.classList.contains('on');
      closeAllPopups();
      if (!wasOpen) { fpop.classList.add('on'); this.classList.add('active'); fixPopupPos(fpop); }
    });
    fpop.addEventListener('click', function(e){
      e.stopPropagation();
      var fi = e.target.closest('.lm-fi[data-ev]');
      if (fi) {
        var ev = fi.dataset.ev;
        if(filterSet[ev]) delete filterSet[ev]; else filterSet[ev]=true;
        fi.classList.toggle('sel',!!filterSet[ev]);
        var n=Object.keys(filterSet).length, bf=document.getElementById('lm-bf');
        bf.childNodes[0].textContent=n>0?'⊞ Фильтр ('+n+')':'⊞ Фильтр';
        drawFull(); return;
      }
      if (e.target.id==='lm-fpop-clear') {
        filterSet={};
        fpop.querySelectorAll('.lm-fi').forEach(function(x){ x.classList.remove('sel'); });
        var bf2=document.getElementById('lm-bf');
        bf2.childNodes[0].textContent='⊞ Фильтр'; bf2.classList.remove('active');
        fpop.classList.remove('on'); drawFull();
      }
    });

    // Стражі
    document.getElementById('lm-bguard').addEventListener('click', function(e){
      var pop = document.getElementById('lm-gpop'), wasOpen = pop.classList.contains('on');
      closeAllPopups();
      if (!wasOpen) { pop.classList.add('on'); this.classList.add('active'); loadGuardianPopup('user'); fixPopupPos(pop); }
    });
    document.getElementById('lm-bclub').addEventListener('click', function(e){
      var pop = document.getElementById('lm-cpop'), wasOpen = pop.classList.contains('on');
      closeAllPopups();
      if (!wasOpen) { pop.classList.add('on'); this.classList.add('active'); loadGuardianPopup('club'); fixPopupPos(pop); }
    });

    // История
    document.getElementById('lm-bhist').addEventListener('click', function(e){
      e.stopPropagation();
      var pop = document.getElementById('lm-hpop'), wasOpen = pop.classList.contains('on');
      closeAllPopups();
      if (!wasOpen) {
        pop.classList.add('on'); this.classList.add('active');
        // НЕ скидаємо histCache щоб зберегти дані після 🔄
        // Скидаємо тільки якщо кешу взагалі нема
        if (Object.keys(histCache).length === 0) {
          histTotal = null; histCurPage = 1;
          loadAndRenderHist(1);
        } else {
          renderHistPopup(histCache[histCurPage] || histCache[1] || {rows:[],totalPages:1}, histCurPage || 1);
        }
        fixPopupPos(pop);
      }
    });

    // Всі попапи блокують спливання кліку — щоб не закривались при кліку всередині
    ['lm-gpop','lm-cpop','lm-fpop','lm-hpop'].forEach(function(id) {
      var el = document.getElementById(id);
      if (el) el.addEventListener('click', function(e){ e.stopPropagation(); });
    });

    // Закриваємо при кліку поза попапами
    document.addEventListener('click', function(e){
      var popupIds = ['lm-bguard','lm-bclub','lm-bf','lm-bhist'];
      var inside = popupIds.some(function(id){
        var b = document.getElementById(id);
        return b && b.contains(e.target);
      });
      if (!inside) closeAllPopups();
    });

    // Drag
    elMbody.addEventListener('mousedown', function(e){ drag=true; dsx=e.clientX-fmX; dsy=e.clientY-fmY; elMbody.classList.add('dr'); });
    document.addEventListener('mousemove', function(e){
      if (!drag) return;
      fmX=e.clientX-dsx; fmY=e.clientY-dsy;
      if (rafId) return;
      rafId=requestAnimationFrame(function(){ rafId=null; drawFull(); });
    });
    document.addEventListener('mouseup', function(){ drag=false; elMbody.classList.remove('dr'); if(rafId){ cancelAnimationFrame(rafId); rafId=null; } });
    // Touch: drag (1 палець) + pinch-zoom (2 пальці)
    var pinchDist0 = 0, pinchScale0 = 1, pinchFmX0 = 0, pinchFmY0 = 0, pinchCx = 0, pinchCy = 0;

    function getTouchDist(e) {
      var dx = e.touches[0].clientX - e.touches[1].clientX;
      var dy = e.touches[0].clientY - e.touches[1].clientY;
      return Math.sqrt(dx*dx + dy*dy);
    }

    // passive:false на touchstart щоб preventDefault працював в touchmove
    elMbody.addEventListener('touchstart', function(e) {
      e.preventDefault();
      if (e.touches.length === 1) {
        pinchDist0 = 0;
        var t = e.touches[0];
        drag = true;
        dsx = t.clientX - fmX;
        dsy = t.clientY - fmY;
        tapStartX = t.clientX;
        tapStartY = t.clientY;
      } else if (e.touches.length === 2) {
        drag = false;
        pinchDist0 = getTouchDist(e);
        pinchScale0 = fmScale;
        pinchFmX0 = fmX;
        pinchFmY0 = fmY;
        var rect = elMbody.getBoundingClientRect();
        pinchCx = ((e.touches[0].clientX + e.touches[1].clientX) / 2) - rect.left;
        pinchCy = ((e.touches[0].clientY + e.touches[1].clientY) / 2) - rect.top;
      }
    }, { passive: false });

    elMbody.addEventListener('touchmove', function(e) {
      e.preventDefault();
      if (e.touches.length === 2 && pinchDist0 > 0) {
        var dist = getTouchDist(e);
        var ns = Math.max(ZOOM_MIN, Math.min(pinchScale0 * (dist / pinchDist0), ZOOM_MAX));
        // Масштабуємо відносно центру між пальцями
        fmX = pinchCx - (pinchCx - pinchFmX0) * (ns / pinchScale0);
        fmY = pinchCy - (pinchCy - pinchFmY0) * (ns / pinchScale0);
        fmScale = ns;
        if (rafId) return;
        rafId = requestAnimationFrame(function() { rafId = null; drawFull(); });
      } else if (e.touches.length === 1 && drag) {
        var t = e.touches[0];
        fmX = t.clientX - dsx;
        fmY = t.clientY - dsy;
        if (rafId) return;
        rafId = requestAnimationFrame(function() { rafId = null; drawFull(); });
      }
    }, { passive: false });

    elMbody.addEventListener('touchend', function(e) {
      e.preventDefault();
      pinchDist0 = 0;
      if (e.touches.length === 0) {
        drag = false;
      } else if (e.touches.length === 1) {
        var t = e.touches[0];
        dsx = t.clientX - fmX;
        dsy = t.clientY - fmY;
        drag = true;
      }
    }, { passive: false });

    // Zoom
    elMbody.addEventListener('wheel',function(e){
      e.preventDefault();
      var rect=elMbody.getBoundingClientRect(), mx=e.clientX-rect.left, my=e.clientY-rect.top;
      var factor=e.deltaY>0?ZOOM_OUT:ZOOM_IN, ns=Math.max(ZOOM_MIN,Math.min(fmScale*factor,ZOOM_MAX));
      fmX=mx-(mx-fmX)*(ns/fmScale); fmY=my-(my-fmY)*(ns/fmScale); fmScale=ns; drawFull();
    },{passive:false});

    // Тулпіт
    var ttRafId=null;
    elMbody.addEventListener('mousemove',function(e){
      if (drag) { elTooltip.style.display='none'; return; }
      if (ttRafId) return;
      ttRafId=requestAnimationFrame(function(){
        ttRafId=null;
        var rect=elMbody.getBoundingClientRect(), cs=CELL_SIZE*fmScale;
        var cx=Math.floor((e.clientX-rect.left-fmX)/cs), cy=Math.floor((e.clientY-rect.top-fmY)/cs);
        var rooms=allRooms(), room=rooms[cx+'_'+cy];
        if (room) {
          var cur2=curPos(), isCur=cx===cur2.x&&cy===cur2.y;
          var gLine=room.guardian?'<div class="tt-guard">'+(room.guardian==='guardian_user'?'👑 Личный страж':room.guardian==='guardian_club'?'🛡 Страж клуба':'⚐ Можно захватить')+'</div>':'';
          var mmAltLine = '';
          var mmAltFiltered = (room.altTypes||[]).filter(function(t){ return t!=='room_player'&&t!=='room_trap'&&t!=='room_gift'&&t!=='shield_block'; });
          if (mmAltFiltered.length) {
            var mmIsVar = room.event!=='unknown' && VARIABLE_EVENTS[room.event];
            if (mmIsVar) {
              var mmAll = [ico(room.event)+' '+roomName(room.event)].concat(mmAltFiltered.map(function(t){ return ico(t)+' '+roomName(t); }));
              mmAltLine = '<div class="tt-alt tt-var">🔀 Варианты: '+mmAll.join(' / ')+'</div>';
            } else {
              mmAltLine = '<div class="tt-alt">Также видели: '+mmAltFiltered.map(function(t){ return ico(t)+' '+roomName(t); }).join(', ')+'</div>';
            }
          }
          var mmObjLine = '';
          if (room.roomObject==='room_trap') mmObjLine='<div class="tt-obj tt-trap">⚠ Здесь установлена ловушка</div>';
          else if (room.roomObject==='room_gift') mmObjLine='<div class="tt-obj tt-gift">🎁 Здесь оставлен подарок</div>';
          elTooltip.innerHTML='<b>'+(isCur?'⚔ Ваша позиция':ico(room.event)+' '+roomName(room.event))+'</b>'+
            (!isCur&&roomDesc(room.event)?'<div class="tt-desc">'+roomDesc(room.event)+'</div>':'')+mmObjLine+mmAltLine+gLine+
            '<div class="tt-coord">'+formatCoords(cx,cy)+'</div>';
          elTooltip.style.display='block';
          var tx=e.clientX+14, ty=e.clientY-10;
          if(tx+270>window.innerWidth) tx=e.clientX-274;
          elTooltip.style.left=tx+'px'; elTooltip.style.top=ty+'px';
        } else elTooltip.style.display='none';
      });
    });
    elMbody.addEventListener('mouseleave',function(){ elTooltip.style.display='none'; });

    // Перехоплення кліків по бустах — показуємо підтвердження
    var BOOST_INFO = {
      'labyrinthBoostVisionBtn':  { ico:'👁',  name:'Всевидящее око',       desc:'Откроет тип следующей комнаты перед входом' },
      'labyrinthBoostShieldBtn':  { ico:'🛡',  name:'Щит',                  desc:'Защитит от следующего штрафа в лабиринте' },
      'labyrinthBoostRewardBtn':  { ico:'🎯',  name:'Гарантированная награда', desc:'Гарантирует награду в следующей комнате' },
      'labyrinthBoostBerserkBtn': { ico:'⚔',  name:'Режим берсерка',        desc:'Усиливает урон по боссам' }
    };
    var _pendingBoostBtn = null;

    // Перехоплення кнопок "Комната игрока"
    var _roomActionConfirmed = false;
    var trapBtn  = document.getElementById('labyrinthPlaceTrapBtn');
    var giftBtn  = document.getElementById('labyrinthPlaceGiftBtn');

    function interceptRoomAction(btn, ico, name, desc) {
      if (!btn) return;
      btn.addEventListener('click', function(e) {
        if (_roomActionConfirmed) return;
        e.stopImmediatePropagation();
        e.preventDefault();
        document.getElementById('lm-boost-conf-ico').textContent   = ico;
        document.getElementById('lm-boost-conf-title').textContent = name + '?';
        document.getElementById('lm-boost-conf-desc').textContent  = desc;
        _pendingBoostBtn = btn;
        _pendingIsRoomAction = true;
        document.getElementById('lm-boost-confirm').classList.add('on');
      }, true);
    }
    var _pendingIsRoomAction = false;

    interceptRoomAction(trapBtn,  '🕸', 'Оставить ловушку',  'Ловушка остановит первого игрока, который войдёт сюда. Стоимость — 1 ход.');
    interceptRoomAction(giftBtn,  '🎁', 'Оставить подарок',  'Подарок сразу достанется следующему игроку. Стоимость — 1 ход.');

    // Чужая шахта — ограбить / помочь
    var robBtn  = document.getElementById('labyrinthRobMineBtn');
    var helpBtn = document.getElementById('labyrinthHelpMineBtn');
    interceptRoomAction(robBtn,  '⚔', 'Ограбить шахту',  'Вы заберёте добычу из шахты другого игрока.');
    interceptRoomAction(helpBtn, '🤝', 'Помочь со сбором', 'Вы поможете другому игроку собрать добычу быстрее.');

    // Перехоплення кнопки "Купить +1 ход"
    var _buyConfirmed = false;
    var buyBtn = document.getElementById('labyrinthBuyAttemptBtn');
    if (buyBtn) {
      buyBtn.addEventListener('click', function(e) {
        if (_buyConfirmed) return;
        e.stopImmediatePropagation();
        e.preventDefault();
        // Читаємо поточний текст з блоку купівлі
        var buyText = document.getElementById('labyrinthBuyAttemptText');
        var buySubtext = document.getElementById('labyrinthBuyAttemptSubtext');
        var descText = (buyText ? buyText.textContent.trim() : '') +
                       (buySubtext ? ' ' + buySubtext.textContent.trim() : '');
        document.getElementById('lm-boost-conf-ico').textContent   = '👣';
        document.getElementById('lm-boost-conf-title').textContent = 'Купить +1 ход?';
        document.getElementById('lm-boost-conf-desc').textContent  = descText || 'Подтвердите покупку дополнительного хода';
        _pendingBoostBtn = buyBtn;
        _pendingIsBuy = true;
        document.getElementById('lm-boost-confirm').classList.add('on');
      }, true);
    }
    var _pendingIsBuy = false;

    var _boostConfirmed = false; // флаг підтвердженого кліку

    Object.keys(BOOST_INFO).forEach(function(btnId) {
      var btn = document.getElementById(btnId);
      if (!btn) return;
      btn.addEventListener('click', function(e) {
        if (_boostConfirmed) return; // підтверджений клік — пропускаємо до game.js
        if (btn.disabled) return;
        e.stopImmediatePropagation();
        e.preventDefault();
        var info = BOOST_INFO[btnId];
        document.getElementById('lm-boost-conf-ico').textContent   = info.ico;
        document.getElementById('lm-boost-conf-title').textContent = 'Активировать «' + info.name + '»?';
        document.getElementById('lm-boost-conf-desc').textContent  = info.desc;
        _pendingBoostBtn = btn;
        document.getElementById('lm-boost-confirm').classList.add('on');
      }, true);
    });

    document.getElementById('lm-boost-conf-no').addEventListener('click', function() {
      document.getElementById('lm-boost-confirm').classList.remove('on');
      _pendingBoostBtn = null; _pendingIsBuy = false; _pendingIsRoomAction = false;
    });
    document.getElementById('lm-boost-confirm').addEventListener('click', function(e) {
      if (e.target === this) { this.classList.remove('on'); _pendingBoostBtn = null; _pendingIsBuy = false; _pendingIsRoomAction = false; }
    });
    document.getElementById('lm-boost-conf-yes').addEventListener('click', function() {
      document.getElementById('lm-boost-confirm').classList.remove('on');
      if (_pendingBoostBtn) {
        var btn = _pendingBoostBtn;
        var isBuy = _pendingIsBuy;
        var isRoom = _pendingIsRoomAction;
        _pendingBoostBtn = null;
        _pendingIsBuy = false;
        _pendingIsRoomAction = false;
        if (isBuy) {
          _buyConfirmed = true;
        } else if (isRoom) {
          _roomActionConfirmed = true;
        } else {
          _boostConfirmed = true;
        }
        btn.click();
        _boostConfirmed = false;
        _buyConfirmed = false;
        _roomActionConfirmed = false;
      }
    });

    // Тап/клік по клітинці — показуємо тултіп
    var tooltipPinned = false;
    var tapStartX = 0, tapStartY = 0; // для відрізнення тапу від drag

    function showTooltipAt(clientX, clientY) {
      var rect = elMbody.getBoundingClientRect(), cs = CELL_SIZE * fmScale;
      var cx = Math.floor((clientX - rect.left - fmX) / cs);
      var cy = Math.floor((clientY - rect.top  - fmY) / cs);
      var rooms = allRooms(), room = rooms[cx+'_'+cy];
      if (room) {
        var cur2 = curPos(), isCur = cx===cur2.x && cy===cur2.y;
        var gLine = room.guardian ? '<div class="tt-guard">'+(room.guardian==='guardian_user'?'👑 Личный страж':room.guardian==='guardian_club'?'🛡 Страж клуба':'⚐ Можно захватить')+'</div>' : '';
        var altLine2 = '';
        var altFiltered2 = (room.altTypes||[]).filter(function(t){ return t!=='room_player'&&t!=='room_trap'&&t!=='room_gift'&&t!=='shield_block'; });
        if (altFiltered2.length) {
          var isVar2 = room.event!=='unknown' && VARIABLE_EVENTS[room.event];
          if (isVar2) {
            var allVar2 = [ico(room.event)+' '+roomName(room.event)].concat(altFiltered2.map(function(t){ return ico(t)+' '+roomName(t); }));
            altLine2 = '<div class="tt-alt tt-var">🔀 Варианты: '+allVar2.join(' / ')+'</div>';
          } else {
            altLine2 = '<div class="tt-alt">Также видели: '+altFiltered2.map(function(t){ return ico(t)+' '+roomName(t); }).join(', ')+'</div>';
          }
        }
        var objLine2 = '';
        if (room.roomObject==='room_trap') objLine2='<div class="tt-obj tt-trap">⚠ Здесь установлена ловушка</div>';
        else if (room.roomObject==='room_gift') objLine2='<div class="tt-obj tt-gift">🎁 Здесь оставлен подарок</div>';
        elTooltip.innerHTML = '<b>'+(isCur?'⚔ Ваша позиция':ico(room.event)+' '+roomName(room.event))+'</b>'+
          (!isCur&&roomDesc(room.event)?'<div class="tt-desc">'+roomDesc(room.event)+'</div>':'')+objLine2+altLine2+gLine+
          '<div class="tt-coord">'+formatCoords(cx,cy)+'</div>';
        var tw = Math.min(260, window.innerWidth - 16);
        var tx = clientX - tw / 2;
        if (tx < 8) tx = 8;
        if (tx + tw + 8 > window.innerWidth) tx = window.innerWidth - tw - 8;
        var ty = clientY - 130;
        if (ty < 60) ty = clientY + 20;
        elTooltip.style.left = tx + 'px';
        elTooltip.style.top  = ty + 'px';
        elTooltip.style.display = 'block';
        tooltipPinned = true;
        return true;
      }
      elTooltip.style.display = 'none';
      tooltipPinned = false;
      return false;
    }

    // ПК: клік мишкою
    elMbody.addEventListener('click', function(e) {
      showTooltipAt(e.clientX, e.clientY);
    });

    // Мобільний: touchend — якщо не було drag (пальець не рухався >10px)
    elMbody.addEventListener('touchend', function(e) {
      if (pinchDist0 > 0) return; // після pinch не показуємо
      var t = e.changedTouches[0];
      var dx = t.clientX - tapStartX, dy = t.clientY - tapStartY;
      if (Math.sqrt(dx*dx + dy*dy) < 10) {
        showTooltipAt(t.clientX, t.clientY);
      }
    }, { passive: false });



    // Закриваємо тултіп при кліку/тапі поза канвасом
    document.addEventListener('click', function(e) {
      if (tooltipPinned && !elMbody.contains(e.target)) {
        elTooltip.style.display = 'none';
        tooltipPinned = false;
      }
    });
  }

  // ============================================================
  // ТОСТЕР ПРОГРЕСУ
  // ============================================================
  var _toastEl = null;
  var _toastTimer = null;

  function showToast(title, sub, pct) {
    if (!_toastEl) _toastEl = document.getElementById('lm-toast');
    if (!_toastEl) return;
    var pctNum = (pct === undefined) ? -1 : pct;
    var barHtml = pctNum >= 0
      ? '<div class="lm-toast-bar"><div class="lm-toast-bar-fill" style="width:'+pctNum+'%"></div></div>'
      : '';
    _toastEl.innerHTML =
      '<div class="lm-toast-msg on">'+
        '<b>'+title+'</b>'+
        (sub ? sub : '')+
        barHtml+
      '</div>';
    if (_toastTimer) clearTimeout(_toastTimer);
  }

  function hideToast(delay) {
    if (_toastTimer) clearTimeout(_toastTimer);
    _toastTimer = setTimeout(function() {
      if (!_toastEl) _toastEl = document.getElementById('lm-toast');
      if (_toastEl) _toastEl.innerHTML = '';
    }, delay || 2500);
  }

  // Повне оновлення з кнопки 🔄 в повній карті
  function doFullRefresh() {
    var btn = document.getElementById('lm-brefresh');
    if (!btn || btn.classList.contains('loading')) return;
    btn.classList.add('loading');
    btn.disabled = true;

    var d = mapData();
    var steps = d && d.steps && d.steps.length ? d.steps : [];
    var domOwn = getCurrentRoomOwnership();

    // Крок 1: пушимо steps в хмару
    showToast('🗺 Отправка маршрута...', 'Шаг 1 из 4: загрузка вашего пути в облако', 10);
    var p = steps.length > 0 ? pushSteps(steps, SESSION_ID) : Promise.resolve();

    p.then(function() {
      // Крок 2: пушимо стражів
      showToast('👑 Отправка стражей...', 'Шаг 2 из 4: синхронизация стражей', 30);
      return new Promise(function(resolve) {
        // Скидаємо кеш стражів щоб перезавантажити
        ownershipCache = null;
        guardianDataCache = {user: null, club: null};
        fetchOwnership(function(hist) {
          var allOwn = domOwn.concat(hist);
          if (allOwn.length) pushSteps(allOwn, SESSION_ID+'_own').then(resolve).catch(resolve);
          else resolve();
        });
      });
    }).then(function() {
      // Крок 3: оновлюємо хмарну карту
      showToast('🌐 Обновление карты...', 'Шаг 3 из 4: загрузка облачной карты', 55);
      return new Promise(function(resolve) { loadCloud(resolve); });
    }).then(function() {
      // Крок 4: завантажуємо всі сторінки acchistory послідовно
      // з затримкою 600мс між запитами щоб не перевищити ліміти сервера
      histCache = {};
      histTotal = null;
      localStorage.setItem(LS_HIST_DATE_KEY, new Date().toISOString());

      function loadPageSeq(pageNum) {
        return new Promise(function(resolve) {
          setTimeout(function() {
            loadHistPage(pageNum, function(result) {
              var total = histTotal || result.totalPages || 1;
              var pct = Math.round(60 + (pageNum / Math.max(total,1)) * 35);
              showToast(
                '📜 Загрузка истории... стр. '+pageNum+' из '+total,
                'Шаг 4 из 4: подсчёт АСС (подождите)',
                pct
              );
              if (pageNum < total) {
                loadPageSeq(pageNum + 1).then(resolve);
              } else {
                resolve();
              }
            });
          }, pageNum === 1 ? 0 : 700);
        });
      }

      showToast('📜 Загрузка истории...', 'Шаг 4 из 4: сканирование всех страниц', 60);
      return loadPageSeq(1);

    }).then(function() {
      // Крок 5: оновлюємо попап якщо відкритий
      var pop = document.getElementById('lm-hpop');
      if (pop && pop.classList.contains('on')) {
        renderHistPopup(histCache[histCurPage] || {rows:[], totalPages:1}, histCurPage);
      }
      // Оновлюємо стражів якщо попапи відкриті
      var gpop = document.getElementById('lm-gpop');
      if (gpop && gpop.classList.contains('on')) loadGuardianPopup('user');
      var cpop = document.getElementById('lm-cpop');
      if (cpop && cpop.classList.contains('on')) loadGuardianPopup('club');

      showToast('✅ Обновление завершено!', 'Загружено стр.: '+Object.keys(histCache).length+' | Данные актуальны', 100);
      hideToast(3000);
      btn.classList.remove('loading');
      btn.disabled = false;
      console.log('[LabMap] Полное обновление завершено. Страниц истории:', Object.keys(histCache).length);
    }).catch(function(e) {
      showToast('❌ Ошибка обновления', e.message || 'Попробуйте ещё раз');
      hideToast(4000);
      console.warn('[LabMap] fullRefresh err:', e);
      btn.classList.remove('loading');
      btn.disabled = false;
    });
  }

  function doRefresh() {
    var btn=document.getElementById('lm-rbtn');
    btn.classList.add('loading'); btn.disabled=true;
    showToast('🗺 Отправка данных...', 'Загрузка маршрута в облако', 15);
    if (elSt) elSt.textContent='Отправка данных...';
    var d=mapData(), steps=d&&d.steps&&d.steps.length?d.steps:[];
    var domOwn=getCurrentRoomOwnership();
    var p=steps.length>0 ? pushSteps(steps,SESSION_ID) : Promise.resolve();
    p.then(function(){
      showToast('👑 Отправка стражей...', 'Синхронизация стражей', 45);
      if (elSt) elSt.textContent='Отправка стражей...';
      return new Promise(function(resolve){
        fetchOwnership(function(hist){
          var allOwn=domOwn.concat(hist);
          if(allOwn.length) pushSteps(allOwn,SESSION_ID+'_own').then(resolve).catch(resolve);
          else resolve();
        });
      });
    }).then(function(){
      showToast('🌐 Загрузка карты...', 'Обновление облачной карты', 75);
      if (elSt) elSt.textContent='Загрузка карты...';
      loadCloud(function(){
        btn.classList.remove('loading'); btn.disabled=false;
        showToast('✅ Карта обновлена!', 'Комнат в облаке: '+Object.keys(cloudMap).length, 100);
        hideToast(2500);
        if(elSt) elSt.textContent='Облако: '+Object.keys(cloudMap).length+' | Всего: '+Object.keys(allRooms()).length;
      });
    }).catch(function(e){
      console.warn('[LabMap] refresh err:',e);
      btn.classList.remove('loading'); btn.disabled=false;
    });
  }

  // Кэш стражей
  var guardianDataCache = {user:null, club:null};

  function loadGuardianPopup(type) {
    var listEl=document.getElementById(type==='user'?'lm-glist':'lm-clist');
    var countEl=document.getElementById(type==='user'?'lm-gcount':'lm-ccount');
    if(!listEl) return;
    if(guardianDataCache[type]){ renderGuardianList(type,guardianDataCache[type],listEl,countEl); return; }
    listEl.className='lm-spop-list lm-spop-loading';
    listEl.textContent='Загрузка...';
    var username=window.visitor_name||'';
    if(!username){ listEl.textContent='Не удалось определить пользователя.'; return; }
    var parser=new DOMParser();
    if(type==='user'){
      fetch('/user/'+encodeURIComponent(username)+'/')
      .then(function(r){ return r.text(); })
      .then(function(html){
        var doc=parser.parseFromString(html,'text/html');
        var items=[];
        doc.querySelectorAll('.user-labyrinth__item').forEach(function(item){
          var roomEl=item.querySelector('.user-labyrinth__room');
          var nameEl=item.querySelector('.user-labyrinth__guardian-name');
          var imgEl =item.querySelector('.user-labyrinth__guardian');
          var dateEl=item.querySelector('.user-labyrinth__date');
          if(!roomEl) return;
          var coords=parseRoomText(roomEl.textContent.trim());
          items.push({ name:nameEl?nameEl.textContent.trim().split(String.fromCharCode(10))[0].trim():'', img:imgEl?imgEl.src:'', room:roomEl.textContent.trim(), date:dateEl?dateEl.textContent.trim():'', x:coords?coords.x:0, y:coords?coords.y:0 });
        });
        guardianDataCache.user=items; renderGuardianList(type,items,listEl,countEl);
      }).catch(function(){ listEl.textContent='Ошибка загрузки.'; });
    } else {
      fetch('/user/'+encodeURIComponent(username)+'/')
      .then(function(r){ return r.text(); })
      .then(function(html){
        var doc=parser.parseFromString(html,'text/html');
        var clubLink=doc.querySelector('.usn__club-item-top a[href*="/clubs/"]');
        if(!clubLink){ var all=doc.querySelectorAll('a[href*="/clubs/"]'); for(var i=0;i<all.length;i++){ if(/\/clubs\/\d+\//.test(all[i].getAttribute('href')||'')){ clubLink=all[i]; break; } } }
        if(!clubLink){ listEl.textContent='Клуб не найден.'; return; }
        return fetch(clubLink.getAttribute('href')).then(function(r){ return r.text(); })
        .then(function(ch){
          var cd=parser.parseFromString(ch,'text/html');
          var items=[];
          cd.querySelectorAll('.club-labyrinth__item').forEach(function(item){
            var roomEl=item.querySelector('.club-labyrinth__room');
            var textEl=item.querySelector('.club-labyrinth__text b');
            var imgEl =item.querySelector('.club-labyrinth__avatar');
            var dateEl=item.querySelector('.club-labyrinth__date');
            if(!roomEl) return;
            var coords=parseRoomText(roomEl.textContent.trim());
            items.push({ name:textEl?textEl.textContent.trim():'', img:imgEl?imgEl.src:'', room:roomEl.textContent.trim(), date:dateEl?dateEl.textContent.trim():'', x:coords?coords.x:0, y:coords?coords.y:0 });
          });
          guardianDataCache.club=items; renderGuardianList(type,items,listEl,countEl);
        });
      }).catch(function(){ listEl.textContent='Ошибка загрузки.'; });
    }
  }

  function renderGuardianList(type, items, listEl, countEl) {
    if(countEl) countEl.textContent='('+items.length+')';
    if(!items.length){ listEl.className='lm-spop-list lm-spop-loading'; listEl.textContent='Нет данных.'; return; }
    listEl.className='lm-spop-list'; listEl.innerHTML='';
    items.forEach(function(it){
      var div=document.createElement('div'); div.className='lm-si'; div.title='Перейти на карте';
      div.innerHTML=(it.img?'<img class="lm-si-img" src="'+it.img+'" alt="">':'')+
        '<div class="lm-si-info"><div class="lm-si-name">'+it.name+'</div><div class="lm-si-room">'+it.room+'</div><div class="lm-si-date">'+it.date+'</div></div>'+
        '<div class="lm-si-nav">→</div>';
      div.addEventListener('click', function(){
        closeAllPopups(); fmScale=1;
        highlightRoom={x:it.x,y:it.y,until:Date.now()+2000};
        openFull(); centerOn(it.x,it.y);
      });
      listEl.appendChild(div);
    });
  }

  function buildUI() {
    if (document.getElementById('lm-wrap')) return;
    injectStyles(); buildDOM(); bindEvents();
    console.log('[LabMap] UI готово');
  }

  // ============================================================
  // INIT
  // ============================================================
  var LS_AUTO_SYNC_KEY = 'lm_last_auto_sync';
  var AUTO_SYNC_INTERVAL = 24 * 60 * 60 * 1000; // 24 години

  function autoSyncIfNeeded() {
    var lastSync = parseInt(localStorage.getItem(LS_AUTO_SYNC_KEY) || '0', 10);
    var now = Date.now();
    if (now - lastSync < AUTO_SYNC_INTERVAL) {
      // Ще не пройшло 24г — завантажуємо кеш і оновлюємо хмарну карту тихо
      loadFromCache();
      drawMini();
      // Завжди підтягуємо свіжу хмару при відкритті (без пушу даних)
      loadCloud(function() { drawMini(); if (fmOpen) drawFull(); });
      console.log('[LabMap] Авто-синк: ще не час, наступний через',
        Math.round((AUTO_SYNC_INTERVAL - (now - lastSync)) / 60000), 'хв');
      return;
    }

    // Пройшло 24г — робимо повний авто-синк тихо (без діалогу)
    console.log('[LabMap] Авто-синк: запускаємо...');
    setTimeout(function() {
      showToast('🔄 Авто-синхронизация...', 'Обновление данных (раз в 24ч)', 5);

      var d = mapData();
      var steps = d && d.steps && d.steps.length ? d.steps : [];
      var domOwn = getCurrentRoomOwnership();

      var p = steps.length > 0 ? pushSteps(steps, SESSION_ID) : Promise.resolve();
      p.then(function() {
        showToast('🔄 Авто-синхронизация...', 'Отправка стражей', 25);
        return new Promise(function(resolve) {
          ownershipCache = null;
          guardianDataCache = {user: null, club: null};
          fetchOwnership(function(hist) {
            var allOwn = domOwn.concat(hist);
            if (allOwn.length) pushSteps(allOwn, SESSION_ID+'_own').then(resolve).catch(resolve);
            else resolve();
          });
        });
      }).then(function() {
        showToast('🔄 Авто-синхронизация...', 'Загрузка облачной карты', 50);
        return new Promise(function(resolve) { loadCloud(resolve); });
      }).then(function() {
        // Завантажуємо всі сторінки історії
        histCache = {};
        histTotal = null;
        localStorage.setItem(LS_HIST_DATE_KEY, new Date().toISOString());

        function autoLoadPage(pageNum) {
          return new Promise(function(resolve) {
            setTimeout(function() {
              loadHistPage(pageNum, function(result) {
                var total = histTotal || result.totalPages || 1;
                var pct = Math.round(55 + (pageNum / Math.max(total,1)) * 40);
                showToast('🔄 Авто-синхронизация...', 'История: стр. '+pageNum+' из '+total, pct);
                if (pageNum < total) {
                  autoLoadPage(pageNum + 1).then(resolve);
                } else {
                  resolve();
                }
              });
            }, pageNum === 1 ? 0 : 700);
          });
        }

        return autoLoadPage(1);
      }).then(function() {
        localStorage.setItem(LS_AUTO_SYNC_KEY, String(Date.now()));
        showToast('✅ Авто-синхронизация завершена!', 'Следующая через 24 часа', 100);
        hideToast(3000);
        drawMini();
        console.log('[LabMap] Авто-синк завершено');
      }).catch(function(e) {
        console.warn('[LabMap] Авто-синк помилка:', e);
        hideToast(1000);
      });
    }, 3000); // затримка 3с після завантаження сторінки
  }

  function init() {
    console.log('[LabMap] init(), шагов:', mapData()&&mapData().steps?mapData().steps.length:0);
    buildUI(); drawMini(); injectTopBtn();
    autoSyncIfNeeded();

    var lastN = mapData()&&mapData().steps ? mapData().steps.length : 0;
    setInterval(function(){
      var cur=mapData(), n=cur&&cur.steps?cur.steps.length:0;
      if (n!==lastN) { lastN=n; invalidateRoomsCache(); drawMini(); if(fmOpen) drawFull(); }
    },1000);
  }

  function wait() {
    var MAX_TRIES=100, tries=0;
    function check() {
      tries++;
      var d2=mapData();
      if (d2&&d2.steps&&d2.steps.length>0) { console.log('[LabMap] Найдено! steps:',d2.steps.length); init(); }
      else if (tries<MAX_TRIES) setTimeout(check,300);
      else console.warn('[LabMap] Данные не найдено після',MAX_TRIES,'спроб');
    }
    check();
  }
  wait();
})();