Интерактивная карта лабиринта (совместное прохождение)
// ==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> • Стр.: <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?' • Баланс: <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();
})();