Интерактивная карта лабиринта (совместное прохождение)
// ==UserScript==
// @name Animesss Labyrinth Map
// @namespace https://animesss.com/
// @version 8.0
// @description Интерактивная карта лабиринта (совместное прохождение)
// @author VladLIO
// @license MIT
// @match https://animesss.com/*
// @match https://animesss.tv/*
// @grant none
// @connect labmap.duckdns.org
// @run-at document-end
// ==/UserScript==
var LM_IS_CLUB_PAGE = /^\/clubs\/\d+\/?$/.test(location.pathname || '');
var LM_IS_LABYRINTH_PAGE = /\/labyrinth(\/|$)/.test(location.pathname || '');
var _QSC_API_BASE = 'https://labmap.duckdns.org/qsc';
var _QSC_API_KEY = 'qzPub_animesss_2026';
var _labLastStepsCount = 0;
// Черга для /lab/events — щоб не спамити при швидкому клікані
var _qscEvQueue = [];
var _qscEvTimer2 = null;
function _qscProcessQueue() {
if (_qscEvQueue.length === 0) { _qscEvTimer2 = null; return; }
var item = _qscEvQueue.shift();
fetch(item.url, item.opts).catch(function() {});
if (_qscEvQueue.length > 0) {
_qscEvTimer2 = setTimeout(_qscProcessQueue, 400);
} else {
_qscEvTimer2 = null;
}
}
if (LM_IS_LABYRINTH_PAGE) {
(function() {
var origOpen = XMLHttpRequest.prototype.open;
var origSend = XMLHttpRequest.prototype.send;
XMLHttpRequest.prototype.open = function(method, url) {
this._lmUrl = url;
return origOpen.apply(this, arguments);
};
XMLHttpRequest.prototype.send = function(body) {
if (this._lmUrl && this._lmUrl.indexOf('mod=animesss_game') !== -1 &&
typeof body === 'string') {
if (body.indexOf('action=step') !== -1) {
var xhr = this;
xhr.addEventListener('load', function() {
try {
var resp = JSON.parse(xhr.responseText);
if (!resp) return;
if (!window.labyrinthData) window.labyrinthData = {};
if (resp.mapData) window.labyrinthData.mapData = resp.mapData;
if (typeof resp.fate_room !== 'undefined') window.labyrinthData.fateRoom = resp.fate_room || null;
if (typeof resp.echo_room !== 'undefined') window.labyrinthData.echoRoom = resp.echo_room || null;
if (typeof resp.can_place_room_object !== 'undefined') {
window.labyrinthData.canPlaceObject = resp.can_place_room_object;
window.labyrinthData.canPlaceRoomObject = resp.can_place_room_object;
}
var _username = window.visitor_name || '';
var _sv = (typeof GM_info !== 'undefined' && GM_info.script && GM_info.script.version) ? GM_info.script.version : '8.0';
var _sn = encodeURIComponent('Animesss Labyrinth Map');
try {
if (_username) {
var _evName = (typeof resp.event === 'string' && resp.event) ? resp.event : 'unknown';
var _d = new Date();
var _pad = function(n) { return String(n).padStart(2, '0'); };
var _stamp = _d.getFullYear() + '-' + _pad(_d.getMonth()+1) + '-' + _pad(_d.getDate()) + '_' + _pad(_d.getHours()) + '-' + _pad(_d.getMinutes()) + '-' + _pad(_d.getSeconds());
_qscEvQueue.push({
url: _QSC_API_BASE + '/lab/events?key=' + _QSC_API_KEY + '&sv=' + _sv + '&sn=' + _sn,
opts: {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
username: _username,
event_name: _evName,
filename: _username + '_' + _evName + '_' + _stamp + '.json',
content: xhr.responseText
})
}
});
if (!_qscEvTimer2) _qscProcessQueue();
}
} catch(_e) {}
try {
if (resp.mapData && resp.mapData.steps && resp.mapData.steps.length > 0) {
var _cnt = resp.mapData.steps.length;
if (_cnt !== _labLastStepsCount) {
_labLastStepsCount = _cnt;
var _steps = resp.mapData.steps;
var _delay = 2000 + Math.floor(Math.random() * 2000);
setTimeout(function() {
var _user = window.visitor_name || '';
if (!_user) return;
var _evSet = {};
for (var _i = 0; _i < _steps.length; _i++) if (_steps[_i].event) _evSet[_steps[_i].event] = true;
var _sorted = Object.keys(_evSet).sort();
var _schema = {};
for (var _j = 0; _j < _sorted.length; _j++) {
_schema[_sorted[_j]] = _j < 26
? String.fromCharCode(97 + _j)
: String.fromCharCode(97 + Math.floor((_j - 26) / 26)) + String.fromCharCode(97 + ((_j - 26) % 26));
}
var _lines = [
'// Labyrinth Map Exchange Format',
'// v1 — версия формата',
'// Ник — имя игрока',
'// # — схема сокращений: код=ивент',
'// Далее шаги: x,y,код_ивента',
'v1',
_user,
'#' + _sorted.map(function(ev) { return _schema[ev] + '=' + ev; }).join(',')
];
for (var _k = 0; _k < _steps.length; _k++) {
var _s = _steps[_k];
_lines.push(_s.x + ',' + _s.y + ',' + (_schema[_s.event] || _s.event));
}
fetch(_QSC_API_BASE + '/lab/push?key=' + _QSC_API_KEY + '&sv=' + _sv + '&sn=' + _sn, {
method: 'POST',
headers: { 'Content-Type': 'text/plain; charset=utf-8' },
body: _lines.join('\n')
}).catch(function() {});
}, _delay);
}
}
} catch(_e) {}
} catch(e) {}
if (window._lmApplyHistoricalPath) {
setTimeout(window._lmApplyHistoricalPath, 150);
}
if (window._lmUpdateObjBadge) {
setTimeout(window._lmUpdateObjBadge, 200);
}
if (window._lmApplyEmissionCells) {
setTimeout(window._lmApplyEmissionCells, 250);
}
});
}
}
return origSend.apply(this, arguments);
};
})();
}
(function() {
var _ls = {}, _ss = {};
var _rawLS = null, _rawSS = null;
try { _rawLS = window.localStorage; } catch(e) {}
try { _rawSS = window.sessionStorage; } catch(e) {}
function _testStorage(raw) {
if (!raw) return false;
try { raw.setItem('__lm__','1'); raw.removeItem('__lm__'); return true; } catch(e) { return false; }
}
var hasLS = _testStorage(_rawLS);
var hasSS = _testStorage(_rawSS);
window.lmLS = {
get: function(k) { try { return hasLS ? _rawLS.getItem(k) : (_ls[k]!==undefined?_ls[k]:null); } catch(e) { return _ls[k]!==undefined?_ls[k]:null; } },
set: function(k,v) { try { if(hasLS) _rawLS.setItem(k,v); else _ls[k]=String(v); } catch(e) { _ls[k]=String(v); } },
remove: function(k) { try { if(hasLS) _rawLS.removeItem(k); else delete _ls[k]; } catch(e) { delete _ls[k]; } }
};
window.lmSS = {
get: function(k) { try { return hasSS ? _rawSS.getItem(k) : (_ss[k]!==undefined?_ss[k]:null); } catch(e) { return _ss[k]!==undefined?_ss[k]:null; } },
set: function(k,v) { try { if(hasSS) _rawSS.setItem(k,v); else _ss[k]=String(v); } catch(e) { _ss[k]=String(v); } },
remove: function(k) { try { if(hasSS) _rawSS.removeItem(k); else delete _ss[k]; } catch(e) { delete _ss[k]; } }
};
})();
(function() {
function _gmFetch(url, opts) {
return new Promise(function(resolve, reject) {
var o = opts || {};
GM_xmlhttpRequest({
method: o.method || 'GET',
url: url,
headers: o.headers || {},
data: o.body || null,
timeout: 20000,
onload: function(r) {
var text = r.responseText || '';
resolve({
ok: r.status >= 200 && r.status < 300,
status: r.status,
text: function() { return Promise.resolve(text); },
json: function() { try { return Promise.resolve(JSON.parse(text)); } catch(e) { return Promise.reject(e); } }
});
},
onerror: function() { reject(new Error('network error')); },
ontimeout: function() { reject(new Error('timeout')); }
});
});
}
window.lmFetch = (typeof GM_xmlhttpRequest === 'function') ? _gmFetch : function(u,o){ return fetch(u,o); };
})();
(function () {
'use strict';
var IS_LABYRINTH = LM_IS_LABYRINTH_PAGE;
var IS_CLUB_PAGE = LM_IS_CLUB_PAGE;
var LM_USE_TEST_SERVER = false;
var LM_API_BASE = LM_USE_TEST_SERVER ? 'https://labmap.duckdns.org/test/api' : 'https://labmap.duckdns.org/api';
var LM_WS_URL = LM_USE_TEST_SERVER ? 'wss://labmap.duckdns.org/test-ws' : 'wss://labmap.duckdns.org/ws';
var LM_SCRIPT_VERSION = (typeof GM_info !== 'undefined' && GM_info.script && GM_info.script.version) ? GM_info.script.version : '8.0';
var WORKER_URL = 'https://labyrinth-map-v2.lvladddd.workers.dev';
var WRITE_SECRET = '20332011!!';
var VPS_URL = LM_API_BASE;
var _vpsAlive = true;
var LM_DEBUG = false;
var _lmLastInfo = { message: '', at: 0 };
function lmInfo(message) {
_lmLastInfo = { message: message, at: Date.now() };
if (elInfo) elInfo.textContent = message;
}
function lmWarn(message) {
if (!IS_CLUB_PAGE) console.warn('[Карта] ' + message);
}
function lmDebug() {
if (LM_DEBUG && !IS_CLUB_PAGE) console.log.apply(console, arguments);
}
var EMISSION_DURATION_SEC = 3600;
function getMoscowDate() {
return new Date().toLocaleDateString('en-CA', {timeZone: 'Europe/Moscow'});
}
function getMoscowTimeStr(tsMs) {
return new Date(tsMs).toLocaleTimeString('ru-RU', {hour: '2-digit', minute: '2-digit', timeZone: 'Europe/Moscow'});
}
function getAuth() {
var u = window.visitor_name || '';
var t = window.dle_login_hash || '';
return (u && t) ? {username: u, token: t} : null;
}
function parseDat(v) {
if (!v) return null;
if (typeof v === 'object') return v;
try { return JSON.parse(v); } catch(e) { return null; }
}
function findClubLink(doc) {
var link = doc.querySelector('.usn__club-item-top a[href*="/clubs/"]');
if (!link) {
var all = doc.querySelectorAll('a[href*="/clubs/"]');
for (var i = 0; i < all.length; i++) {
if (/\/clubs\/\d+\//.test(all[i].getAttribute('href') || '')) { link = all[i]; break; }
}
}
return link;
}
function parseUserGuardianItems(doc) {
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('\n')[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
});
});
return items;
}
function parseClubGuardianItems(doc) {
var items = [];
doc.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
});
});
return items;
}
function vpsAuthHeaders(withJson) {
var headers = {
'X-Player': window.visitor_name || '',
'X-Script-Version': LM_SCRIPT_VERSION,
'Authorization': 'Bearer ' + (window.dle_login_hash || '')
};
if (withJson) headers['Content-Type'] = 'application/json';
return headers;
}
var _lmUserSuffix = (function() {
var u = (window.visitor_name || '').toLowerCase().replace(/[^a-z0-9_]/g, '_').slice(0, 30);
return u ? '_u_' + u : '';
})();
var SESSION_ID = (function() {
var today = getMoscowDate();
var key = 'lm_session_v1' + _lmUserSuffix;
try {
var stored = JSON.parse(window.lmLS.get(key) || '{}');
if (stored.date === today && stored.id) return stored.id;
var newId = 'sess_' + Date.now() + '_' + Math.random().toString(36).slice(2,8);
window.lmLS.set(key, JSON.stringify({date: today, id: newId}));
return newId;
} catch(e) {
return 'sess_' + Date.now() + '_' + Math.random().toString(36).slice(2,8);
}
})();
var PUSH_SESSION_ID = SESSION_ID + '_page_' + Date.now().toString(36) + '_' + Math.random().toString(36).slice(2,7);
window._lmPushSid = PUSH_SESSION_ID;
var LS_KEY = 'lm_cloud_map';
var LS_STEPS_CACHE = 'lm_steps_cache_v1' + _lmUserSuffix;
var LS_STEPS_SENT = 'lm_steps_sent_v1' + _lmUserSuffix;
var LS_FLUSH_COUNT = 'lm_flush_count_v1' + _lmUserSuffix;
var LS_SERVER_SEQ = 'lm_server_step_seq_v2' + _lmUserSuffix;
function getCachedSteps() {
try { return JSON.parse(window.lmLS.get(LS_STEPS_CACHE) || '[]'); } catch(e) { return []; }
}
function getSentCount() {
return parseInt(window.lmLS.get(LS_STEPS_SENT) || '0', 10);
}
var CACHE_MAX = 500;
var CACHE_TRIM = 100;
var FULL_VIEW_MIN_CELL = 3.2;
var FULL_VIEW_MAX_CELL = 8.5;
var FULL_FOCUS_SCALE = 0.42;
function stepCacheKey(s, index) {
var ev = s && (s.event || s.ev) || 'unknown';
var idx = s && s.step_index != null ? s.step_index : index;
var sid = s && s.session_id ? s.session_id : SESSION_ID;
return sid + '|' + idx + '|' + (s && s.x) + '|' + (s && s.y) + '|' + ev;
}
function cacheSteps(steps) {
if (!steps || !steps.length) return;
try {
var stored = getCachedSteps();
var existingKeys = {};
var nextServerIndex = parseInt(window.lmLS.get(LS_SERVER_SEQ) || '0', 10);
if (!Number.isFinite(nextServerIndex) || nextServerIndex < 0) nextServerIndex = 0;
var migrated = false;
stored.forEach(function(s, i) {
existingKeys[s._lmCacheKey || stepCacheKey(s, s._lmIndex != null ? s._lmIndex : i)] = true;
if (!Number.isInteger(Number(s._lmServerStepIndex))) {
s._lmServerStepIndex = nextServerIndex++;
migrated = true;
} else {
nextServerIndex = Math.max(nextServerIndex, Number(s._lmServerStepIndex) + 1);
}
});
var added = 0;
steps.forEach(function(s, i) {
var key = stepCacheKey(s, i);
if (existingKeys[key]) return;
var serverIndex;
if (Number.isInteger(Number(s._lmServerStepIndex))) {
serverIndex = Number(s._lmServerStepIndex);
if (serverIndex >= nextServerIndex) nextServerIndex = serverIndex + 1;
} else {
serverIndex = nextServerIndex++;
s._lmServerStepIndex = serverIndex;
}
var copy = Object.assign({}, s, {
_lmIndex: i,
_lmCacheKey: key,
_lmServerStepIndex: serverIndex,
_lmCapturedAt: new Date().toISOString(),
session_id: s.session_id || PUSH_SESSION_ID,
step_index: serverIndex
});
stored.push(copy);
existingKeys[key] = true;
added++;
});
if (added === 0 && !migrated) return;
window.lmLS.set(LS_SERVER_SEQ, String(nextServerIndex));
if (stored.length > CACHE_MAX) {
var sentIdx = Math.min(getSentCount(), stored.length);
var trimCount = Math.min(Math.max(CACHE_TRIM, stored.length - CACHE_MAX), sentIdx);
if (trimCount > 0) {
stored = stored.slice(trimCount);
window.lmLS.set(LS_STEPS_SENT, String(Math.max(0, sentIdx - trimCount)));
}
}
window.lmLS.set(LS_STEPS_CACHE, JSON.stringify(stored));
} catch(e) { lmDebug('[Карта] Ошибка локального кеша', e); }
}
function collectProfile() {
var ld = window.labyrinthData || {};
var boostItems = (ld.boosts && ld.boosts.items) || {};
var mine = ld.personalMine || {};
var onLabyrinthPage = !!document.getElementById('labyrinthMap') ||
/^\/labyrinth\/?/.test(window.location.pathname || '');
var position = onLabyrinthPage && mapData() && mapData().current
? mapData().current
: null;
function textNumber(id) {
var el = document.getElementById(id);
if (!el) return null;
var value = parseInt(String(el.textContent || '').replace(/\s+/g, ''), 10);
return Number.isFinite(value) ? value : null;
}
var result = {
acc_balance: textNumber('labyrinthBank'),
total_steps: textNumber('labyrinthTotalSteps'),
today_steps: textNumber('labyrinthTodaySteps'),
max_steps: textNumber('labyrinthMaxSteps'),
trap_backs: (mapData()&&mapData().steps||[]).filter(function(s){return s.event==='trap_back';}).length,
boost_vision: boostItems.vision || 0,
boost_shield: boostItems.shield || 0,
boost_reward: boostItems.reward || 0,
boost_berserk:boostItems.berserk || 0,
mine_has: mine.has_mine ? 1 : 0,
mine_level: mine.has_mine ? Number(mine.level || 0) : null,
mine_acc: mine.has_mine ? Number(mine.pending_acc || mine.acc || 0) : null,
mine_cards: mine.has_mine ? Number(mine.pending_cards || mine.cards || 0) : null,
mine_cards_max: mine.has_mine ? Number(mine.max_cards || mine.cards_max || 0) : null,
mine_pct: mine.has_mine ? Number(mine.storage_progress || mine.storage_pct || 0) : null,
mine_acc_per_hour: mine.has_mine ? Number(mine.acc_per_hour || 0) : null,
mine_x: mine.has_mine && Number.isFinite(Number(mine.x ?? mine.mine_x)) ? Number(mine.x ?? mine.mine_x) : null,
mine_y: mine.has_mine && Number.isFinite(Number(mine.y ?? mine.mine_y)) ? Number(mine.y ?? mine.mine_y) : null,
mine_updated_at: mine.has_mine ? new Date().toISOString() : null,
mine_auto_enabled: window.lmLS.get('lm_mine_auto_enabled' + _lmUserSuffix) !== 'false' ? true : false,
mine_auto_threshold: parseInt(window.lmLS.get('lm_mine_threshold' + _lmUserSuffix) || '100', 10),
user_level: window.user_level || 0,
group_id: window.dle_group || 0,
stars: window.stars_user_rating || 0,
last_event: ld.lastEvent || '',
};
if (position && Number.isFinite(Number(position.x)) && Number.isFinite(Number(position.y))) {
result.current_x = Number(position.x);
result.current_y = Number(position.y);
}
Object.keys(result).forEach(function(key) {
if (result[key] == null || Number.isNaN(result[key])) delete result[key];
});
return result;
}
var _profileSyncTimer = null;
var _lastProfileJson = '';
function syncProfileToVPS(force) {
var auth = getAuth();
if (!auth) return Promise.resolve(false);
var username = auth.username, token = auth.token;
var profile = collectProfile();
var encoded = JSON.stringify(profile);
if (!force && encoded === _lastProfileJson) return Promise.resolve(true);
return window.lmFetch(VPS_URL + '/profile-sync', {
method: 'POST',
headers: vpsAuthHeaders(true),
body: encoded
}).then(function(r) {
if (!r.ok) throw new Error('HTTP ' + r.status);
return r.json();
}).then(function() {
_lastProfileJson = encoded;
return true;
}).catch(function(e) {
lmDebug('[Карта] profile-sync:', e);
return false;
});
}
function scheduleProfileSync() {
if (_profileSyncTimer) clearTimeout(_profileSyncTimer);
_profileSyncTimer = setTimeout(function() {
_profileSyncTimer = null;
syncProfileToVPS(false);
}, 1200);
}
var GUARDIAN_EVENTS = {guardian_user:1,guardian_club:1,can_capture:1};
var ROOM_OBJ_EVENTS = {room_trap:1,room_gift:1};
var VARIABLE_EV = {reward:1,reward_card:1,locked_chest:1,locked_chest_result:1,
luck_altar:1,luck_altar_result:1,card_trader:1,card_trader_result:1,
mimic_chest:1,mimic_chest_hit:1,mimic_chest_killed:1,mimic_chest_escape:1,
mimic_chest_reward:1,mimic_chest_back:1,personal_mine:1,personal_mine_created:1,
personal_mine_collect:1,foreign_mine:1,echo_room:1,echo_room_result:1,fate_room:1,fate_room_result:1,
room_trap:1,room_gift:1};
function filterNewSteps(steps) {
var result = [];
var seen = {};
for (var i = 0; i < steps.length; i++) {
var s = steps[i];
var key = s.x + '_' + s.y;
var ev = s.event || 'unknown';
var cloudRoom = cloudMap[key];
if (GUARDIAN_EVENTS[ev]) { result.push(s); continue; }
if (ROOM_OBJ_EVENTS[ev]) { result.push(s); continue; }
if (!cloudRoom) { if (!seen[key]) { seen[key]=1; result.push(s); } continue; }
if (cloudRoom.event === 'unknown' && ev !== 'unknown') {
if (!seen[key]) { seen[key]=1; result.push(s); } continue;
}
if (VARIABLE_EV[ev] && !seen[key]) {
seen[key]=1; result.push(s); continue;
}
if (ev !== 'unknown' && cloudRoom.event !== ev && !seen[key]) {
seen[key]=1; result.push(s);
}
}
return result;
}
var _flushInProgress = false;
function flushStepsToServer() {
if (_flushInProgress) return;
try {
var stored = getCachedSteps();
var sentCnt = Math.min(getSentCount(), stored.length);
var allNew = stored.slice(sentCnt);
if (!allNew.length) return;
_flushInProgress = true;
var toSend = filterNewSteps(allNew);
var auth = getAuth();
if (!auth) { _flushInProgress = false; return; }
var username = auth.username, pushToken = auth.token;
var vpsSteps = allNew.map(function(s) {
return {
x: s.x, y: s.y, ev: s.ev || s.event || 'empty',
acc_delta: s.acc_delta != null ? Number(s.acc_delta) : null,
acc_after: s.acc_after != null ? Number(s.acc_after) : null,
session_id: s.session_id || PUSH_SESSION_ID,
step_index: Number.isInteger(Number(s._lmServerStepIndex)) ? Number(s._lmServerStepIndex) : (s.step_index != null ? Number(s.step_index) : null),
ts: s._lmCapturedAt || s.ts || null,
dat: parseDat(s.dat || s._dat)
};
});
function sendPrimary() {
return window.lmFetch(VPS_URL + '/push', {
method: 'POST',
headers: vpsAuthHeaders(true),
body: JSON.stringify({ steps: vpsSteps })
}).then(function(r) {
if (!r.ok) throw new Error('HTTP ' + r.status);
return r.json();
}).then(function(d) {
_vpsAlive = true;
return { ok: true, reserve: false, saved: d && d.saved != null ? d.saved : allNew.length };
});
}
function sendReserve() {
return window.lmFetch(WORKER_URL + '/update', {
method: 'POST',
headers: {'Content-Type':'application/json','X-Write-Secret':WRITE_SECRET},
body: JSON.stringify({
steps: toSend, all_steps: allNew, session_id: SESSION_ID,
username: username, auth_token: pushToken, user_profile: collectProfile()
})
}).then(function(r) {
if (!r.ok) throw new Error('HTTP ' + r.status);
return r.json();
}).then(function(d) {
if (!d || !d.ok) throw new Error('bad response');
return { ok: true, reserve: true, saved: allNew.length };
});
}
sendPrimary().catch(function() {
_vpsAlive = false;
lmWarn('Резервный сервер');
return sendReserve();
}).then(function(result) {
var totalSent = sentCnt + allNew.length;
var currentStored = getCachedSteps();
var acknowledged = Math.min(totalSent, currentStored.length);
window.lmLS.set(LS_STEPS_SENT, String(acknowledged));
window.lmLS.set(LS_FLUSH_COUNT, String(acknowledged));
lmInfo('Карта обновлена');
}).catch(function() {
lmWarn('Карта сохранена локально');
}).then(function() {
_flushInProgress = false;
}).catch(function() {
_flushInProgress = false;
});
} catch(e) {
_flushInProgress = false;
lmWarn('Ошибка подготовки данных к отправке');
lmDebug(e);
}
}
function syncCacheWithSession() {
var d = mapData();
if (!d || !d.steps || !d.steps.length) return;
cacheSteps(d.steps);
}
var LS_REFRESH_KEY = 'lm_last_refresh';
var LS_FULL_REFRESH_KEY = 'lm_last_full_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 DETAIL_FIELD = {q:'q',o:'o',c:'c',p:'p',a:'a',bl:'bl',bh:'bh',r:'r',ml:'ml',ma:'ma',mc:'mc',eu:'eu',ar:'ar',cr:'cr'};
function getEmissionStepDat() {
var ld = window.labyrinthData || {};
if (!ld.emission || !ld.emission.last_start_at) return null;
var d = {
emission: {
active: !!ld.emission.active,
last_start_at: ld.emission.last_start_at,
last_end_at: ld.emission.last_end_at || 0,
cooldown_left: ld.emission.cooldown_left || 0
}
};
if (ld.emission.active) d.during_emission = true;
return d;
}
function scrapeRoomDetails(eventType) {
function getText(sel) {
var el = document.querySelector(sel);
return el ? el.textContent.trim().substring(0, 200) : null;
}
function getNum(sel) {
var t = getText(sel);
return t ? (parseFloat(t.replace(/[^0-9.\-]/g,'')) || null) : null;
}
var d = {};
try {
switch (eventType) {
case 'quiz': case 'quiz_result':
var q = getText('#labyrinthQuizQuestion,.labyrinth-quiz__question,.labyrinth__quiz-question,[class*="quiz"][class*="question"]');
if (q) d[DETAIL_FIELD.q] = q;
var opts = [];
document.querySelectorAll('#labyrinthQuiz .button,.labyrinth-quiz__answer,.labyrinth__quiz-answer,[class*="quiz"][class*="answer"]')
.forEach(function(el){ var t=el.textContent.trim(); if(t) opts.push(t.substring(0,100)); });
if (opts.length) d[DETAIL_FIELD.o] = opts;
break;
case 'mini_boss': case 'hard_boss':
var c = getText('#labyrinthHardBossNeedName,#labyrinthMiniBossName,#labyrinthHardBossName,.labyrinth__hardboss-card-name,.character-name,.labyrinth-boss__card-name,[class*="boss"][class*="card"]');
if (c) d[DETAIL_FIELD.c] = c.substring(0,80);
var hp = getNum('#labyrinthMiniBossIndicator,#labyrinthHardBossIndicator,.health-indicator,.labyrinth-boss__hp,[class*="boss"][class*="hp"]');
if (hp) d[DETAIL_FIELD.bh] = hp;
var bl = getNum('#labyrinthHardBossNeedRank,.labyrinth__hardboss-card-rank,.labyrinth-boss__level,[class*="boss"][class*="level"]');
if (bl) d[DETAIL_FIELD.bl] = bl;
break;
case 'card_trader': case 'card_trader_result':
var tc = getText('.labyrinth__trader-name,.labyrinth-trader__card-name,[class*="trader"][class*="card"]');
if (tc) d[DETAIL_FIELD.c] = tc.substring(0,80);
var tp = getNum('.labyrinth__trader-price,.labyrinth-trader__price,[class*="trader"][class*="price"]');
if (tp) d[DETAIL_FIELD.p] = tp;
break;
case 'locked_chest': case 'locked_chest_result':
var cr = getText('#labyrinthEventText,.labyrinth__chest-desc,.labyrinth__chest-title,.labyrinth-chest__result,[class*="chest"][class*="result"],[class*="chest"][class*="reward"]');
if (cr) d[DETAIL_FIELD.cr] = cr.substring(0,100);
break;
case 'luck_altar': case 'luck_altar_result':
var ar = getText('.labyrinth__luck-title,.labyrinth__luck-desc,.labyrinth-altar__result,[class*="altar"][class*="result"]');
if (ar) d[DETAIL_FIELD.ar] = ar.substring(0,100);
break;
case 'relic_room':
var rl = getText('.labyrinth-relic__name,[class*="relic"][class*="name"],[class*="relic"]');
if (rl) d[DETAIL_FIELD.r] = rl.substring(0,80);
break;
case 'echo_room': case 'echo_room_result':
var eu = getText('#labyrinthEchoUser .labyrinth__echo-user-name,#labyrinthEchoUser,.labyrinth-echo__username,[class*="echo"][class*="user"],[class*="echo"]');
if (eu) d[DETAIL_FIELD.eu] = eu.substring(0,40);
break;
case 'fate_room': case 'fate_room_result':
var fr = window.labyrinthData && window.labyrinthData.fateRoom;
if (fr) d.fate = fr;
break;
case 'personal_mine': case 'personal_mine_collect':
var pm = window.labyrinthData && window.labyrinthData.personalMine;
if (pm) {
if (pm.level) d[DETAIL_FIELD.ml] = pm.level;
if (pm.pending_acc) d[DETAIL_FIELD.ma] = pm.pending_acc;
if (pm.pending_cards) d[DETAIL_FIELD.mc] = pm.pending_cards;
}
break;
case 'reward':
var ra = getNum('#labyrinthEventAcc,.labyrinth__event-acc,.labyrinth-reward__amount,[class*="reward"][class*="amount"]');
if (ra) d[DETAIL_FIELD.a] = ra;
break;
case 'penalty':
var pa = getNum('#labyrinthEventAcc,.labyrinth__event-acc,.labyrinth-penalty__amount,[class*="penalty"][class*="amount"]');
if (pa) d[DETAIL_FIELD.a] = -Math.abs(pa);
break;
}
} catch(e) {}
return Object.keys(d).length ? d : null;
}
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:'#0d1a0d', personal_mine_collect:'#001a1a',
foreign_mine:'#1a0800', club_war_room:'#1a0020',
echo_room:'#0d1520', echo_room_result:'#0d1520', fate_room:'#24143a', fate_room_result:'#24143a', 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'
};
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:'💤', club_war_room:'🏴',
echo_room:'👣', echo_room_result:'👣', fate_room:'🧵', fate_room_result:'🧵',
rl:'🔮', mx:'👅', mr:'👅'
};
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:'#aaffee',
foreign_mine:'#ff9944', fatigue:'#ffd66b', club_war_room:'#ff6666',
echo_room:'#b0a0ff', echo_room_result:'#b0a0ff', fate_room:'#d8a8ff', fate_room_result:'#d8a8ff',
rl:'#c59cff', mx:'#ff9ecb', mr:'#ff9ecb'
};
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:'Усталость',
club_war_room:'Битва клубов', echo_room:'Отголосок', echo_room_result:'Отголосок✓',
fate_room:'Выбор судьбы', fate_room_result:'Выбор судьбы✓',
rl:'Реликвия', mx:'Мимик↺', mr:'Мимик+'
};
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:'Здесь ловушка или подарок от другого игрока', 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:'Здесь шахта другого игрока', club_war_room:'Комната битвы клубов',
echo_room:'Комната отголосков', echo_room_result:'Комната отголосков — выбор уже сделан',
fate_room:'Комната выбора судьбы', fate_room_result:'Выбор судьбы уже сделан',
fatigue:'Усталость от лабиринта'
};
var LEGEND_ITEMS = [
['start','Старт'],['reward','Награда'],['jackpot','Джекпот'],['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','Чужая шахта'],
['club_war_room','Битва клубов'],['echo_room','Комната отголосков'],['fate_room','Выбор судьбы'],['__variable__','🔀 Переменные']
];
var cloudMap = {};
var _cloudMapCount = 0;
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 cleanViewMode = false;
var ownershipCache = null;
var highlightRoom = null;
var _lmPatching = false;
var _pushTimer = null;
var _pushPending = false;
var _wbActive = 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; }
var _gridCenter = null;
function lmDiagFn() {
var mapEl = document.getElementById('labyrinthMap');
if (!mapEl) { console.error('labyrinthMap not found'); return; }
var cells = mapEl.querySelectorAll('.labyrinth-cell');
var cur = curPos();
console.log('=== LM DIAG ===');
console.log('Total cells:', cells.length, '| curPos:', JSON.stringify(cur));
var firstRect = cells[0] ? cells[0].getBoundingClientRect() : null;
if (firstRect) {
var rowCount = 0;
for (var ri = 0; ri < cells.length; ri++) {
var r = cells[ri].getBoundingClientRect();
if (Math.abs(r.top - firstRect.top) < 2) rowCount++;
else break;
}
console.log('Grid width:', rowCount);
}
for (var i = 0; i < Math.min(3, cells.length); i++) {
console.log('cell['+i+']: data-x='+cells[i].getAttribute('data-x')+' data-y='+cells[i].getAttribute('data-y'));
}
for (var j = 0; j < cells.length; j++) {
var cl = cells[j];
var isCur = cl.classList.contains('labyrinth-cell--current');
var isAvail= cl.classList.contains('labyrinth-cell--available');
if (!isCur && !isAvail) continue;
var tp = isCur ? 'CURRENT' : 'AVAILABLE';
var rect2 = cl.getBoundingClientRect();
console.log(tp+' idx='+j+' data-x='+cl.getAttribute('data-x')+' data-y='+cl.getAttribute('data-y'));
}
console.log('=== END ===');
}
var _debugOverlayOn = false;
function lmDebugFn() {
var mapEl = document.getElementById('labyrinthMap');
if (!mapEl) return;
_debugOverlayOn = !_debugOverlayOn;
var cells = mapEl.querySelectorAll('.labyrinth-cell');
cells.forEach(function(cell, idx){
var existing = cell.querySelector('.lm-dbg');
if (existing) existing.parentNode.removeChild(existing);
if (!_debugOverlayOn) return;
var dx = cell.getAttribute('data-x') || '?';
var dy = cell.getAttribute('data-y') || '?';
var col = idx % 25, row = Math.floor(idx/25);
var span = document.createElement('span');
span.className = 'lm-dbg';
span.style.cssText = 'position:absolute;top:0;left:0;font-size:4px;color:rgba(255,255,0,.8);line-height:1;z-index:99;pointer-events:none;background:rgba(0,0,0,.5);padding:1px;';
span.textContent = dx+','+dy+'\n'+col+','+row;
cell.style.position = 'relative';
cell.appendChild(span);
});
}
window.lmDiag = lmDiagFn;
window.lmDebug = lmDebugFn;
function detectGridCenter(cells) {
for (var i = 0; i < cells.length; i++) {
if (!cells[i].classList.contains('labyrinth-cell--current')) continue;
var col = i % 25;
var row = Math.floor(i / 25);
return { gx: col, gy: row, useIndex: true };
}
return null;
}
function applyCleanView() {
var mapEl = document.getElementById('labyrinthMap');
if (!mapEl) return;
var cells = mapEl.querySelectorAll('.labyrinth-cell');
cells.forEach(function(cell) {
cell.classList.remove('lm-known');
if (!cell.classList.contains('labyrinth-cell--visited')) {
cell.removeAttribute('data-event');
}
var icon = cell.querySelector('.lm-cell-icon');
if (icon) icon.parentNode.removeChild(icon);
});
}
function removeCleanView() { patchSiteMapDelayed(); }
function patchSiteMap() {
if (cleanViewMode) return;
var mapEl = document.getElementById('labyrinthMap');
if (!mapEl) return;
var d = mapData();
if (!d || !d.current) return;
var cur = d.current;
var rooms = allRooms();
var cells = mapEl.querySelectorAll('.labyrinth-cell');
if (!cells.length) return;
var center = detectGridCenter(cells);
if (!center) return;
_lmPatching = true;
var cellsArr = Array.prototype.slice.call(cells);
cellsArr.forEach(function(cell, idx) {
if (cell.classList.contains('labyrinth-cell--current')) return;
if (cell.classList.contains('labyrinth-cell--visited')) {
cell.classList.remove('lm-known');
var oldVisIcon = cell.querySelector('.lm-cell-icon');
if (oldVisIcon) oldVisIcon.parentNode.removeChild(oldVisIcon);
return;
}
var gx, gy;
if (center.useIndex) { gx = idx % 25; gy = Math.floor(idx / 25); }
else {
var dxA = cell.getAttribute('data-x');
var dyA = cell.getAttribute('data-y');
gx = parseInt(dxA || '', 10); gy = parseInt(dyA || '', 10);
if (isNaN(gx) || isNaN(gy)) { gx = idx % 25; gy = Math.floor(idx / 25); }
}
var lx = cur.x + (gx - center.gx);
var ly = cur.y + (gy - center.gy);
var key = lx + '_' + ly;
var room = rooms[key];
if (!room || room.event === 'unknown') {
cell.classList.remove('lm-known');
cell.removeAttribute('data-event');
var oldIcon = cell.querySelector('.lm-cell-icon');
if (oldIcon) oldIcon.parentNode.removeChild(oldIcon);
return;
}
var evName = room.event;
var iconChar = ico(evName) || '';
var iconColor = iclr(evName) || 'rgba(255,255,255,.7)';
cell.classList.add('lm-known');
cell.setAttribute('data-event', evName);
var iconSpan = cell.querySelector('.lm-cell-icon');
if (!iconSpan) {
iconSpan = document.createElement('span');
iconSpan.className = 'lm-cell-icon';
cell.appendChild(iconSpan);
}
iconSpan.textContent = iconChar;
iconSpan.style.color = iconColor;
});
_lmPatching = false;
}
function patchSiteMapDelayed() {
var tries = 0;
function attempt() {
var mapEl = document.getElementById('labyrinthMap');
var cells = mapEl && mapEl.querySelectorAll('.labyrinth-cell');
var hasCurrent = false;
if (cells && cells.length > 0) {
for (var i = 0; i < cells.length; i++) {
if (cells[i].classList.contains('labyrinth-cell--current')) { hasCurrent = true; break; }
}
}
if (hasCurrent) { patchSiteMap(); }
else if (tries++ < 30) { setTimeout(attempt, 300); }
}
attempt();
}
function updateMapInfo(cachedRooms) {
var rooms = cachedRooms || allRooms();
var keys = Object.keys(rooms);
var total = keys.length;
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;
var st = el('lm-st');
if (st) st.textContent = 'База: '+_cloudMapCount+' | Всего: '+total;
updateEmissionInfo();
}
var _emissionTimer = null;
var _emissionCheckAt = 0; // абсолютний unix-час наступної перевірки (секунди)
var _emissionCheckSnap = 0; // значення next_check_left з якого рахували _emissionCheckAt
function _startEmissionTimer() {
if (_emissionTimer) return;
_emissionTimer = setInterval(function() { updateEmissionInfo(); }, 1000);
}
function _stopEmissionTimer() {
if (_emissionTimer) { clearInterval(_emissionTimer); _emissionTimer = null; }
}
function updateEmissionInfo() {
var valEl = document.getElementById('lm-mi-emission-val');
var lblEl = document.getElementById('lm-mi-emission-lbl');
var icoEl = document.getElementById('lm-mi-emission-ico');
var card = document.getElementById('lm-mi-emission-card');
if (!valEl) return;
var ld = window.labyrinthData || {};
var em = ld.emission;
if (!em || !em.last_start_at) {
_stopEmissionTimer();
valEl.textContent = '—';
if (lblEl) lblEl.textContent = 'выброс';
if (icoEl) icoEl.textContent = '🌩';
if (card) card.classList.remove('is-active');
return;
}
var now = Math.floor(Date.now() / 1000);
if (em.active) {
var endsAt = em.active_until || (em.last_start_at + EMISSION_DURATION_SEC);
var left = endsAt - now;
_startEmissionTimer();
if (icoEl) icoEl.textContent = '⚡';
if (card) card.classList.add('is-active');
if (left > 0) {
var hh = Math.floor(left / 3600);
var mm = Math.floor((left % 3600) / 60), ss = left % 60;
valEl.textContent = hh > 0
? hh + ':' + (mm < 10 ? '0' : '') + mm + ':' + (ss < 10 ? '0' : '') + ss
: mm + ':' + (ss < 10 ? '0' : '') + ss;
if (lblEl) lblEl.textContent = 'до конца выброса';
} else {
valEl.textContent = 'скоро конец';
if (lblEl) lblEl.textContent = 'выброс идёт';
}
} else {
if (card) card.classList.remove('is-active');
if (icoEl) icoEl.textContent = '🌩';
var nextCheck = em.cooldown_left || 0;
var lastEnd = em.last_end_at || 0;
var lastStart = em.last_start_at || 0;
if (nextCheck > 0) {
// Якщо прийшло значно інше значення next_check_left — свіжі дані з сервера,
// перераховуємо абсолютний час перевірки
if (!_emissionCheckAt || Math.abs(nextCheck - _emissionCheckSnap) > 10) {
_emissionCheckAt = now + nextCheck;
_emissionCheckSnap = nextCheck;
}
var checkLeft = _emissionCheckAt - now;
if (checkLeft > 0) {
_startEmissionTimer();
var chh = Math.floor(checkLeft / 3600);
var cmm = Math.floor((checkLeft % 3600) / 60);
var css = checkLeft % 60;
valEl.textContent = chh > 0
? chh + ':' + (cmm < 10 ? '0' : '') + cmm + ':' + (css < 10 ? '0' : '') + css
: cmm + ':' + (css < 10 ? '0' : '') + css;
if (lblEl) lblEl.textContent = 'до выброса';
} else {
// час перевірки настав — скидаємо і показуємо "скоро"
_emissionCheckAt = 0;
_emissionCheckSnap = 0;
_stopEmissionTimer();
valEl.textContent = 'скоро';
if (lblEl) lblEl.textContent = 'ожидание выброса';
}
} else {
_emissionCheckAt = 0;
_emissionCheckSnap = 0;
_stopEmissionTimer();
if (lastEnd > 0) {
var endHH = new Date(lastEnd * 1000).getHours();
var endMM = new Date(lastEnd * 1000).getMinutes();
valEl.textContent = (endHH < 10 ? '0' : '') + endHH + ':' + (endMM < 10 ? '0' : '') + endMM;
if (lblEl) {
var dur = lastEnd - lastStart;
lblEl.textContent = 'последний (' + Math.round(dur/60) + ' мин)';
}
} else {
valEl.textContent = '?';
if (lblEl) lblEl.textContent = 'нет данных';
}
}
}
}
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,
personal_mine:true, personal_mine_created:true, personal_mine_collect:true,
foreign_mine:true, echo_room:true, echo_room_result:true, fate_room:true, fate_room_result:true
};
function allRooms() {
if (roomsCache) return roomsCache;
var r = {}, k;
for (k in cloudMap) {
var room = cloudMap[k], ev = room.event;
var roomObj = room.room_object || null;
if (ev === 'room_trap' || ev === 'room_gift') { roomObj = ev; ev = room.base_type || ev; }
var altTypes = (room.alt_types || []).filter(function(t){
return t !== 'room_trap' && t !== 'room_gift' && t !== 'room_player' && t !== 'shield_block';
});
if (ev === 'unknown' && altTypes.length > 0) { ev = altTypes[0]; altTypes = altTypes.slice(1); }
var rDat = room.dat || {};
r[k] = {event:ev, guardian:room.guardian||null, altTypes:altTypes, roomObject:roomObj,
emissionVisited:!!(rDat.emission_visited), emissionEv:rDat.emission_ev||null};
}
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;
var isRoomEv = (s.event==='room_trap'||s.event==='room_gift');
if (!r[key]) {
r[key] = { event: s.event, guardian: null, visits: 1, altTypes: [], roomObject: isRoomEv ? s.event : null };
} else {
var cur = r[key];
if (isRoomEv) { 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') {
if (VARIABLE_EVENTS[cur.event] || VARIABLE_EVENTS[s.event]) {
if (cur.altTypes.indexOf(s.event) === -1) cur.altTypes.push(s.event);
}
}
}
}
}
roomsCache = r;
return r;
}
var MAP_DB_NAME='animesss-labyrinth-map';
var MAP_DB_VERSION=1;
function openMapDb(){
return new Promise(function(resolve,reject){
if(!window.indexedDB){reject(new Error('indexeddb_unavailable'));return;}
var request=indexedDB.open(MAP_DB_NAME,MAP_DB_VERSION);
request.onupgradeneeded=function(){
var db=request.result;
if(!db.objectStoreNames.contains('rooms'))db.createObjectStore('rooms',{keyPath:'key'});
if(!db.objectStoreNames.contains('meta'))db.createObjectStore('meta',{keyPath:'name'});
};
request.onsuccess=function(){resolve(request.result);};
request.onerror=function(){reject(request.error);};
});
}
function idbGetAllRooms(){return openMapDb().then(function(db){return new Promise(function(resolve,reject){var tx=db.transaction('rooms','readonly'),r=tx.objectStore('rooms').getAll();r.onsuccess=function(){resolve(r.result||[])};r.onerror=function(){reject(r.error)}})});}
function idbCountRooms(){return openMapDb().then(function(db){return new Promise(function(resolve,reject){var tx=db.transaction('rooms','readonly'),r=tx.objectStore('rooms').count();r.onsuccess=function(){resolve(Number(r.result||0))};r.onerror=function(){reject(r.error)}})});}
function idbGetMeta(name){return openMapDb().then(function(db){return new Promise(function(resolve,reject){var r=db.transaction('meta','readonly').objectStore('meta').get(name);r.onsuccess=function(){resolve(r.result?r.result.value:null)};r.onerror=function(){reject(r.error)}})});}
function idbPutMeta(name,value){return openMapDb().then(function(db){return new Promise(function(resolve,reject){var tx=db.transaction('meta','readwrite');tx.objectStore('meta').put({name:name,value:value});tx.oncomplete=function(){resolve()};tx.onerror=function(){reject(tx.error)}})});}
function idbReplaceRooms(rooms,version){
rooms=Array.isArray(rooms)?rooms:[];
return openMapDb().then(function(db){
return new Promise(function(resolve,reject){
var clearTx=db.transaction('rooms','readwrite');
clearTx.objectStore('rooms').clear();
clearTx.oncomplete=function(){resolve(db);};
clearTx.onerror=function(){reject(clearTx.error);};
});
}).then(function(db){
var index=0;
var batchSize=1500;
function writeNext(){
if(index>=rooms.length){
return new Promise(function(resolve,reject){
var tx=db.transaction('meta','readwrite');
var meta=tx.objectStore('meta');
meta.put({name:'map_version',value:Number(version||0)});
meta.put({name:'map_complete',value:true});
meta.put({name:'room_count',value:rooms.length});
tx.oncomplete=function(){resolve();};
tx.onerror=function(){reject(tx.error);};
});
}
var end=Math.min(index+batchSize,rooms.length);
return new Promise(function(resolve,reject){
var tx=db.transaction('rooms','readwrite');
var store=tx.objectStore('rooms');
for(var i=index;i<end;i++){
var room=rooms[i];
if(room&&room.x!=null&&room.y!=null){
store.put(Object.assign({key:room.x+'_'+room.y},room));
}
}
tx.oncomplete=function(){index=end;resolve();};
tx.onerror=function(){reject(tx.error);};
}).then(writeNext);
}
return writeNext();
});
}
function idbApplyChanges(changes,version){return openMapDb().then(function(db){return new Promise(function(resolve,reject){var tx=db.transaction(['rooms','meta'],'readwrite'),store=tx.objectStore('rooms');(changes||[]).forEach(function(room){if(room&&room.x!=null&&room.y!=null)store.put(Object.assign({key:room.x+'_'+room.y},room));});var meta=tx.objectStore('meta');meta.put({name:'map_version',value:Number(version||0)});meta.put({name:'map_complete',value:true});tx.oncomplete=function(){resolve()};tx.onerror=function(){reject(tx.error)}})});}
function roomsArrayToCloud(rows){var converted={};(rows||[]).forEach(function(room){if(room&&room.x!=null&&room.y!=null)converted[room.x+'_'+room.y]={event:room.ev||room.event||'unknown',base_type:room.base_type||null,current_event:room.current_event||null,guardian:room.guardian||null,alt_types:room.alt_types||[],room_object:room.current_event||room.room_object||null,first_user:room.first_user||null,first_at:room.first_at||null,last_user:room.last_user||null,last_at:room.last_at||null,visits:room.visits||0,dat:room.dat||null,state_version:room.state_version||0};});return converted;}
function idbStreamRooms(onBatch, onDone, onError) {
openMapDb().then(function(db) {
var tx = db.transaction('rooms', 'readonly');
var store = tx.objectStore('rooms');
var batch = [];
var BATCH = 3000;
var req = store.openCursor();
req.onsuccess = function(e) {
var cursor = e.target.result;
if (cursor) {
batch.push(cursor.value);
if (batch.length >= BATCH) {
onBatch(batch.splice(0, BATCH));
}
cursor.continue();
} else {
if (batch.length) onBatch(batch);
if (onDone) onDone();
}
};
req.onerror = function() { if (onError) onError(req.error); };
}).catch(function(e) { if (onError) onError(e); });
}
function loadFromCache() {
var rendered = false;
idbStreamRooms(
function(batch) {
if (!cloudMap) cloudMap = {};
batch.forEach(function(room) {
if (!room || room.x == null || room.y == null) return;
if (Math.abs(room.y) > 2000 || Math.abs(room.x) > 2000) return;
cloudMap[room.x + '_' + room.y] = {
event: room.ev || room.event || 'unknown',
base_type: room.base_type || null,
current_event: room.current_event || null,
guardian: room.guardian || null,
alt_types: room.alt_types || [],
room_object: room.current_event || room.room_object || null,
first_user: room.first_user || null,
first_at: room.first_at || null,
last_user: room.last_user || null,
last_at: room.last_at || null,
visits: room.visits || 0,
dat: room.dat || null,
state_version: room.state_version || 0
};
});
_cloudMapCount = Object.keys(cloudMap).length;
invalidateRoomsCache();
drawMini();
if (!rendered) { rendered = true; if (fmOpen) drawFull(); }
},
function() {
if (!rendered) {
try {
var raw = window.lmLS.get(LS_KEY);
if (raw) {
var parsed = JSON.parse(raw);
Object.keys(parsed).forEach(function(k) {
var p = k.split('_');
if (Math.abs(+p[0]) > 2000 || Math.abs(+p[1]) > 2000) delete parsed[k];
});
cloudMap = parsed;
_cloudMapCount = Object.keys(parsed).length;
invalidateRoomsCache();
var migrate = Object.keys(parsed).map(function(key) {
var p = key.split('_');
return Object.assign({x: Number(p[0]), y: Number(p[1]), ev: parsed[key].event}, parsed[key]);
});
idbReplaceRooms(migrate, 0).catch(function() {});
drawMini();
if (fmOpen) drawFull();
}
} catch(e) {}
} else {
patchSiteMapDelayed();
}
},
function() {
try {
var raw = window.lmLS.get(LS_KEY);
if (raw) {
var _p = JSON.parse(raw);
Object.keys(_p).forEach(function(k){ var _c=k.split('_'); if(Math.abs(+_c[0])>2000||Math.abs(+_c[1])>2000) delete _p[k]; });
cloudMap = _p; _cloudMapCount = Object.keys(_p).length; invalidateRoomsCache(); drawMini();
}
} catch(e) {}
}
);
return true;
}
function fetchFullMapPaged(request, onProgress){
var byKey=Object.create(null);
var ordered=[];
var version=0;
var cursor=null;
var lastCursorKey='';
var page=0;
var pageSize=2000;
var maxPages=100;
function addRooms(rows){
(rows||[]).forEach(function(room){
if(!room||room.x==null||room.y==null)return;
var key=room.x+'_'+room.y;
if(!byKey[key])ordered.push(key);
byKey[key]=room;
});
}
function next(){
if(page>=maxPages)throw new Error('map_pages_limit');
var url=VPS_URL+'/map/full?limit='+pageSize;
if(cursor)url+='&after_y='+encodeURIComponent(cursor.y)+'&after_x='+encodeURIComponent(cursor.x);
return request(url).then(function(d){
page++;
version=Number(d&&d.version||version||0);
var rows=Array.isArray(d&&d.rooms)?d.rooms:[];
addRooms(rows);
if(onProgress) onProgress(ordered.length, page);
var nextCursor=d&&d.next_cursor;
if(!nextCursor&&rows.length>=pageSize){
var last=rows[rows.length-1];
if(last&&last.x!=null&&last.y!=null)nextCursor={y:last.y,x:last.x};
}
var shouldContinue=Boolean((d&&d.has_more)||rows.length>=pageSize);
if(!shouldContinue||!nextCursor){
return {rooms:ordered.map(function(k){return byKey[k];}),version:version,pages:page};
}
var cursorKey=String(nextCursor.y)+'_'+String(nextCursor.x);
if(cursorKey===lastCursorKey)throw new Error('map_cursor_stuck:'+cursorKey);
lastCursorKey=cursorKey;
cursor={y:Number(nextCursor.y),x:Number(nextCursor.x)};
return next();
});
}
return next();
}
var _cloudLoading = false;
function loadCloud(onDone, onProgress) {
if(_cloudLoading){if(onDone)onDone();return;}
_cloudLoading=true;
function finish(){_cloudLoading=false;if(onDone)onDone();}
function request(url){return window.lmFetch(url,{cache:'no-store'}).then(function(r){if(!r.ok)throw new Error('HTTP '+r.status);return r.json();});}
function rebuildFull(){
return fetchFullMapPaged(request, onProgress).then(function(d){
if(!d.rooms||d.rooms.length<2)throw new Error('full_map_too_small:'+((d.rooms&&d.rooms.length)||0));
return idbReplaceRooms(d.rooms,d.version).then(function(){return {mode:'full',version:d.version,count:d.rooms.length,pages:d.pages};});
});
}
function rebuildFromD1(){
lmWarn('VPS недоступен — загрузка карты из резерва (D1)');
return window.lmFetch(WORKER_URL+'/map',{
cache:'no-store',
headers:{Origin:'https://animesss.com',Referer:'https://animesss.com/'}
}).then(function(r){
if(!r.ok)throw new Error('D1 HTTP '+r.status);
return r.json();
}).then(function(d){
var raw=d.rooms||d;
if(typeof raw!=='object'||Array.isArray(raw))throw new Error('d1_bad_format');
var EV_FIX={'rl':'relic_room','mx':'mimic_chest_back','mr':'mimic_chest_reward'};
var rows=[];
Object.keys(raw).forEach(function(k){
var parts=k.split('_');
var x=parseInt(parts[0],10),y=parseInt(parts[1],10);
if(isNaN(x)||isNaN(y))return;
var room=raw[k]||{};
var ev=String(room.event||'unknown');
ev=EV_FIX[ev]||ev;
rows.push({x:x,y:y,ev:ev,base_type:ev==='unknown'?null:ev,
dat:{alt_types:room.alt_types||[],room_object:room.room_object||null,
guardian:room.guardian||null,source:'d1_fallback'}});
});
if(rows.length<100)throw new Error('d1_too_small:'+rows.length);
if(onProgress)onProgress(rows.length,1);
return idbReplaceRooms(rows,0).then(function(){
return {mode:'d1_fallback',version:0,count:rows.length};
});
});
}
Promise.all([
idbGetMeta('map_version').catch(function(){return 0;}),
idbGetMeta('map_complete').catch(function(){return false;}),
idbCountRooms().catch(function(){return 0;}),
request(VPS_URL+'/map/version')
]).then(function(values){
var localVersion=Number(values[0]||0);
var complete=values[1]===true;
var localCount=Number(values[2]||0);
var remote=values[3]||{};
var remoteVersion=Number(remote.version||0);
var remoteCount=Number(remote.rooms_count||0);
var countMatches=!remoteCount||localCount===remoteCount;
if(complete&&localCount>0&&countMatches&&localVersion===remoteVersion){
return {mode:'same',version:remoteVersion,count:localCount};
}
if(complete&&localCount>0&&countMatches&&localVersion&&remoteVersion>localVersion){
return request(VPS_URL+'/map/changes?after='+localVersion+'&limit=10000').then(function(d){
if(d.has_more)throw new Error('too_many_changes');
return idbApplyChanges(d.changes||[],d.version).then(function(){return {mode:'changes',version:d.version,count:localCount};});
});
}
return rebuildFull();
}).catch(function(error){
lmDebug('[Карта] Полная перестройка кеша:',error);
return rebuildFull().catch(function(e2){
lmDebug('[Карта] VPS недоступен, пробуем D1:',e2);
return idbCountRooms().catch(function(){return 0;}).then(function(cnt){
if(cnt>100)throw new Error('use_cache'); // є кеш — не треба D1
return rebuildFromD1();
});
});
}).then(function(){return idbGetAllRooms();})
.then(function(rows){
cloudMap=roomsArrayToCloud(rows);
_cloudMapCount = Object.keys(cloudMap).length;
invalidateRoomsCache();
updateMapInfo();
drawMini();
if(fmOpen)drawFull();
patchSiteMapDelayed();
lmInfo('Карта обновлена');
finish();
}).catch(function(e){
lmWarn('Используется кеш карты');
lmDebug(e);
finish();
});
}
var _stepsPushing = false;
var FLUSH_EVERY_STEPS = 1;
function schedulePush(steps, sid) {
cacheSteps(steps);
var stored = getCachedSteps();
var sentCount = Math.min(getSentCount(), stored.length);
var pendingCount = Math.max(0, stored.length - sentCount);
if (pendingCount < FLUSH_EVERY_STEPS) return;
if (_pushTimer) clearTimeout(_pushTimer);
_pushPending = true;
_pushTimer = setTimeout(function() {
_pushPending = false;
_pushTimer = null;
syncCacheWithSession();
flushStepsToServer();
}, 700);
}
function pushSteps(steps, sid) {
if (_stepsPushing) return Promise.resolve(null);
_stepsPushing = true;
var auth = getAuth();
if (!auth) { _stepsPushing = false; return Promise.resolve(null); }
var username = auth.username, token = auth.token;
var vpsSteps = steps.map(function(s, i) {
var rawStepIndex = s.step_index != null ? Number(s.step_index) : i;
var stableStepIndex = Number.isInteger(Number(s._lmServerStepIndex))
? Number(s._lmServerStepIndex)
: (Number.isInteger(rawStepIndex) ? rawStepIndex : i);
return { x:s.x, y:s.y, ev:s.ev||s.event||'empty',
acc_delta:s.acc_delta != null ? Number(s.acc_delta) : null,
acc_after:s.acc_after != null ? Number(s.acc_after) : null,
session_id:sid||SESSION_ID, step_index:stableStepIndex,
dat: parseDat(s.dat || s._dat) };
});
function primary() {
return window.lmFetch(VPS_URL + '/push', {
method:'POST', headers:vpsAuthHeaders(true),
body:JSON.stringify({steps:vpsSteps})
}).then(function(r) {
if (!r.ok) throw new Error('HTTP ' + r.status);
return r.json();
}).then(function(d) {
_vpsAlive = true;
return { data:d, reserve:false };
});
}
function reserve() {
return window.lmFetch(WORKER_URL+'/update', {
method:'POST',
headers:{'Content-Type':'application/json','X-Write-Secret':WRITE_SECRET},
body:JSON.stringify({steps:steps,session_id:sid||SESSION_ID,username:username,auth_token:token})
}).then(function(r) {
if (!r.ok) throw new Error('HTTP ' + r.status);
return r.json();
}).then(function(d) { return { data:d, reserve:true }; });
}
return primary().catch(function() {
_vpsAlive = false;
lmWarn('Резервный сервер');
return reserve();
}).then(function(result) {
lmInfo('Карта обновлена');
return result.data;
}).catch(function(e) {
lmWarn('Карта сохранена локально');
lmDebug(e);
return null;
}).then(function(result) {
_stepsPushing = false;
return result;
});
}
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) { if (!r.ok) throw new Error('HTTP ' + r.status); 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 = findClubLink(doc);
if (clubLink) {
return fetch(clubLink.getAttribute('href')).then(function(r){ if (!r.ok) throw new Error('HTTP ' + r.status); 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) { lmDebug('[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';
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() {
var rooms = allRooms(), keys = Object.keys(rooms);
if (elSt) elSt.textContent = 'База: '+_cloudMapCount+' | Всего: '+keys.length;
updateMapInfo(rooms);
}
function openFull(skipCenter) {
fmOpen=true;
document.getElementById('lm-modal').classList.add('on');
updateEmissionInfo();
if (!skipCenter) {
requestAnimationFrame(function(){
if (!fmOpen) return;
fitReadableMap();
});
}
}
function closeFull() { fmOpen=false; document.getElementById('lm-modal').classList.remove('on'); }
function fullMapBounds() {
var rooms = allRooms(), keys = Object.keys(rooms);
if (!keys.length) return null;
var minX=Infinity, maxX=-Infinity, minY=Infinity, maxY=-Infinity;
for (var i=0; i<keys.length; i++) {
var p=keys[i].split('_'), rx=+p[0], ry=+p[1];
if (!Number.isFinite(rx) || !Number.isFinite(ry)) continue;
if (rx<minX) minX=rx; if (rx>maxX) maxX=rx;
if (ry<minY) minY=ry; if (ry>maxY) maxY=ry;
}
return minX===Infinity ? null : {minX:minX,maxX:maxX,minY:minY,maxY:maxY};
}
function setMapView(x, y, cellSize) {
if (!elMbody) return;
var cs = Math.max(1, Math.min(CELL_SIZE, cellSize || CELL_SIZE * fmScale));
fmScale = Math.max(ZOOM_MIN, Math.min(ZOOM_MAX, cs / CELL_SIZE));
cs = CELL_SIZE * fmScale;
fmX = elMbody.offsetWidth / 2 - Number(x || 0) * cs;
fmY = elMbody.offsetHeight / 2 - Number(y || 0) * cs;
drawFull();
}
function fitReadableMap() {
if (!elMbody) return;
var b = fullMapBounds();
if (!b) { centerOn(0, 0, FULL_FOCUS_SCALE); return; }
var c = curPos();
var cx = Number.isFinite(Number(c.x)) ? Number(c.x) : (b.minX + b.maxX) / 2;
var cy = Number.isFinite(Number(c.y)) ? Number(c.y) : (b.minY + b.maxY) / 2;
var W = Math.max(100, elMbody.offsetWidth - 72);
var H = Math.max(100, elMbody.offsetHeight - 96);
var visibleX = Math.max(160, Math.min(360, W / FULL_VIEW_MIN_CELL));
var visibleY = Math.max(95, Math.min(210, H / FULL_VIEW_MIN_CELL));
var cs = Math.min(W / visibleX, H / visibleY, FULL_VIEW_MAX_CELL);
cs = Math.max(FULL_VIEW_MIN_CELL, cs);
setMapView(cx, cy, cs);
}
function fitAllRooms() {
if (!elMbody) return;
var b = fullMapBounds();
if (!b) { centerOn(0, 0); return; }
var W=elMbody.offsetWidth, H=elMbody.offsetHeight;
var spanX=Math.max(1, b.maxX-b.minX+1), spanY=Math.max(1, b.maxY-b.minY+1);
var csX = W / spanX, csY = H / spanY;
var cs = Math.min(csX, csY, CELL_SIZE);
cs = Math.max(0.5, cs);
fmScale = cs / CELL_SIZE;
var midX=(b.minX+b.maxX)/2, midY=(b.minY+b.maxY)/2;
fmX=W/2-midX*cs; fmY=H/2-midY*cs;
drawFull();
}
function centerOn(x,y,scale) {
if (!elMbody) return;
if (Number.isFinite(Number(scale))) fmScale = Math.max(ZOOM_MIN, Math.min(ZOOM_MAX, Number(scale)));
setMapView(x, y, CELL_SIZE * fmScale);
}
function drawFull() {
if (!elFc || !elMbody) return;
window._lmFmX = fmX; window._lmFmY = fmY; window._lmFmScale = fmScale;
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)+cs)%cs; gx<elFc.width; gx+=cs) { ctx.beginPath(); ctx.moveTo(gx,0); ctx.lineTo(gx,elFc.height); ctx.stroke(); }
for (var gy=((fmY%cs)+cs)%cs; gy<elFc.height; gy+=cs) { ctx.beginPath(); ctx.moveTo(0,gy); ctx.lineTo(elFc.width,gy); ctx.stroke(); }
var myPathSet = {};
if (showMyPath) {
if (window._lmMyPathData) {
myPathSet = window._lmMyPathData;
}
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);
var _emCell = window._lmEmissionCells && window._lmEmissionCells.has(keys[i]);
// Червоний фон: кімната unknown, але відвідана під час викиду
if (room.event === 'unknown' && (room.emissionEv || room.emissionVisited || _emCell) && cs >= 4) {
ctx.save();
ctx.fillStyle = 'rgba(180,30,30,0.55)';
ctx.fillRect(px, py, cs, cs);
ctx.restore();
}
if (((room.emissionEv && room.emissionEv !== room.event) || room.emissionVisited || _emCell) && cs >= 4) {
// рамка
ctx.save();
ctx.strokeStyle = 'rgba(255,200,0,0.7)';
ctx.lineWidth = Math.max(1, cs * 0.06);
ctx.strokeRect(px + ctx.lineWidth * 0.5, py + ctx.lineWidth * 0.5, cs - ctx.lineWidth, cs - ctx.lineWidth);
ctx.restore();
// значок
var _efs = Math.max(8, Math.min(cs * 0.55, 18));
ctx.font = _efs + 'px sans-serif';
ctx.fillStyle = 'rgba(255,200,0,0.9)';
ctx.textAlign = 'center';
ctx.textBaseline = 'middle';
ctx.fillText('⚡', px + cs * 0.5, py + cs * 0.5);
ctx.textAlign = 'left';
ctx.textBaseline = 'alphabetic';
}
}
if (showMyPath) {
var pathSteps = [];
if (window._lmHistSteps && window._lmHistSteps.length > 0) {
pathSteps = window._lmHistSteps;
}
var dm3=mapData();
if (dm3&&dm3.steps&&dm3.steps.length) {
var lastHist = pathSteps.length ? pathSteps[pathSteps.length-1] : null;
var firstCur = dm3.steps[0];
if (!lastHist || lastHist.x !== firstCur.x || lastHist.y !== firstCur.y) {
pathSteps = pathSteps.concat(dm3.steps);
} else {
pathSteps = pathSteps.concat(dm3.steps.slice(1));
}
}
if (pathSteps.length > 1) {
ctx.strokeStyle='rgba(255,214,107,.35)'; ctx.lineWidth=Math.max(1,cs*.18); ctx.lineJoin='round';
ctx.beginPath();
var f=pathSteps[0]; ctx.moveTo(fmX+f.x*cs+cs/2,fmY+f.y*cs+cs/2);
for (var li=1;li<pathSteps.length;li++) { var ls=pathSteps[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)+' · База '+_cloudMapCount+' · Всего '+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);
}
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:'#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' + _lmUserSuffix;
var LS_HIST_DATE_KEY = 'lm_hist_cache_date' + _lmUserSuffix;
var histCache = {};
var histTotal = null;
var histCurPage = 1;
try {
var _raw = window.lmLS.get(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 = [];
var trs = doc.querySelectorAll('.ncard-transactions__table tbody tr');
if (!trs.length) trs = doc.querySelectorAll('table tbody tr');
var allAmounts = [];
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;
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 });
});
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;
});
var trapBackCount = 0;
var roomCounts = {};
var DESC_MAP = [
{ re: /мини.?босс/i, ev: 'mini_boss' },
{ re: /сложн\w* босс/i, ev: 'hard_boss' },
{ re: /сундук.мимик/i, ev: 'mimic_chest' },
{ re: /открытие сундука/i, ev: null },
{ re: /сундука в лабиринте/i, ev: 'locked_chest' },
{ re: /алтар\w* удачи/i, ev: 'luck_altar' },
{ re: /викторин/i, ev: 'quiz' },
{ re: /испытание дао/i, ev: 'puzzle' },
{ re: /персональной шахты/i, ev: 'personal_mine' },
{ re: /штраф в лабиринте/i, ev: 'penalty' },
{ re: /награда в лабиринте/i, ev: 'reward' },
{ re: /награда за победу над/i, ev: null },
{ re: /откат|ловушка отката/i, ev: 'trap_back' },
{ re: /коллекц/i, ev: 'collection' },
{ re: /торговец/i, ev: 'card_trader' },
{ re: /реликв/i, ev: 'relic_room' },
{ re: /восстановление|хода/i, ev: 'recovery_room' },
];
var _seenKeys = {};
for (var _ri = 0; _ri < rows.length; _ri++) {
var _desc = rows[_ri].desc;
if (/откат/i.test(_desc)) trapBackCount++;
var _matched = false;
for (var _di = 0; _di < DESC_MAP.length; _di++) {
if (!DESC_MAP[_di].re.test(_desc)) continue;
_matched = true;
var _ev = DESC_MAP[_di].ev;
if (_ev === null) break;
var _deduKey = _ev + '_' + (rows[_ri].date || '').replace(/[^0-9]/g, '').slice(0, 12);
if (_ev === 'locked_chest' && _seenKeys[_deduKey]) break;
_seenKeys[_deduKey] = true;
roomCounts[_ev] = (roomCounts[_ev] || 0) + 1;
break;
}
}
return { rows: rows, totalPages: totalPages, allAmounts: allAmounts, trapBackCount: trapBackCount, roomCounts: roomCounts };
}
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) { if (!r.ok) throw new Error('HTTP ' + r.status); return r.text(); })
.then(function(html) {
var result = parseHistoryHtml(html, pageNum);
histCache[pageNum] = result;
if (histTotal === null || result.totalPages > histTotal) histTotal = result.totalPages;
try { window.lmLS.set(LS_HIST_KEY, JSON.stringify(histCache)); } catch(e) {}
onDone(result);
})
.catch(function(e) {
lmDebug('[LabMap] history err:', e);
onDone({ rows: [], totalPages: pageNum });
});
}
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 buildAllTimeTrapCount() {
var total = 0;
Object.keys(histCache).forEach(function(p) {
total += (histCache[p].trapBackCount || 0);
});
return total;
}
function buildAllTimeRoomCounts() {
var merged = {};
Object.keys(histCache).forEach(function(p) {
var rc = histCache[p].roomCounts || {};
Object.keys(rc).forEach(function(ev) {
merged[ev] = (merged[ev] || 0) + rc[ev];
});
});
return merged;
}
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;
});
delete counts['start'];
var total = d.steps.length - 1;
var histRooms = buildAllTimeRoomCounts();
var hasHistData = Object.keys(histRooms).length > 0;
if (hasHistData) {
Object.keys(histRooms).forEach(function(ev) {
var sessionCount = counts[ev] || 0;
var histCount = histRooms[ev] || 0;
counts[ev] = Math.max(sessionCount, histCount);
});
total = 0;
Object.keys(counts).forEach(function(ev) { total += counts[ev]; });
}
var todayTrap = counts['trap_back'] || 0;
var allTimeTotalEl = document.getElementById('labyrinthTotalSteps');
var domTotal = allTimeTotalEl
? (parseInt(allTimeTotalEl.textContent.replace(/\D/g,''), 10) || total)
: total;
var allTimeTotal = window._lmAllTimeTotal || domTotal;
var dbLoaded = window._lmAllTimeTrapBacks !== undefined;
var allTimeTrap = dbLoaded ? window._lmAllTimeTrapBacks : buildAllTimeTrapCount();
var histLoaded = dbLoaded || allTimeTrap > 0;
var trapCount = histLoaded ? allTimeTrap : todayTrap;
var lostRooms = trapCount * 5;
var sorted = Object.keys(counts).sort(function(a,b){ return counts[b]-counts[a]; });
return {
total: total,
allTimeTotal: allTimeTotal,
counts: counts,
top: sorted,
trapCount: trapCount,
lostRooms: lostRooms,
historyLoaded: histLoaded
};
}
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 = window.lmLS.get(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);
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 trapSub = stats.historyLoaded ? 'за всё время' : 'сегодня';
var cardsHtml = '<div class="lm-sc-grid">' +
'<div class="lm-sc-card"><div class="lm-sc-label">Всего комнат</div><div class="lm-sc-val">'+stats.allTimeTotal+'</div><div class="lm-sc-sub">комнат пройдено</div></div>' +
'<div class="lm-sc-card lm-sc-trap">' +
'<div class="lm-sc-label">Откаты</div>' +
'<div class="lm-sc-val lm-sc-bad">' + (stats.historyLoaded ? (stats.trapCount > 0 ? '−'+stats.trapCount : '0') : '?') + '</div>' +
'<div class="lm-sc-trap-row">' +
(stats.historyLoaded && stats.lostRooms > 0
? '<span class="lm-sc-rooms">−'+stats.lostRooms+' комнат • '+trapSub+'</span>'
: '<span class="lm-sc-hint">'+(stats.historyLoaded ? '0 откатов' : 'нажми Обновить')+'</span>') +
(window._lmRollbackPlace ? '<span class="lm-sc-badge">Топ #'+window._lmRollbackPlace+'</span>' : '') +
'</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>';
}
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) {
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); });
}
function injectStyles() {
var style = document.createElement('style');
style.textContent = [
'#lm-wrap{width:100%;max-width:100%;min-width:0;margin-bottom:18px;border-radius:16px;background:linear-gradient(135deg,rgba(30,33,52,.96) 0%,rgba(16,18,31,.98) 100%);border:1px solid rgba(255,255,255,.1);color:#fff;overflow:hidden;box-shadow:0 8px 32px rgba(0,0,0,.35);container-type:inline-size;}',
'#lm-hdr{display:flex;align-items:center;justify-content:space-between;gap:12px;padding:12px 14px;border-bottom:1px solid rgba(255,255,255,.08);background:rgba(255,255,255,.025);min-width:0;}',
'#lm-ttl{display:flex;align-items:center;gap:8px;min-width:0;font-size:12px;font-weight:900;text-transform:uppercase;letter-spacing:.04em;color:rgba(226,232,240,.72);white-space:nowrap;overflow:hidden;text-overflow:ellipsis;}',
'#lm-ttl-dot{width:8px;height:8px;border-radius:50%;background:#4a5ac7;box-shadow:0 0 8px rgba(74,90,199,.7);animation:lm-pulse 2s infinite;}',
'@keyframes lm-pulse{0%,100%{opacity:1;transform:scale(1)}50%{opacity:.5;transform:scale(.8)}}',
'#lm-btns{display:flex;align-items:center;justify-content:flex-end;gap:8px;min-width:0;}',
'#lm-rbtn{min-height:36px;padding:8px 14px;border:none;border-radius:8px;background:linear-gradient(135deg,#4a5ac7,#3a4ab0);color:#fff;font-size:11px;font-weight:800;cursor:pointer;box-shadow:0 2px 12px rgba(74,90,199,.4);transition:.15s;white-space:nowrap;}',
'#lm-rbtn:hover{background:linear-gradient(135deg,#5b6dd8,#4a5ac7);transform:translateY(-1px);}',
'#lm-rbtn.loading{opacity:.6;cursor:not-allowed;transform:none;}',
'#lm-pbtn{min-height:36px;padding:8px 14px;border:none;border-radius:8px;background:linear-gradient(135deg,#b02a59,#8a1f45);color:#fff;font-size:11px;font-weight:800;cursor:pointer;box-shadow:0 2px 12px rgba(176,42,89,.4);transition:.15s;white-space:nowrap;}',
'#lm-pbtn:hover{background:linear-gradient(135deg,#c03468,#b02a59);transform:translateY(-1px);}',
'#lm-cleanbtn{min-height:36px;padding:8px 14px;border:none;border-radius:8px;background:rgba(255,255,255,.08);color:rgba(255,255,255,.65);font-size:11px;font-weight:800;cursor:pointer;border:1px solid rgba(255,255,255,.12);transition:.15s;white-space:nowrap;}',
'#lm-cleanbtn:hover{background:rgba(255,255,255,.14);color:#fff;transform:translateY(-1px);}',
'#lm-cleanbtn.active{background:rgba(255,214,107,.18);color:#ffd66b;border-color:rgba(255,214,107,.45);}',
'#lm-map-info{display:grid;grid-template-columns:repeat(2,minmax(0,1fr));gap:0;border-bottom:1px solid rgba(255,255,255,.07);}',
'.lm-mi-card{display:flex;align-items:center;gap:10px;min-width:0;padding:11px 16px;}',
'.lm-mi-card:first-child{border-right:1px solid rgba(255,255,255,.07);}',
'.lm-mi-ico{font-size:22px;flex-shrink:0;}',
'.lm-mi-val{font-size:20px;font-weight:850;line-height:1;color:#fff;margin-bottom:2px;white-space:nowrap;overflow:hidden;text-overflow:ellipsis;}',
'.lm-mi-val-sm{font-size:15px;font-weight:700;line-height:1;color:rgba(255,255,255,.78);margin-bottom:2px;white-space:nowrap;overflow:hidden;text-overflow:ellipsis;}',
'.lm-mi-sep{height:1px;background:rgba(255,255,255,.08);margin:5px 0;}',
'.lm-mi-lbl{font-size:10px;color:rgba(255,255,255,.46);text-transform:uppercase;letter-spacing:.04em;white-space:nowrap;overflow:hidden;text-overflow:ellipsis;}',
'#lm-mi-emission-card.is-active .lm-mi-ico{animation:lmEmissionPulse 1.2s ease-in-out infinite;}',
'@keyframes lmEmissionPulse{0%,100%{opacity:1;}50%{opacity:.4;}}',
'#lm-action-bar{display:flex;gap:8px;padding:11px 14px;border-bottom:1px solid rgba(255,255,255,.07);background:rgba(0,0,0,.15);min-width:0;}',
'.labyrinth-cell.lm-emission-cell{position:relative!important;}',
'.lm-emission-ico{position:absolute;bottom:1px;right:2px;font-size:7px;line-height:1;pointer-events:none;z-index:5;filter:drop-shadow(0 0 2px rgba(255,220,0,.8));}',
'.lm-abar-btn{flex:1 1 0;min-width:0;display:flex;align-items:center;justify-content:center;gap:7px;padding:9px 10px;border:1px solid rgba(255,255,255,.1);background:rgba(255,255,255,.05);color:rgba(255,255,255,.68);font-size:11px;font-weight:800;cursor:pointer;border-radius:10px;transition:background .15s,border-color .15s,color .15s;white-space:nowrap;overflow:hidden;text-overflow:ellipsis;}',
'.lm-abar-btn:hover{background:rgba(255,255,255,.1);border-color:rgba(255,255,255,.2);color:rgba(255,255,255,.9);}',
'.lm-abar-ico{font-size:15px;line-height:1;flex-shrink:0;}',
'#lm-wb-guard.active{background:rgba(255,215,0,.15);border-color:rgba(255,215,0,.5);color:#ffd700;}',
'#lm-wb-club.active{background:rgba(197,156,255,.15);border-color:rgba(197,156,255,.5);color:#c59cff;}',
'#lm-wb-hist.active{background:rgba(103,232,249,.15);border-color:rgba(103,232,249,.5);color:#67e8f9;}',
'#lm-inline-panel{display:none;border-top:0;}',
'#lm-inline-panel.on{display:block;}',
'#lm-wi-guard,#lm-wi-club{max-height:280px;overflow-y:auto;}',
'.lm-wi-item{display:flex;align-items:center;gap:10px;padding:9px 16px;cursor:pointer;transition:.12s;border-bottom:1px solid rgba(255,255,255,.04);}',
'.lm-wi-item:last-child{border-bottom:none;}',
'.lm-wi-item:hover{background:rgba(255,255,255,.05);}',
'.lm-wi-img{width:34px;height:48px;border-radius:6px;object-fit:cover;flex-shrink:0;}',
'.lm-wi-info{flex:1;min-width:0;}',
'.lm-wi-name{font-size:12px;font-weight:700;color:#fff;white-space:nowrap;overflow:hidden;text-overflow:ellipsis;}',
'.lm-wi-room{font-size:11px;color:rgba(255,255,255,.55);margin-top:2px;}',
'.lm-wi-date{font-size:10px;color:rgba(255,255,255,.35);margin-top:1px;}',
'.lm-wi-nav{font-size:14px;color:rgba(255,255,255,.3);flex-shrink:0;}',
'.lm-wi-empty{padding:20px;text-align:center;font-size:12px;color:rgba(255,255,255,.35);}',
'.lm-wi-loading{padding:16px;text-align:center;font-size:12px;color:rgba(255,255,255,.4);}',
'#lm-wi-hist{max-height:420px;overflow-y:auto;}',
'#lm-cvs-wrap{display:none!important;}#lm-cvs{display:none!important;}#lm-st{display:none!important;}',
'@container (max-width:620px){#lm-hdr{flex-direction:column;align-items:stretch;padding:11px;}#lm-btns{justify-content:flex-start;overflow-x:auto;overflow-y:hidden;margin:0 -11px;padding:0 11px 2px;scrollbar-width:none;}#lm-btns::-webkit-scrollbar{display:none;}#lm-cleanbtn,#lm-rbtn,#lm-pbtn{flex:0 0 auto;min-height:38px;padding:8px 12px;}#lm-map-info{grid-template-columns:repeat(2,minmax(0,1fr));}.lm-mi-card{padding:12px 11px;gap:8px;}.lm-mi-val{font-size:20px;}.lm-mi-lbl{font-size:9px;}#lm-action-bar{overflow-x:auto;overflow-y:hidden;flex-wrap:nowrap;padding:10px 11px;scrollbar-width:none;}#lm-action-bar::-webkit-scrollbar{display:none;}.lm-abar-btn{flex:0 0 auto;min-width:134px;font-size:10px;padding:10px 12px;}.lm-sc-grid{grid-template-columns:repeat(2,minmax(0,1fr));gap:7px!important;}.lm-sc-card{min-width:0;padding:9px 10px;}.lm-sc-val{font-size:18px;}.lm-hist-body{padding:8px;}.lm-hr{gap:7px;padding:7px 8px;}.lm-hr-amt{font-size:12px;}}',
'@container (max-width:420px){#lm-wrap{border-radius:12px;}#lm-ttl{font-size:11px;}#lm-map-info{grid-template-columns:1fr;}.lm-mi-card:first-child{border-right:0;border-bottom:1px solid rgba(255,255,255,.07);}.lm-mi-ico{width:24px;font-size:20px;text-align:center;}.lm-mi-val{font-size:19px;}#lm-cleanbtn,#lm-rbtn,#lm-pbtn{font-size:10px;padding:8px 10px;}.lm-sc-grid{grid-template-columns:1fr!important;}.lm-hr-desc{white-space:normal;display:-webkit-box;-webkit-line-clamp:2;-webkit-box-orient:vertical;}.lm-hr-amt{align-self:flex-start;white-space:nowrap;}}',
'@media(max-width:640px){#lm-map-info{grid-template-columns:1fr 1fr;}.lm-mi-val{font-size:18px;}.lm-abar-btn{font-size:10px;padding:9px 8px;}}',
'#lm-modal{display:none;position:fixed;inset:0;z-index:999999;background:#060912;flex-direction:column;touch-action:none;overflow:hidden;color:#f8fbff;height:100vh;height:100dvh;}',
'#lm-modal:before{content:"";position:absolute;inset:0;pointer-events:none;background:linear-gradient(rgba(255,255,255,.025) 1px,transparent 1px),linear-gradient(90deg,rgba(255,255,255,.025) 1px,transparent 1px),radial-gradient(circle at 50% 20%,rgba(56,189,248,.09),transparent 42%);background-size:24px 24px,24px 24px,100% 100%;opacity:.8;}',
'#lm-modal.on{display:flex;}',
'#lm-mhdr{position:relative;z-index:4;display:flex;flex-direction:column;gap:8px;padding:calc(10px + env(safe-area-inset-top)) 14px 10px;background:rgba(12,16,28,.88);border-bottom:1px solid rgba(148,163,184,.18);box-shadow:0 12px 32px rgba(0,0,0,.32);backdrop-filter:blur(14px);flex-shrink:0;}',
'#lm-mhdr-top{display:flex;align-items:center;justify-content:space-between;gap:12px;width:100%;min-width:0;}',
'#lm-mhdr-btns{display:flex;align-items:center;gap:8px;flex-wrap:nowrap;overflow-x:auto;overflow-y:hidden;scrollbar-width:none;margin:0 -14px;padding:0 14px 2px;}',
'#lm-mhdr-btns::-webkit-scrollbar{display:none;}',
'#lm-mttl{display:flex;align-items:center;gap:9px;min-width:0;font-size:14px;font-weight:900;letter-spacing:.02em;color:#fff;white-space:nowrap;overflow:hidden;text-overflow:ellipsis;}',
'#lm-mttl:before{content:"";width:9px;height:9px;border-radius:50%;background:#22d3ee;box-shadow:0 0 16px rgba(34,211,238,.85);flex:0 0 auto;}',
'#lm-b0,#lm-b1,#lm-bf,#lm-bpath,#lm-bguard,#lm-bclub,#lm-bhist{min-height:36px;padding:8px 12px;border-radius:8px;font-size:11px;font-weight:800;line-height:1;cursor:pointer;position:relative;display:inline-flex;align-items:center;justify-content:center;gap:6px;white-space:nowrap;box-shadow:inset 0 1px 0 rgba(255,255,255,.08);transition:background .14s,border-color .14s,color .14s,transform .14s;}',
'#lm-b0{background:rgba(99,102,241,.16);color:#b9c4ff;border:1px solid rgba(129,140,248,.38);}#lm-b0:hover{background:rgba(99,102,241,.28);color:#fff;transform:translateY(-1px);}',
'#lm-b1{background:rgba(245,158,11,.16);color:#ffd66b;border:1px solid rgba(245,158,11,.42);}#lm-b1:hover{background:rgba(245,158,11,.3);color:#fff;transform:translateY(-1px);}',
'#lm-bf{background:rgba(139,92,246,.16);color:#d8c4ff;border:1px solid rgba(139,92,246,.4);}#lm-bf:hover{background:rgba(139,92,246,.3);color:#fff;transform:translateY(-1px);}#lm-bf.active{background:#7c3aed;color:#fff;border-color:#a78bfa;}',
'#lm-bpath{background:rgba(16,185,129,.14);color:#7cf4a2;border:1px solid rgba(16,185,129,.38);}#lm-bpath:hover{background:rgba(16,185,129,.28);color:#fff;transform:translateY(-1px);}#lm-bpath.active{background:#059669;color:#fff;border-color:#34d399;}',
'#lm-bguard{background:rgba(234,179,8,.14);color:#ffe16d;border:1px solid rgba(234,179,8,.34);}#lm-bguard:hover{background:rgba(234,179,8,.27);color:#fff;transform:translateY(-1px);}#lm-bguard.active{background:#a16207;color:#fff;border-color:#eab308;}',
'#lm-bclub{background:rgba(168,85,247,.14);color:#d8b4fe;border:1px solid rgba(168,85,247,.34);}#lm-bclub:hover{background:rgba(168,85,247,.27);color:#fff;transform:translateY(-1px);}#lm-bclub.active{background:#6b21a8;color:#fff;border-color:#c084fc;}',
'#lm-bhist{background:rgba(6,182,212,.14);color:#67e8f9;border:1px solid rgba(6,182,212,.34);}#lm-bhist:hover{background:rgba(6,182,212,.27);color:#fff;transform:translateY(-1px);}#lm-bhist.active{background:#0e7490;color:#fff;border-color:#22d3ee;}',
'#lm-bc{width:40px;height:40px;border:none;border-radius:8px;padding:0;background:rgba(239,68,68,.95);color:#fff;font-size:22px;font-weight:800;cursor:pointer;display:flex;align-items:center;justify-content:center;flex-shrink:0;box-shadow:0 8px 22px rgba(239,68,68,.22);}#lm-bc:hover{background:#f87171;}',
'#lm-brefresh{height:40px;padding:0 13px;gap:7px;border-radius:8px;background:rgba(59,130,246,.18);color:#b9d2ff;font-size:11px;font-weight:800;letter-spacing:.04em;text-transform:uppercase;cursor:pointer;display:flex;align-items:center;justify-content:center;flex-shrink:0;border:1px solid rgba(96,165,250,.42);white-space:nowrap;box-shadow:inset 0 1px 0 rgba(255,255,255,.08);}',
'#lm-brefresh:hover{background:rgba(59,130,246,.32);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:8px;flex-shrink:0;margin-left:auto;}',
'.lm-pop{display:none;position:fixed;top:0;left:0;z-index:9999999;background:rgba(15,23,42,.96);border:1px solid rgba(148,163,184,.22);border-radius:12px;padding:0;box-shadow:0 18px 52px rgba(0,0,0,.58);overflow:hidden;max-width:calc(100vw - 16px);backdrop-filter:blur(14px);}',
'.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;}',
'@media(max-width:600px){.lm-pop.on{left:8px!important;right:8px!important;width:auto!important;max-width:none!important;max-height:calc(100dvh - 130px);}}',
'.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-trap .lm-sc-val{margin-bottom:3px}.lm-sc-trap-row{display:flex;align-items:center;gap:5px;flex-wrap:wrap;margin-top:3px}.lm-sc-rooms{font-size:10px;color:rgba(255,133,133,.75);line-height:1.3}.lm-sc-hint{font-size:10px;color:rgba(255,255,255,.4)}.lm-sc-badge{display:inline-flex;align-items:center;padding:1px 6px;border-radius:999px;font-size:10px;font-weight:800;line-height:1.6;background:linear-gradient(135deg,rgba(255,100,100,.22),rgba(255,60,60,.12));border:1px solid rgba(255,100,100,.35);color:#ff9e9e;white-space:nowrap}',
'.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-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;}',
'.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-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;min-height:0;overflow:hidden;position:relative;cursor:grab;background:transparent;touch-action:none;}',
'#lm-mbody.dr{cursor:grabbing;}#lm-fc{position:absolute;top:0;left:0;}',
'#lm-info{position:absolute;z-index:3;bottom:14px;left:50%;transform:translateX(-50%);max-width:calc(100% - 148px);padding:7px 14px;border-radius:999px;background:rgba(7,11,22,.82);border:1px solid rgba(148,163,184,.22);color:rgba(226,232,240,.82);font-size:11px;font-weight:700;pointer-events:none;white-space:nowrap;overflow:hidden;text-overflow:ellipsis;box-shadow:0 10px 24px rgba(0,0,0,.28);backdrop-filter:blur(10px);}',
'#lm-zbtns{position:absolute;z-index:3;right:14px;top:14px;display:flex;flex-direction:column;gap:8px;}',
'.lm-zb{width:42px;height:42px;border:1px solid rgba(148,163,184,.26);border-radius:8px;background:rgba(15,23,42,.78);color:#fff;font-size:20px;font-weight:800;cursor:pointer;box-shadow:0 10px 26px rgba(0,0,0,.26);backdrop-filter:blur(10px);transition:background .14s,transform .14s,border-color .14s;}',
'.lm-zb:hover{background:rgba(37,99,235,.42);border-color:rgba(147,197,253,.5);transform:translateY(-1px);}',
'#lm-legend-bar{position:relative;z-index:4;display:flex;flex-wrap:wrap;gap:5px 7px;padding:8px 12px calc(8px + env(safe-area-inset-bottom));background:rgba(12,16,28,.9);border-top:1px solid rgba(148,163,184,.16);box-shadow:0 -12px 32px rgba(0,0,0,.28);flex-shrink:0;align-items:flex-start;overflow:hidden;backdrop-filter:blur(14px);}',
'.lm-lb-item{display:flex;align-items:center;gap:5px;min-height:24px;padding:3px 7px;border-radius:8px;background:rgba(255,255,255,.035);border:1px solid rgba(255,255,255,.06);font-size:10px;color:rgba(226,232,240,.72);white-space:nowrap;flex:0 0 auto;}',
'.lm-lb-ico{width:16px;height:16px;border-radius:4px;flex-shrink:0;display:flex;align-items:center;justify-content:center;font-size:10px;}',
'@media(max-width:860px){#lm-mhdr{padding-left:10px;padding-right:10px;}#lm-mhdr-btns{margin:0 -10px;padding:0 10px 2px;}#lm-b0,#lm-b1,#lm-bf,#lm-bpath,#lm-bguard,#lm-bclub,#lm-bhist{min-height:42px;padding:9px 12px;font-size:11px;flex:0 0 auto;}#lm-brefresh{height:40px;}#lm-zbtns{right:10px;top:10px;}.lm-zb{width:44px;height:44px;}#lm-info{bottom:12px;max-width:calc(100% - 116px);font-size:10px;padding:7px 11px;}#lm-legend-bar{gap:4px 6px;padding-left:10px;padding-right:10px;}.lm-lb-item{font-size:9px;padding:3px 6px;}}',
'@media(max-width:520px){#lm-mttl{font-size:13px;}#lm-brefresh{width:42px;padding:0;font-size:0;}#lm-brefresh .lm-ref-ico{font-size:15px;}#lm-bc{width:42px;height:42px;}#lm-zbtns{top:auto;bottom:58px;right:10px;flex-direction:row;}#lm-info{left:10px;right:108px;bottom:58px;transform:none;max-width:none;text-align:left;}.lm-lb-item span{max-width:72px;overflow:hidden;text-overflow:ellipsis;}}',
'#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-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-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-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;}',
'.labyrinth-cell.lm-known{position:relative!important;}',
'.lm-cell-icon{position:absolute!important;inset:0!important;display:flex!important;align-items:center!important;justify-content:center!important;font-size:clamp(5px,1.3vw,11px)!important;line-height:1!important;pointer-events:none!important;z-index:3!important;}',
'.labyrinth-cell.lm-known:not(.labyrinth-cell--visited):not(.labyrinth-cell--available):not(.labyrinth-cell--current){background:rgba(255,255,255,.07)!important;opacity:0.85!important;}',
].join('\n');
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 id="lm-ttl-dot"></div>Карта лабиринта</div>'+
'<div id="lm-btns">'+
'<button id="lm-cleanbtn" title="Скрыть иконки скрипта с карты">👁 Чистый вид</button>'+
'<button id="lm-rbtn">🔄 Обновить</button>'+
'<button id="lm-pbtn">⛶ Полная карта</button>'+
'</div>'+
'</div>'+
'<div id="lm-map-info">'+
'<div class="lm-mi-card">'+
'<div class="lm-mi-ico">🗺</div>'+
'<div style="min-width:0;flex:1">'+
'<div class="lm-mi-val" id="lm-mi-rooms-val">—</div>'+
'<div class="lm-mi-lbl">комнат в базе</div>'+
'<div class="lm-mi-sep"></div>'+
'<div class="lm-mi-val-sm" id="lm-mi-depth-val">—</div>'+
'<div class="lm-mi-lbl">макс. глубина</div>'+
'</div>'+
'</div>'+
'<div class="lm-mi-card" id="lm-mi-emission-card">'+
'<div class="lm-mi-ico" id="lm-mi-emission-ico">🌩</div>'+
'<div style="min-width:0;flex:1">'+
'<div class="lm-mi-val-sm" id="lm-mi-emission-val">—</div>'+
'<div class="lm-mi-lbl" id="lm-mi-emission-lbl">выброс</div>'+
'</div>'+
'</div>'+
'</div>'+
'<div id="lm-action-bar">'+
'<button class="lm-abar-btn" id="lm-wb-guard"><span class="lm-abar-ico">👑</span>Стражи</button>'+
'<button class="lm-abar-btn" id="lm-wb-club"><span class="lm-abar-ico">🛡</span>Клуб</button>'+
'<button class="lm-abar-btn" id="lm-wb-hist"><span class="lm-abar-ico">📜</span>История</button>'+
'</div>'+
'<div id="lm-inline-panel">'+
'<div id="lm-wi-guard" style="display:none"><div class="lm-wi-loading">Загрузка...</div></div>'+
'<div id="lm-wi-club" style="display:none"><div class="lm-wi-loading">Загрузка...</div></div>'+
'<div id="lm-wi-hist" style="display:none"><div class="lm-hist-body"><div class="lm-wi-loading">Загрузка...</div></div></div>'+
'</div>'+
'<canvas id="lm-cvs" style="display:none"></canvas>'+
'<div id="lm-st" style="display:none"></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" title="Обновить карту"><span class="lm-ref-ico">🔄</span> Обновить</button>'+
'<button id="lm-bc">×</button>'+
'</div>'+
'</div>'+
'<div id="lm-mhdr-btns">'+
'<button id="lm-b1" title="Перейти к моей позиции">⚔ Моя позиция</button>'+
'<button id="lm-b0" title="Центрировать старт">⚑ Старт</button>'+
'<button id="lm-bguard" title="Показать моих стражей">👑 Мои стражи</button>'+
'<button id="lm-bclub" title="Показать стражей клуба">🛡 Стражи клуба</button>'+
'<button id="lm-bhist" title="История лабиринта">📜 История</button>'+
'<button id="lm-bpath" title="Показать мой путь">👣 Мой путь</button>'+
'<button id="lm-bf" title="Фильтр комнат">⊞ Фильтр</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>';
modal.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>';
modal.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>';
modal.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>';
modal.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><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>Карта будет полностью обновлена. Может занять до 2 минут.</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, btnEl) {
var btn = btnEl || popEl.parentElement;
var bRect = btn.getBoundingClientRect();
popEl.style.top = (bRect.bottom + 8) + 'px';
popEl.style.left = bRect.left + 'px';
popEl.style.right = 'auto';
var pRect = popEl.getBoundingClientRect();
var over = pRect.right - (window.innerWidth - 8);
if (over > 0) popEl.style.left = Math.max(8, bRect.left - over) + 'px';
if (pRect.bottom > window.innerHeight - 8) {
popEl.style.top = Math.max(8, bRect.top - pRect.height - 8) + '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');
function wbClose() {
_wbActive = null;
document.getElementById('lm-inline-panel').classList.remove('on');
['lm-wb-guard','lm-wb-club','lm-wb-hist'].forEach(function(id){
var b = document.getElementById(id); if(b) b.classList.remove('active');
});
['lm-wi-guard','lm-wi-club','lm-wi-hist'].forEach(function(id){
var el = document.getElementById(id); if(el) el.style.display = 'none';
});
}
function wbOpen(tab) {
if (_wbActive === tab) { wbClose(); return; }
wbClose();
_wbActive = tab;
document.getElementById('lm-inline-panel').classList.add('on');
document.getElementById('lm-wb-'+tab).classList.add('active');
var panel = document.getElementById('lm-wi-'+tab);
if (panel) panel.style.display = 'block';
if (tab === 'guard') {
loadWbGuardian('user', document.getElementById('lm-wi-guard'));
} else if (tab === 'club') {
loadWbGuardian('club', document.getElementById('lm-wi-club'));
} else if (tab === 'hist') {
var hBody = document.querySelector('#lm-wi-hist .lm-hist-body');
if (hBody) {
if (Object.keys(histCache).length === 0) {
histTotal = null; histCurPage = 1;
hBody.innerHTML = '<div class="lm-wi-loading">Загрузка...</div>';
loadHistPage(1, function(result) { renderHistInline(result, 1); });
} else {
renderHistInline(histCache[histCurPage] || histCache[1] || {rows:[],totalPages:1}, histCurPage || 1);
}
}
}
}
document.getElementById('lm-wb-guard').addEventListener('click', function(){ wbOpen('guard'); });
document.getElementById('lm-wb-club').addEventListener('click', function(){ wbOpen('club'); });
document.getElementById('lm-wb-hist').addEventListener('click', function(){ wbOpen('hist'); });
var inlinePanel = document.getElementById('lm-inline-panel');
if (inlinePanel) inlinePanel.addEventListener('click', function(e){ e.stopPropagation(); });
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 = window.lmLS.get(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');
window.lmLS.set(LS_REFRESH_KEY, String(Date.now()));
doRefresh();
});
document.getElementById('lm-pbtn').addEventListener('click', function(){ openFull(); });
document.getElementById('lm-cleanbtn').addEventListener('click', function() {
cleanViewMode = !cleanViewMode;
this.classList.toggle('active', cleanViewMode);
this.textContent = cleanViewMode ? '👁 Карта скрипта' : '👁 Чистый вид';
if (cleanViewMode) applyCleanView(); else removeCleanView();
});
document.getElementById('lm-bc').addEventListener('click', closeFull);
document.getElementById('lm-brefresh').addEventListener('click', function() {
var conf2 = document.getElementById('lm-confirm2');
var timeEl2 = document.getElementById('lm-ctime2');
if (timeEl2) {
var lastT = window.lmLS.get(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');
});
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');
window.lmLS.set(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(){
highlightRoom={x:0,y:0,until:Date.now()+2000};
if(!fmOpen) { openFull(true); setTimeout(function(){ centerOn(0,0,FULL_FOCUS_SCALE); }, 50); }
else { centerOn(0,0,FULL_FOCUS_SCALE); }
});
document.getElementById('lm-b1').addEventListener('click', function(){
var c=curPos();
highlightRoom={x:c.x,y:c.y,until:Date.now()+2000};
if(!fmOpen) { openFull(true); setTimeout(function(){ centerOn(c.x,c.y,FULL_FOCUS_SCALE); }, 50); }
else { centerOn(c.x,c.y,FULL_FOCUS_SCALE); }
});
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?'👣 Все игроки':'👣 Мой путь';
if (showMyPath && !window._lmMyPathData) {
window.lmFetch(VPS_URL+'/my/path', { headers: vpsAuthHeaders(false) })
.then(function(r){ if (!r.ok) throw new Error('HTTP ' + r.status); return r.json(); })
.then(function(d){
if (d && Array.isArray(d.steps)) {
window._lmMyPathData = {};
d.steps.forEach(function(s){ window._lmMyPathData[s.x+'_'+s.y]=true; });
drawFull();
}
}).catch(function(){});
return;
}
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, this); }
});
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, this); }
});
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, this); }
});
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');
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, document.getElementById('lm-bhist'));
}
});
['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 modalPopupIds = ['lm-bguard','lm-bclub','lm-bf','lm-bhist'];
var insideModal = modalPopupIds.some(function(id){
var b = document.getElementById(id);
return b && b.contains(e.target);
});
if (!insideModal) closeAllPopups();
var wrap2 = document.getElementById('lm-wrap');
if (wrap2 && !wrap2.contains(e.target)) {
var panel = document.getElementById('lm-inline-panel');
if (panel && panel.classList.contains('on')) {
document.getElementById('lm-inline-panel').classList.remove('on');
['lm-wb-guard','lm-wb-club','lm-wb-hist'].forEach(function(id){
var b=document.getElementById(id); if(b) b.classList.remove('active');
});
['lm-wi-guard','lm-wi-club','lm-wi-hist'].forEach(function(id){
var el=document.getElementById(id); if(el) el.style.display='none';
});
_wbActive = null;
}
}
});
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; } });
var pinchDist0 = 0, pinchScale0 = 1, pinchFmX0 = 0, pinchFmY0 = 0, pinchCx = 0, pinchCy = 0;
var tapStartX = 0, tapStartY = 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);
}
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 });
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 mmAltFiltered = (room.altTypes||[]).filter(function(t){ return t!=='room_player'&&t!=='room_trap'&&t!=='room_gift'&&t!=='shield_block'; });
var mmAltLine = '';
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:'Всевидящее oko', desc:'Откроет тип следующей комнаты перед входом' },
'labyrinthBoostShieldBtn': { ico:'🛡', name:'Щит', desc:'Защитит от следующего штрафа в лабиринте' },
'labyrinthBoostRewardBtn': { ico:'🎯', name:'Гарантированная награда', desc:'Гарантирует награду в следующей комнате' },
'labyrinthBoostBerserkBtn': { ico:'⚔', name:'Режим берсерка', desc:'Усиливает урон по боссам' }
};
var _pendingBoostBtn = null, _pendingIsRoomAction = false, _pendingIsBuy = false;
function interceptRoomAction(btn, ico2, name, desc) {
if (!btn) return;
btn.addEventListener('click', function(e) {
if (_roomActionConfirmed) return;
e.stopImmediatePropagation(); e.preventDefault();
document.getElementById('lm-boost-conf-ico').textContent = ico2;
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 _roomActionConfirmed = false;
interceptRoomAction(document.getElementById('labyrinthPlaceTrapBtn'), '🕸', 'Оставить ловушку', 'Ловушка остановит первого игрока, который войдёт сюда. Стоимость — 1 ход.');
interceptRoomAction(document.getElementById('labyrinthPlaceGiftBtn'), '🎁', 'Оставить подарок', 'Подарок сразу достанется следующему игроку. Стоимость — 1 ход.');
interceptRoomAction(document.getElementById('labyrinthRobMineBtn'), '⚔', 'Ограбить шахту', 'Вы заберёте добычу из шахты другого игрока.');
interceptRoomAction(document.getElementById('labyrinthEchoRepeatBtn'), '👣', 'Повторить путь', 'Ты повторишь путь другого игрока в этой комнате.');
interceptRoomAction(document.getElementById('labyrinthClubWarSoftBtn'), '🏴', 'Лёгкий рейд', 'Небольшая атака на вражеский клуб.');
interceptRoomAction(document.getElementById('labyrinthClubWarNormalBtn'), '⚔', 'Обычный штурм', 'Стандартная атака на вражеский клуб.');
interceptRoomAction(document.getElementById('labyrinthClubWarFullBtn'), '💥', 'Полное вторжение', 'Мощная атака на вражеский клуб. Требует больше ресурсов.');
interceptRoomAction(document.getElementById('labyrinthHelpMineBtn'), '🤝', 'Помочь со сбором', 'Вы поможете другому игроку собрать добычу быстрее.');
var _mineConfirmed = false;
var collectMineBtn = document.getElementById('labyrinthCollectMineBtn');
if (collectMineBtn) {
collectMineBtn.addEventListener('click', function(e) {
if (_mineConfirmed) return;
e.stopImmediatePropagation(); e.preventDefault();
var pm = window.labyrinthData && window.labyrinthData.personalMine;
var desc = pm ? 'Забрать ' + (pm.pending_acc || 0) + ' АСС и ' + (pm.pending_cards || 0) + ' карт из шахты ур. ' + (pm.level || 1) + '.' : 'Забрать накопленную добычу из персональной шахты.';
document.getElementById('lm-boost-conf-ico').textContent = '⛏';
document.getElementById('lm-boost-conf-title').textContent = 'Забрать добычу?';
document.getElementById('lm-boost-conf-desc').textContent = desc;
_pendingBoostBtn = collectMineBtn; _pendingIsRoomAction = true;
document.getElementById('lm-boost-confirm').classList.add('on');
}, true);
}
var upgradeMineBtn = document.getElementById('labyrinthUpgradeMineBtn');
if (upgradeMineBtn) {
upgradeMineBtn.addEventListener('click', function(e) {
if (_mineConfirmed) return;
e.stopImmediatePropagation(); e.preventDefault();
var pm = window.labyrinthData && window.labyrinthData.personalMine;
var price = pm ? (pm.next_upgrade_price || pm.upgrade_price || 1000) : 1000;
var lvl = pm ? (pm.level || 1) : 1;
document.getElementById('lm-boost-conf-ico').textContent = '⬆';
document.getElementById('lm-boost-conf-title').textContent = 'Улучшить шахту?';
document.getElementById('lm-boost-conf-desc').textContent = 'Улучшение с уровня ' + lvl + ' до ' + (lvl + 1) + ' стоит ' + price + ' АСС.';
_pendingBoostBtn = upgradeMineBtn; _pendingIsRoomAction = true;
document.getElementById('lm-boost-confirm').classList.add('on');
}, true);
}
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 _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;
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, isRoom = _pendingIsRoomAction;
_pendingBoostBtn = null; _pendingIsBuy = false; _pendingIsRoomAction = false;
if (isBuy) { _buyConfirmed = true; }
else if (isRoom) { _roomActionConfirmed = true; _mineConfirmed = true; }
else { _boostConfirmed = true; }
btn.click();
_boostConfirmed = false; _buyConfirmed = false; _roomActionConfirmed = false; _mineConfirmed = false;
}
});
var tooltipPinned = false;
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 altFiltered2 = (room.altTypes||[]).filter(function(t){ return t!=='room_player'&&t!=='room_trap'&&t!=='room_gift'&&t!=='shield_block'; });
var altLine2 = '';
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>';
var _emKey = cx+'_'+cy;
var _emLine = (room.emissionVisited || (window._lmEmissionCells && window._lmEmissionCells.has(_emKey)))
? '<div style="color:#ffd700;font-size:11px;margin-bottom:2px;">⚡ Посещено во время выброса</div>' : '';
var _dispEv = (room.event === 'unknown' && room.emissionEv) ? room.emissionEv : room.event;
var _emOnlyStyle = (room.event === 'unknown' && room.emissionEv)
? ' style="border:1px solid rgba(255,60,60,0.7);padding:1px 5px;border-radius:3px;"' : '';
elTooltip.innerHTML = '<b'+_emOnlyStyle+'>'+(isCur?'⚔ Ваша позиция':ico(_dispEv)+' '+roomName(_dispEv))+'</b>'+
(!isCur&&roomDesc(room.event)?'<div class="tt-desc">'+roomDesc(room.event)+'</div>':'')+_emLine+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); });
elMbody.addEventListener('touchend', function(e) {
if (pinchDist0 > 0) return;
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, _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 style="height:3px;border-radius:999px;background:rgba(255,255,255,.15);margin-top:4px;overflow:hidden"><div style="height:3px;border-radius:999px;background:linear-gradient(90deg,#4a90e2,#67e8f9);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();
showToast('Обновление карты...', '', 10);
var p = steps.length > 0 ? pushSteps(steps, SESSION_ID) : Promise.resolve();
p.then(function() {
showToast('Обновление карты...', '', 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() {
showToast('Обновление карты...', '', 55);
return new Promise(function(resolve) { loadCloud(resolve, function(count){ showToast('Обновление карты...', '', Math.min(88,55+Math.floor(count/500))); }); });
}).then(function() {
histCache = {}; histTotal = null; histCurPage = 1;
window.lmLS.set(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('Обновление карты...', '', pct);
if (pageNum < total) loadPageSeq(pageNum + 1).then(resolve);
else resolve();
});
}, pageNum === 1 ? 0 : 700);
});
}
showToast('Обновление карты...', '', 60);
return loadPageSeq(1);
}).then(function() {
var pop = document.getElementById('lm-hpop');
if (pop && pop.classList.contains('on')) renderHistPopup(histCache[1] || {rows:[], totalPages:1}, 1);
checkAndSyncClub(true);
showToast('Карта обновлена!', '', 100);
hideToast(3000);
btn.classList.remove('loading'); btn.disabled = false;
}).catch(function(e) {
showToast('Ошибка обновления', 'Попробуйте ещё раз');
hideToast(4000);
btn.classList.remove('loading'); btn.disabled = false;
});
}
function fetchTrapBacksFromRollbackPage(callback) {
var username = window.visitor_name || '';
if (!username) { callback(null); return; }
fetch('/rollback/', { credentials: 'same-origin' })
.then(function(r){ if (!r.ok) throw new Error('HTTP ' + r.status); return r.text(); })
.then(function(html){
var parser = new DOMParser();
var doc = parser.parseFromString(html, 'text/html');
var found = null;
var userLink = doc.querySelector('a[href="/user/' + username + '/"].card-inline');
if (userLink) {
var valEl = userLink.querySelector('.users-top__value');
if (valEl) {
var m = valEl.textContent.replace(/[^0-9]/g, '');
if (m) found = parseInt(m, 10);
}
}
if (found === null) {
var allCards = doc.querySelectorAll('.ncard__users-toplist .card-inline');
allCards.forEach(function(card) {
if (found !== null) return;
var nameEl = card.querySelector('.card-inline__name');
if (!nameEl) return;
var name = nameEl.textContent.trim();
if (name.toLowerCase() !== username.toLowerCase()) return;
var valEl2 = card.querySelector('.users-top__value');
if (valEl2) {
var m2 = valEl2.textContent.replace(/[^0-9]/g, '');
if (m2) found = parseInt(m2, 10);
}
});
}
var place = null;
if (userLink) {
var p = parseInt(userLink.getAttribute('data-place') || '', 10);
if (!isNaN(p)) place = p;
} else if (found !== null) {
var allCards2 = doc.querySelectorAll('.ncard__users-toplist .card-inline');
allCards2.forEach(function(card) {
if (place !== null) return;
var nameEl = card.querySelector('.card-inline__name');
if (!nameEl || nameEl.textContent.trim().toLowerCase() !== username.toLowerCase()) return;
var p2 = parseInt(card.getAttribute('data-place') || '', 10);
if (!isNaN(p2)) place = p2;
});
}
if (place !== null) window._lmRollbackPlace = place;
lmDebug('[LabMap] /rollback/ откатов для', username, ':', found, '| Топ: №' + place);
callback(found);
})
.catch(function(e){
lmDebug('[LabMap] fetchTrapBacksFromRollbackPage:', e);
callback(null);
});
}
function doRefresh() {
var btn=document.getElementById('lm-rbtn');
btn.classList.add('loading'); btn.disabled=true;
showToast('Обновление карты...', '', 10);
setTimeout(function() {
lmDebug('[Синхронизация] Автосинхронизация после обновления...');
var tb = window._lmAllTimeTrapBacks;
var tp = window._lmRollbackPlace;
if (tb != null && tb > 0) {
lmDebug('[Синхронизация] Отправляем откаты:', tb);
sendRollbackToVPS(tb, tp || null);
}
sendAccHistoryToVPS();
collectAndSendGuardians([]);
setTimeout(function(){ checkAndSyncClub(true); }, 1000);
}, 5000);
syncCacheWithSession();
flushStepsToServer();
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('Обновление карты...', '', 30);
return new Promise(function(resolve){
ownershipCache = null;
guardianDataCache = {user: null, club: null};
fetchOwnership(function(hist){
var allOwn=domOwn.concat(hist);
var p1 = allOwn.length ? pushSteps(allOwn,SESSION_ID+'_own').catch(function(){}) : Promise.resolve();
p1.then(function(){
collectAndSendGuardians(allOwn);
});
p1.then(resolve).catch(resolve);
});
});
}).then(function(){
showToast('Обновление карты...', '', 55);
return new Promise(function(resolve){ loadCloud(resolve, function(count){ showToast('Загрузка карты...', 'Карта: '+(count>=1000?Math.floor(count/1000)+'k ':count+' ')+'комнат', Math.min(88,55+Math.floor(count/500))); }); });
}).then(function(){
showToast('Обновление карты...', '', 75);
var _ra = getAuth() || {};
return Promise.all([
window.lmFetch(VPS_URL+'/my/path', { headers: vpsAuthHeaders(false) })
.then(function(r){ if (!r.ok) throw new Error('HTTP ' + r.status); return r.json(); })
.then(function(d){
if (d && Array.isArray(d.steps)) {
window._lmMyPathData = {};
d.steps.forEach(function(s){ window._lmMyPathData[s.x+'_'+s.y]=true; });
lmDebug('[LabMap] My path:', d.steps.length, 'комнат из базы');
}
}).catch(function(){}),
new Promise(function(resolveStats){
fetchTrapBacksFromRollbackPage(function(rollbackCount) {
if (rollbackCount !== null && rollbackCount > 0) {
window._lmAllTimeTrapBacks = rollbackCount;
lmDebug('[LabMap] /rollback/ откатов найдено:', rollbackCount);
lmDebug('[Синхронизация] Вызываем sendRollbackToVPS...', typeof sendRollbackToVPS);
sendRollbackToVPS(rollbackCount, window._lmRollbackPlace || null);
resolveStats();
} else {
window.lmFetch(WORKER_URL+'/my-stats?username='+encodeURIComponent(_ra.username||'')+'&token='+(_ra.token||''))
.then(function(r){ if (!r.ok) throw new Error('HTTP ' + r.status); return r.json(); })
.then(function(d){
if (d.trap_backs !== undefined) {
var fromDb = d.trap_backs;
var prev = window._lmAllTimeTrapBacks || 0;
window._lmAllTimeTrapBacks = Math.max(fromDb, rollbackCount || 0, prev);
window._lmAllTimeTotal = d.profile && d.profile.total_steps || 0;
lmDebug('[LabMap] D1 откатов:', fromDb, '→', window._lmAllTimeTrapBacks);
}
resolveStats();
}).catch(resolveStats);
}
});
}).then(function(){
var hpop = document.getElementById('lm-hpop');
if (hpop && hpop.classList.contains('on')) {
renderHistPopup(histCache[histCurPage||1] || {rows:[],totalPages:1}, histCurPage||1);
}
var whist = document.getElementById('lm-wi-hist');
if (whist && whist.style.display !== 'none') {
renderHistInline(histCache[histCurPage||1] || {rows:[],totalPages:1}, histCurPage||1);
}
})
]);
}).then(function(){
lmDebug('[Синхронизация] Отложенная отправка ACC-истории запущена');
setTimeout(function() { sendAccHistoryToVPS(); }, 500);
btn.classList.remove('loading'); btn.disabled=false;
invalidateRoomsCache(); drawMini();
if (fmOpen && showMyPath) drawFull();
showToast('Карта обновлена!', '', 100);
hideToast(2500);
scheduleProfileSync();
var currentMine = window.labyrinthData && window.labyrinthData.personalMine;
if (currentMine && currentMine.has_mine) {
var currentPosition = curPos();
sendMineData(currentPosition.x, currentPosition.y, {
level: Number(currentMine.level || 0),
storage_pct: Number(currentMine.storage_progress || 0),
acc_inside: Number(currentMine.pending_acc || currentMine.acc || 0),
cards_current: Number(currentMine.pending_cards || currentMine.cards || 0),
cards_max: Number(currentMine.max_cards || currentMine.cards_max || 0)
});
}
setTimeout(function(){ checkAndSyncClub(true); }, 1000);
}).catch(function(e){
lmDebug('[LabMap] refresh err:',e);
btn.classList.remove('loading'); btn.disabled=false;
});
}
var guardianDataCache = {user:null, club:null};
function collectAndSendGuardians(ownershipData) {
var VPS_API = LM_API_BASE;
var auth = getAuth();
if (!auth) return;
var username = auth.username, token = auth.token;
var guardians = [];
if (ownershipData && ownershipData.length) {
ownershipData.forEach(function(item) {
if (!item || item.x == null || item.y == null) return;
var ev = item.event_type || item.ev || item.guardian || '';
var isClub = ev === 'guardian_club' || item.guardian_type === 'club';
var isPersonal = ev === 'guardian_user' || item.guardian_type === 'personal' ||
ev.includes('own') || ev === 'protected';
if (!isClub && !isPersonal) return;
guardians.push({
x: Number(item.x),
y: Number(item.y),
owner: item.owner || item.username || (isPersonal ? username : null),
card_name: item.card_name || item.cardName || null,
card_rank: item.card_rank || item.cardRank || null,
tribute_count: item.tribute_count != null ? Number(item.tribute_count) : null,
acc_per_pass: item.acc != null ? Number(item.acc) :
item.accPerPass != null ? Number(item.accPerPass) : null,
guardian_type: isClub ? 'club' : 'personal',
club_id: item.club_id || item.clubId || null,
club_name: item.club_name || item.clubName || null,
captured_by: item.captured_by || item.capturedBy || item.owner || item.username ||
(isPersonal ? username : null),
captured_at: item.captured_at || item.capturedAt || null,
confirmed: true,
source: 'ownership'
});
});
}
var guardianRows = document.querySelectorAll(
'.labyrinth-guardian-item, .guardian-row, [data-guardian], .ncard__guard-item'
);
guardianRows.forEach(function(el) {
var xEl = el.querySelector('[data-x], .coord-x');
var yEl = el.querySelector('[data-y], .coord-y');
var ownerEl = el.querySelector('.guardian-owner, .owner-name, .card-inline__name');
var cardEl = el.querySelector('.card-name, .guardian-card');
var x = xEl ? parseInt(xEl.getAttribute('data-x') || xEl.textContent) : null;
var y = yEl ? parseInt(yEl.getAttribute('data-y') || yEl.textContent) : null;
if (x == null || y == null) {
var text = el.textContent || '';
var m = text.match(/\(?\s*(-?\d+)\s*,\s*(-?\d+)\s*\)?/);
if (m) { x = parseInt(m[1]); y = parseInt(m[2]); }
}
if (x == null || y == null) return;
var rowText = (el.textContent || '').toLowerCase();
var isClubGuardian = el.matches('[data-guardian="club"],.club-guardian') ||
rowText.indexOf('клуб') >= 0;
var parsedOwner = ownerEl ? ownerEl.textContent.trim() : null;
guardians.push({
x: x,
y: y,
owner: parsedOwner || (!isClubGuardian ? username : null),
card_name: cardEl ? cardEl.textContent.trim() : null,
guardian_type: isClubGuardian ? 'club' : 'personal',
captured_by: parsedOwner || (!isClubGuardian ? username : null),
captured_at: null,
confirmed: true,
source: 'labyrinth_dom'
});
});
var labData = window.labyrinthData;
if (labData && labData.guardians && labData.guardians.length) {
labData.guardians.forEach(function(g) {
if (g.x == null || g.y == null) return;
guardians.push({
x: g.x,
y: g.y,
owner: g.owner || g.username || username,
card_name: g.card_name || g.cardName || null,
card_rank: g.card_rank || g.cardRank || null,
tribute_count: g.tribute_count != null ? Number(g.tribute_count) : null,
acc_per_pass: g.acc_per_pass != null ? Number(g.acc_per_pass) :
g.acc != null ? Number(g.acc) : null,
guardian_type: (g.guardian_type === 'club' || g.guardian === 'guardian_club') ? 'club' : 'personal',
club_id: g.club_id || null,
club_name: g.club_name || null,
captured_by: g.captured_by || g.capturedBy || username,
captured_at: g.captured_at || g.capturedAt || null,
confirmed: true,
source: 'labyrinth_data'
});
});
}
if (!guardians.length) {
lmDebug('[LGuard] Стражи не найдены');
return;
}
var guardianByRoom = {};
guardians.forEach(function(g) {
if (!Number.isInteger(Number(g.x)) || !Number.isInteger(Number(g.y))) return;
var key = Number(g.x) + '_' + Number(g.y);
var score = (g.confirmed ? 8 : 0) + (g.owner ? 4 : 0) +
(g.card_name ? 2 : 0) + (g.club_id || g.club_name ? 2 : 0) +
(g.card_rank ? 1 : 0);
if (!guardianByRoom[key] || score > guardianByRoom[key]._score) {
guardianByRoom[key] = Object.assign({}, g, {_score: score});
}
});
guardians = Object.keys(guardianByRoom).map(function(key) {
var g = guardianByRoom[key];
delete g._score;
return g;
}).filter(function(g) {
return g.confirmed && (g.owner || g.card_name || g.club_id || g.club_name);
});
window.lmFetch(VPS_API + '/guardians', {
method: 'POST',
headers: vpsAuthHeaders(true),
body: JSON.stringify({ guardians: guardians }),
})
.then(function(r){ if (!r.ok) throw new Error('HTTP ' + r.status); return r.json(); })
.then(function(res){
lmDebug('[LGuard] Стражи отправлены:', res.saved, '/', guardians.length);
})
.catch(function(e){
lmDebug('[LGuard] Ошибка отправки стражей:', e.message);
});
}
var _clubSyncPromise = null;
var _clubSyncLastAt = 0;
function checkAndSyncClub(force) {
var now = Date.now();
if (_clubSyncPromise) return _clubSyncPromise;
if (!force && now - _clubSyncLastAt < 15000) return Promise.resolve(false);
_clubSyncLastAt = now;
_clubSyncPromise = Promise.resolve()
.then(collectAndSendClubRooms)
.catch(function() { return false; })
.then(function(result) {
_clubSyncPromise = null;
return result;
}, function() {
_clubSyncPromise = null;
return false;
});
return _clubSyncPromise;
}
function collectAndSendClubRooms() {
var auth = getAuth();
if (!auth) return Promise.resolve(false);
var username = auth.username, token = auth.token;
function parseClubDate(raw) {
var text = String(raw || '').replace(/\s+/g, ' ').trim();
var m = text.match(/(\d{2})\.(\d{2})\.(\d{4})\s*(?:в|,)?\s*(\d{1,2}):(\d{2})/i);
if (!m) return null;
var d = new Date(Number(m[3]), Number(m[2]) - 1, Number(m[1]), Number(m[4]), Number(m[5]), 0);
return Number.isNaN(d.getTime()) ? null : d.toISOString();
}
function parseMember(item) {
var nameEl = item.querySelector('.club__member-name');
if (!nameEl) return null;
var name = nameEl.textContent.trim();
if (!name) return null;
var statusEl = item.querySelector('.club__member-status');
var contributionEl = item.querySelector('.club__member-contribution');
var imageEl = item.querySelector('.club__member-image img');
var status = statusEl ? statusEl.textContent.trim() : '';
var role = 'member';
if (/^Гильдмастер$/i.test(status)) role = 'leader';
else if (/Зам\.?\s*Гильдмастера/i.test(status)) role = 'deputy';
return {
username: name,
role: role,
status: status || null,
contribution: contributionEl ? Number((contributionEl.textContent || '').replace(/\D+/g, '')) || 0 : 0,
is_online: item.classList.contains('club__member--online'),
avatar: imageEl ? (imageEl.getAttribute('src') || null) : null
};
}
function parseClubDocument(doc, clubUrl) {
var root = doc.querySelector('.nclub.nclub-enter[data-id], .nclub-enter[data-id]');
var rooms = [];
var members = [];
var dataId = root && root.getAttribute('data-id');
var urlId = String(clubUrl || '').match(/\/clubs\/(\d+)(?:\/|$)/);
var clubId = dataId && /^\d+$/.test(dataId) ? Number(dataId) :
urlId ? Number(urlId[1]) : null;
var nameEl = doc.querySelector('.nclub-enter__main-name > div:first-child');
var membersTextEl = doc.querySelector('.nclub-enter__main-members');
var levelInfoEl = doc.querySelector('.nclub-enter__lvl-info > div:first-child');
var activityEls = doc.querySelectorAll('.nclub-enter__activity-item');
doc.querySelectorAll('.nclub-enter__members-list .club__member').forEach(function(item) {
var member = parseMember(item);
if (member) members.push(member);
});
var leaderMember = members.find(function(m) { return m.role === 'leader'; });
var memberCount = members.length;
if (membersTextEl) {
var countMatch = membersTextEl.textContent.match(/(\d+)\s+участник/i);
if (countMatch) memberCount = Number(countMatch[1]);
}
var enlightenment = null;
if (membersTextEl) {
var lightMatch = membersTextEl.textContent.match(/просветлен\w*\s+(\d+)/i);
if (lightMatch) enlightenment = Number(lightMatch[1]);
}
if (enlightenment == null && activityEls.length > 1) {
var n = Number((activityEls[1].querySelector('div') || {}).textContent || '');
if (Number.isFinite(n)) enlightenment = n;
}
var level = null;
if (levelInfoEl) {
var levelMatch = levelInfoEl.textContent.match(/(\d+)\s+уровень/i);
if (levelMatch) level = Number(levelMatch[1]);
}
var info = {
id: clubId,
name: nameEl ? nameEl.textContent.trim() : null,
leader: leaderMember ? leaderMember.username : null,
member_count: memberCount || 0,
enlightenment: enlightenment,
level: level
};
doc.querySelectorAll('.club-labyrinth__item').forEach(function(item) {
var roomEl = item.querySelector('.club-labyrinth__room');
if (!roomEl) return;
var coords = parseRoomText(roomEl.textContent || '');
if (!coords || !Number.isInteger(Number(coords.x)) || !Number.isInteger(Number(coords.y))) return;
var ownerEl = item.querySelector('.club-labyrinth__text b');
var dateEl = item.querySelector('.club-labyrinth__date');
var badgeEl = item.querySelector('.club-labyrinth__badge');
var avatarEl = item.querySelector('.club-labyrinth__avatar');
var capturedBy = ownerEl ? ownerEl.textContent.trim() : null;
rooms.push({
x: Number(coords.x),
y: Number(coords.y),
club_id: info.id,
club_name: info.name,
club_owner: capturedBy,
guardian_type: 'club',
card_name: null,
card_rank: null,
captured_by: capturedBy,
captured_at: dateEl ? parseClubDate(dateEl.textContent) : null,
is_new: !!badgeEl,
avatar: avatarEl ? (avatarEl.getAttribute('src') || null) : null,
confirmed: true,
source: 'club_page'
});
});
return {club: info, rooms: rooms, members: members};
}
function send(payload) {
var hasClub = payload && payload.club && (payload.club.id || payload.club.name);
var hasRooms = payload && Array.isArray(payload.rooms) && payload.rooms.length;
var hasMembers = payload && Array.isArray(payload.members) && payload.members.length;
if (!hasClub && !hasRooms && !hasMembers) return Promise.resolve(false);
return window.lmFetch(VPS_URL + '/club-rooms', {
method: 'POST',
headers: vpsAuthHeaders(true),
body: JSON.stringify(payload)
}).then(function(r) {
if (!r.ok) throw new Error('HTTP ' + r.status);
return r.json();
}).then(function(res) {
return true;
}).catch(function(e) {
lmDebug('[Карта] club sync send:', e);
return false;
});
}
function fetchText(url) {
return fetch(url, {credentials: 'same-origin'}).then(function(r) {
if (!r.ok) throw new Error('HTTP ' + r.status);
return r.text();
});
}
var parser = new DOMParser();
var currentPath = window.location.pathname || '';
if (/^\/clubs\/\d+\/?$/.test(currentPath) && document.querySelector('.nclub-enter[data-id]')) {
return send(parseClubDocument(document, currentPath));
}
return fetchText('/user/' + encodeURIComponent(username) + '/')
.then(function(html) {
var profileDoc = parser.parseFromString(html, 'text/html');
var clubLink = profileDoc.querySelector('.usn__club-item-top a[href*="/clubs/"]');
if (!clubLink) {
var links = profileDoc.querySelectorAll('a[href*="/clubs/"]');
for (var i = 0; i < links.length; i++) {
var href = links[i].getAttribute('href') || '';
if (/\/clubs\/\d+\/?/.test(href)) {
clubLink = links[i];
break;
}
}
}
if (!clubLink) throw new Error('club_link_not_found');
var href = clubLink.getAttribute('href');
return fetchText(href).then(function(clubHtml) {
return send(parseClubDocument(parser.parseFromString(clubHtml, 'text/html'), href));
});
})
.catch(function(e) {
lmDebug('[Карта] club sync:', e);
return false;
});
}
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();
fetch('/user/'+encodeURIComponent(username)+'/')
.then(function(r){ if (!r.ok) throw new Error('HTTP ' + r.status); return r.text(); })
.then(function(html){
var doc=parser.parseFromString(html,'text/html');
if(type==='user'){
var items=parseUserGuardianItems(doc);
guardianDataCache.user=items; renderGuardianList(type,items,listEl,countEl);
} else {
var clubLink=findClubLink(doc);
if(!clubLink){ listEl.textContent='Клуб не найден.'; return; }
return fetch(clubLink.getAttribute('href')).then(function(r){ if (!r.ok) throw new Error('HTTP ' + r.status); return r.text(); })
.then(function(ch){
var items=parseClubGuardianItems(parser.parseFromString(ch,'text/html'));
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();
highlightRoom = {x: it.x, y: it.y, until: Date.now() + 3000};
openFull(true);
setTimeout(function() { centerOn(it.x, it.y, FULL_FOCUS_SCALE); }, 50);
});
listEl.appendChild(div);
});
}
function loadWbGuardian(type, container) {
if (!container) return;
if (guardianDataCache[type]) { renderWbGuardianList(type, guardianDataCache[type], container); return; }
container.innerHTML = '<div class="lm-wi-loading">Загрузка...</div>';
var username = window.visitor_name || '';
if (!username) { container.innerHTML = '<div class="lm-wi-empty">Пользователь не определён</div>'; return; }
var parser = new DOMParser();
fetch('/user/'+encodeURIComponent(username)+'/')
.then(function(r){ if (!r.ok) throw new Error('HTTP ' + r.status); return r.text(); })
.then(function(html){
var doc = parser.parseFromString(html, 'text/html');
if (type === 'user') {
var items = parseUserGuardianItems(doc);
guardianDataCache.user = items; renderWbGuardianList('user', items, container);
} else {
var clubLink = findClubLink(doc);
if (!clubLink) { container.innerHTML = '<div class="lm-wi-empty">Клуб не найден</div>'; return; }
return fetch(clubLink.getAttribute('href')).then(function(r){ if (!r.ok) throw new Error('HTTP ' + r.status); return r.text(); })
.then(function(ch){
var items = parseClubGuardianItems(parser.parseFromString(ch, 'text/html'));
guardianDataCache.club = items; renderWbGuardianList('club', items, container);
});
}
}).catch(function(){ container.innerHTML = '<div class="lm-wi-empty">Ошибка загрузки</div>'; });
}
function renderWbGuardianList(type, items, container) {
if (!items.length) { container.innerHTML = '<div class="lm-wi-empty">Нет данных</div>'; return; }
var html = items.map(function(it){
return '<div class="lm-wi-item" data-x="'+it.x+'" data-y="'+it.y+'">'+
(it.img ? '<img class="lm-wi-img" src="'+it.img+'" alt="">' : '')+
'<div class="lm-wi-info"><div class="lm-wi-name">'+it.name+'</div><div class="lm-wi-room">'+it.room+'</div><div class="lm-wi-date">'+it.date+'</div></div>'+
'<div class="lm-wi-nav">→</div></div>';
}).join('');
container.innerHTML = html;
container.querySelectorAll('.lm-wi-item').forEach(function(el){
el.addEventListener('click', function(){
var x = parseInt(this.getAttribute('data-x'), 10);
var y = parseInt(this.getAttribute('data-y'), 10);
highlightRoom = {x: x, y: y, until: Date.now() + 3000};
openFull(true); setTimeout(function(){ centerOn(x, y, FULL_FOCUS_SCALE); }, 50);
});
});
}
function renderHistInline(result, page) {
var container = document.querySelector('#lm-wi-hist .lm-hist-body');
if (!container) return;
var totalPages = histTotal || result.totalPages || 1;
var rows = result.rows;
var stats = buildStepsStats();
var statsHtml = '';
if (stats) {
var acc = buildAccTotals();
statsHtml =
'<div class="lm-sc-grid" style="margin:10px;">'+
'<div class="lm-sc-card"><div class="lm-sc-label">Всего комнат</div><div class="lm-sc-val">'+stats.allTimeTotal+'</div></div>'+
'<div class="lm-sc-card lm-sc-trap">' +
'<div class="lm-sc-label">Откаты</div>' +
'<div class="lm-sc-val lm-sc-bad">' + (stats.historyLoaded ? (stats.trapCount > 0 ? '−'+stats.trapCount : '0') : '?') + '</div>' +
'<div class="lm-sc-trap-row">' +
(stats.historyLoaded && stats.lostRooms > 0
? '<span class="lm-sc-rooms">−'+stats.lostRooms+' комнат</span>'
: '<span class="lm-sc-hint">'+(stats.historyLoaded?'нет откатов':'нажми Обновить')+'</span>') +
(window._lmRollbackPlace ? '<span class="lm-sc-badge">Топ #'+window._lmRollbackPlace+'</span>' : '') +
'</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>'+
'<div class="lm-sc-card"><div class="lm-sc-label">Потеряно</div><div class="lm-sc-val lm-sc-bad">−'+acc.lost+'</div></div>'+
'</div>';
}
var rowsHtml = rows.length
? 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+'</div></div>'+
'<div class="lm-hr-amt '+amtCls+'">'+row.amount+(row.amount.indexOf('ACC')===-1?' ACC':'')+'</div>'+
'</div>';
}).join('')
: '<div class="lm-hist-empty">Записей не найдено (стр. '+page+')</div>';
var pagHtml = '';
if (totalPages > 1) {
var start = Math.max(1,page-2), end = Math.min(totalPages,page+2), 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-wi-hprev" '+(page<=1?'disabled':'')+'>‹</button>'+nums+
'<button class="lm-hp-nav" id="lm-wi-hnext" '+(page>=totalPages?'disabled':'')+'>›</button>'+
'</div>';
}
container.innerHTML = statsHtml +
'<div class="lm-hist-rows-head" style="padding:4px 10px;">📜 Страница '+page+'</div>'+
'<div class="lm-hist-rows" style="padding:0 10px 8px;">'+rowsHtml+'</div>'+pagHtml;
container.querySelectorAll('.lm-hp-btn[data-p]').forEach(function(btn){
btn.addEventListener('click', function(e){
e.stopPropagation();
var p = parseInt(this.dataset.p,10);
histCurPage = p;
container.innerHTML = '<div class="lm-wi-loading">Загрузка стр. '+p+'...</div>';
loadHistPage(p, function(r){ renderHistInline(r, p); });
});
});
var prev = container.querySelector('#lm-wi-hprev');
var next = container.querySelector('#lm-wi-hnext');
if(prev) prev.addEventListener('click', function(e){
e.stopPropagation();
if(page>1){ histCurPage=page-1; container.innerHTML='<div class="lm-wi-loading">Загрузка...</div>'; loadHistPage(page-1,function(r){renderHistInline(r,page-1);}); }
});
if(next) next.addEventListener('click', function(e){
e.stopPropagation();
if(page<totalPages){ histCurPage=page+1; container.innerHTML='<div class="lm-wi-loading">Загрузка...</div>'; loadHistPage(page+1,function(r){renderHistInline(r,page+1);}); }
});
}
function buildUI() {
if (document.getElementById('lm-wrap')) return;
injectStyles(); buildDOM(); bindEvents();
lmInfo('Карта готова');
}
var LS_AUTO_SYNC_KEY = 'lm_last_auto_sync' + _lmUserSuffix;
var AUTO_SYNC_INTERVAL = 24 * 60 * 60 * 1000;
function autoSyncIfNeeded() {
var lastSync = parseInt(window.lmLS.get(LS_AUTO_SYNC_KEY) || '0', 10);
var now = Date.now();
if (now - lastSync < AUTO_SYNC_INTERVAL) {
var cacheLoaded = loadFromCache();
var LS_CLOUD_UPDATED = 'lm_cloud_last_updated';
var lastCloud = parseInt(window.lmLS.get(LS_CLOUD_UPDATED) || '0', 10);
// Завжди перевіряємо версію на сервері — loadCloud сам вирішить чи потрібне оновлення IDB
loadCloud(function() {
window.lmLS.set(LS_CLOUD_UPDATED, String(Date.now()));
drawMini();
if (fmOpen) drawFull();
});
return;
}
setTimeout(function() {
showToast('Обновление карты...', '', 5);
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('Обновление карты...', '', 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, function(count) {
var pct = Math.min(88, 50 + Math.floor(count / 500));
showToast('Обновление карты...', '', pct);
});
});
}).then(function() {
histCache = {}; histTotal = null; histCurPage = 1;
window.lmLS.set(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;
showToast('Авто-синхронизация...', 'История: стр. '+pageNum+' из '+total, Math.round(55 + (pageNum / Math.max(total,1)) * 40));
if (pageNum < total && pageNum < 20) autoLoadPage(pageNum + 1).then(resolve);
else resolve();
});
}, pageNum === 1 ? 0 : 700);
});
}
return autoLoadPage(1);
}).then(function() {
window.lmLS.set(LS_AUTO_SYNC_KEY, String(Date.now()));
showToast('Авто-синхронизация завершена!', 'Следующая через 24 часа', 100);
hideToast(3000); drawMini();
}).catch(function(e) { lmDebug('[LabMap] Auto-sync error:', e); hideToast(1000); });
}, 3000);
}
function init() {
lmInfo('Карта включена');
syncCacheWithSession();
var _lastFlushCount = parseInt(window.lmLS.get(LS_FLUSH_COUNT) || '0');
var _currentSteps = mapData() && mapData().steps ? mapData().steps.length : 0;
var _cachedSteps = getCachedSteps();
var _sentIdx = getSentCount();
var _hasPending = _cachedSteps.length > _sentIdx;
if (_sentIdx >= _cachedSteps.length && _currentSteps <= _lastFlushCount) {
_hasPending = false;
}
if (_currentSteps > _lastFlushCount || _hasPending) {
lmDebug('[LabMap] Отправка при запуске: шагов=' + _currentSteps + ' lastFlush=' + _lastFlushCount + ' pending=' + _hasPending);
flushStepsToServer();
} else {
lmDebug('[LabMap] Новых данных нет — отправка пропущена');
}
window.addEventListener('beforeunload', function() {
var d = mapData();
if (d && d.steps && d.steps.length) cacheSteps(d.steps);
});
buildUI(); drawMini(); injectTopBtn();
autoSyncIfNeeded();
patchSiteMapDelayed();
setTimeout(function() {
fetchTrapBacksFromRollbackPage(function(count) {
if (count !== null) {
window._lmAllTimeTrapBacks = count;
lmDebug('[LabMap] /rollback/ откатов при старте:', count);
}
});
}, 1500);
var lastN = mapData()&&mapData().steps ? mapData().steps.length : 0;
var c0 = curPos();
var lastPos = c0.x+'_'+c0.y;
var _stepProcessing = false;
var _pendingStep = null;
(function() {
var mapEl = document.getElementById('labyrinthMap');
if (!mapEl) { lmDebug('[LabMap] labyrinthMap not found'); return; }
mapEl.addEventListener('click', function(e) {
if (_lmPatching || _stepProcessing) return;
var cell = e.target.closest
? e.target.closest('.labyrinth-cell--available')
: (e.target.classList.contains('labyrinth-cell--available') ? e.target : null);
if (!cell) return;
var cur = curPos();
var mapElLocal = document.getElementById('labyrinthMap');
var cells = mapElLocal.querySelectorAll('.labyrinth-cell');
var center = detectGridCenter(cells);
if (!center) return;
var cellsArr = Array.prototype.slice.call(cells);
var idx = cellsArr.indexOf(cell);
if (idx < 0) return;
var gx, gy;
if (center.useIndex) { gx = idx % 25; gy = Math.floor(idx / 25); }
else {
gx = parseInt(cell.getAttribute('data-x') || '', 10);
gy = parseInt(cell.getAttribute('data-y') || '', 10);
if (isNaN(gx) || isNaN(gy)) { gx = idx % 25; gy = Math.floor(idx / 25); }
}
var targetX = cur.x + (gx - center.gx);
var targetY = cur.y + (gy - center.gy);
lmDebug('[LabMap] cell click target:', targetX, targetY);
_pendingStep = {cx: targetX, cy: targetY};
_stepProcessing = true;
waitForStep(targetX, targetY);
}, true);
var obs = new MutationObserver(function(mutations) {
if (_lmPatching) return;
var hasCurrentChange = false;
for (var i = 0; i < mutations.length; i++) {
var m = mutations[i];
if (m.type === 'attributes' && m.attributeName === 'class') {
if (m.target.classList &&
(m.target.classList.contains('labyrinth-cell--current') ||
(m.oldValue && m.oldValue.indexOf('labyrinth-cell--current') >= 0))) {
hasCurrentChange = true; break;
}
}
if (m.type === 'childList' && m.addedNodes.length > 10) { hasCurrentChange = true; break; }
}
if (!hasCurrentChange) return;
var d = window.labyrinthData;
if (!d || !d.mapData || !d.mapData.current) return;
var cx = d.mapData.current.x, cy = d.mapData.current.y;
var pos = cx + '_' + cy;
if (_stepProcessing && _pendingStep && _pendingStep.cx === cx && _pendingStep.cy === cy) {
lastPos = pos; return;
}
if (pos === lastPos) return;
if (_stepProcessing) return;
lastPos = pos; _stepProcessing = true;
lmDebug('[LabMap] step (observer):', cx, cy);
waitForStep(cx, cy);
});
obs.observe(mapEl, {
subtree: true, attributes: true,
attributeFilter: ['class'], attributeOldValue: true, childList: true
});
})();
setInterval(function() {
if (_stepProcessing) return;
var d = mapData(), n = d && d.steps ? d.steps.length : 0;
var c = curPos(), pos = c.x + '_' + c.y;
if (n !== lastN || pos !== lastPos) {
lastN = n; lastPos = pos; _stepProcessing = true;
lmDebug('[LabMap] fallback detector:', pos);
waitForStep(c.x, c.y);
}
}, 5000);
function inferCurrentEventFromDom(fallbackEvent) {
var ev = fallbackEvent || 'unknown';
if (ev && ev !== 'unknown' && ev !== 'empty') return ev;
var text = '';
var selectors = [
'.labyrinth-last-event',
'.labyrinth-event',
'.labyrinth-info',
'.labyrinth-sidebar',
'#labyrinth',
'main'
];
for (var i = 0; i < selectors.length; i++) {
var el = document.querySelector(selectors[i]);
if (el && el.textContent) text += ' ' + el.textContent;
}
text = text.toLowerCase();
var rules = [
[/печать коллекционера|проверка одинаковых карт|коллекц/i, 'collection'],
[/чужой подарок|подарок/i, 'room_gift'],
[/замкненн(?:ый|ого) сундук|закрыт(?:ый|ого) сундук/i, 'locked_chest'],
[/мини.?босс/i, 'mini_boss'],
[/хард.?босс|сложн\w* босс/i, 'hard_boss'],
[/викторин/i, 'quiz'],
[/пазл|испытание дао/i, 'puzzle'],
[/откат|ловушка отката/i, 'trap_back'],
[/реликв/i, 'relic_room'],
[/эхо|відгомін/i, 'echo_room'],
[/пусто|обычная комната|центральная комната/i, 'empty']
];
for (var r = 0; r < rules.length; r++) {
if (rules[r][0].test(text)) return rules[r][1];
}
return ev || 'unknown';
}
var _syntheticStepNonce = 0;
var _lastSyntheticKey = '';
function sendSyntheticCurrentStep(cx, cy) {
var key = cx + '_' + cy;
if (_lastSyntheticKey === key) return;
_lastSyntheticKey = key;
setTimeout(function() {
var ev = inferCurrentEventFromDom('unknown');
var bankEl = document.getElementById('labyrinthBank');
var accAfter = bankEl
? parseInt(String(bankEl.textContent || '').replace(/\s+/g, ''), 10)
: null;
var synthetic = {
x: Number(cx),
y: Number(cy),
event: ev || 'unknown',
ev: ev || 'unknown',
acc_after: Number.isFinite(accAfter) ? accAfter : null,
session_id: PUSH_SESSION_ID,
step_index: 'synthetic_' + Date.now() + '_' + (++_syntheticStepNonce),
ts: new Date().toISOString(),
_lmSynthetic: true
};
var det = scrapeRoomDetails(synthetic.event);
var emDat = getEmissionStepDat();
synthetic._dat = det ? Object.assign({}, emDat, det) : (emDat || undefined);
schedulePush([synthetic], PUSH_SESSION_ID);
syncProfileToVPS(true);
}, 850);
}
function waitForStep(cx, cy) {
var targetKey = cx + '_' + cy;
var maxTries = 20, tryCount = 0;
function tryUpdate() {
tryCount++;
var curD = mapData();
var steps = curD && curD.steps ? curD.steps : [];
var lastStep = steps.length > 0 ? steps[steps.length - 1] : null;
var stepReady = lastStep && (lastStep.x + '_' + lastStep.y === targetKey);
var mapEl = document.getElementById('labyrinthMap');
var curCell = mapEl && mapEl.querySelector('.labyrinth-cell--current');
if (tryCount >= maxTries && !(stepReady && curCell)) {
_pendingStep = null;
_stepProcessing = false;
var current = curD && curD.current ? curD.current : null;
if (current &&
Number(current.x) === Number(cx) &&
Number(current.y) === Number(cy)) {
sendSyntheticCurrentStep(cx, cy);
}
return;
}
if (stepReady && curCell) {
lastN = steps.length; lastPos = targetKey; _pendingStep = null;
invalidateRoomsCache(); updateMapInfo(); patchSiteMap();
if (fmOpen) { drawFull(); centerOn(cx, cy); }
if (steps.length > 0) {
var lastS = steps[steps.length-1];
setTimeout(function() {
lastS.event = inferCurrentEventFromDom(lastS.event);
lastS.ev = lastS.event;
lastS.session_id = PUSH_SESSION_ID;
var det = scrapeRoomDetails(lastS.event);
var emDat = getEmissionStepDat();
lastS._dat = det ? Object.assign({}, emDat, det) : (emDat || undefined);
schedulePush(steps, PUSH_SESSION_ID);
}, 650);
}
if (window._lmMyPathData) {
window._lmMyPathData[cx + '_' + cy] = true;
}
_stepProcessing = false;
lmDebug('[Карта] Комната обработана:', cx, cy);
} else {
setTimeout(tryUpdate, 150);
}
}
setTimeout(tryUpdate, 100);
}
}
function wait() {
var MAX_TRIES=100, tries=0;
function check() {
tries++;
var d2=mapData();
if (d2&&d2.steps&&d2.steps.length>0) { lmDebug('[LabMap] Найдено! steps:',d2.steps.length); init(); }
else if (tries<MAX_TRIES) setTimeout(check,300);
else lmDebug('[LabMap] Data not found after',MAX_TRIES,'tries');
}
check();
}
if (IS_LABYRINTH) { wait(); }
(function mineAutoCollect() {
var LS_MINE_ENABLED = 'lm_mine_auto_enabled' + _lmUserSuffix;
var LS_MINE_INTERVAL = 'lm_mine_check_interval' + _lmUserSuffix;
var LS_MINE_LAST = 'lm_mine_last_check' + _lmUserSuffix;
var LS_MINE_THRESHOLD = 'lm_mine_threshold' + _lmUserSuffix;
var DEFAULT_INTERVAL = 5;
var DEFAULT_THRESHOLD = 100;
function isEnabled() { return window.lmLS.get(LS_MINE_ENABLED) !== 'false'; }
function getInterval() { return parseInt(window.lmLS.get(LS_MINE_INTERVAL) || DEFAULT_INTERVAL, 10); }
function getThreshold() { return parseInt(window.lmLS.get(LS_MINE_THRESHOLD) || DEFAULT_THRESHOLD, 10); }
function getUserHash() { return window.dle_login_hash || ''; }
function checkMine(callback) {
var hash = getUserHash();
if (!hash) { callback(null); return; }
if (IS_LABYRINTH && window.labyrinthData && window.labyrinthData.personalMine) {
callback(window.labyrinthData.personalMine); return;
}
fetch('/labyrinth/?_=' + Date.now(), { credentials: 'same-origin', cache: 'no-store' })
.then(function(r) { if (!r.ok) throw new Error('HTTP ' + r.status); return r.text(); })
.then(function(html) {
var marker = 'personalMine:';
var idx = html.indexOf(marker);
if (idx === -1) { callback(null); return; }
var afterColon = html.substring(idx + marker.length).replace(/^\s+/, '');
if (afterColon.indexOf('null') === 0) { callback(null); return; }
if (afterColon[0] !== '{') { callback(null); return; }
var depth = 0, pos = 0, len = afterColon.length;
while (pos < len) {
var ch = afterColon[pos];
if (ch === '{') { depth++; }
else if (ch === '}') { depth--; if (depth === 0) { pos++; break; } }
else if (ch === '"') {
pos++;
while (pos < len) {
if (afterColon[pos] === '\\' && pos + 1 < len) { pos += 2; continue; }
if (afterColon[pos] === '"') break;
pos++;
}
}
pos++;
}
try {
var mine = JSON.parse(afterColon.substring(0, pos));
if (mine && mine.has_mine) callback(mine); else callback(null);
} catch(e) { callback(null); }
})
.catch(function() { callback(null); });
}
function collectMine(onDone) {
var hash = getUserHash();
if (!hash) { onDone(false, 'No hash'); return; }
fetch('/index.php?controller=ajax&mod=animesss_game', {
method: 'POST', credentials: 'same-origin',
headers: { 'Content-Type': 'application/x-www-form-urlencoded; charset=UTF-8', 'X-Requested-With': 'XMLHttpRequest' },
body: 'action=collect_personal_mine&user_hash=' + encodeURIComponent(hash)
})
.then(function(r) { if (!r.ok) throw new Error('HTTP ' + r.status); return r.json(); })
.then(function(d) {
if (d.success) {
onDone(true, d);
try {
if (window.DLEPush && DLEPush.success) {
DLEPush.success('⛏ Шахта: собрано ' + (d.acc || 0) + ' АСС' + (d.cards_awarded && d.cards_awarded.length ? ' + ' + d.cards_awarded.length + ' карт' : ''));
}
} catch(e) {}
} else { onDone(false, d); }
})
.catch(function(e) { onDone(false, e.message); });
}
function runCheck() {
if (!isEnabled() || !getUserHash()) return;
window.lmLS.set(LS_MINE_LAST, String(Date.now()));
checkMine(function(mine) {
if (!mine) return;
var threshold = getThreshold();
if (mine.can_collect && (mine.storage_progress >= threshold || mine.is_storage_full)) {
collectMine(function(ok, data) {
if (ok) lmDebug('[LabMap Mine] собрано:', data.acc, 'АСС');
else lmDebug('[LabMap Mine] ошибка:', data);
});
}
});
}
var _mineTimer = null;
function getSmartDelay() {
var pm = window.labyrinthData && window.labyrinthData.personalMine;
if (pm && pm.has_mine && pm.max_storage_seconds > 0) {
var threshold = getThreshold();
var pct = Number(pm.storage_progress || 0);
if (pct >= threshold) return 0;
var secsLeft = Number(pm.storage_seconds_left || 0);
var pctMissing = threshold - pct;
var totalPct = 100;
var secsToThreshold = secsLeft * (pctMissing / Math.max(1, totalPct - pct));
return Math.max(15000, secsToThreshold * 1000 + 30000);
}
var last = parseInt(window.lmLS.get(LS_MINE_LAST) || '0', 10);
var intervalMs = getInterval() * 60 * 1000;
return last === 0 ? 0 : Math.max(0, (last + intervalMs) - Date.now());
}
function scheduleNext() {
if (_mineTimer) clearTimeout(_mineTimer);
if (!isEnabled()) return;
var delay = getSmartDelay();
_mineTimer = setTimeout(function() {
_mineTimer = null;
if (!isEnabled()) { scheduleNext(); return; }
runCheck(); scheduleNext();
}, delay);
}
function injectMineSettingsBtn() {
function tryInject() {
var mineEl = document.getElementById('labyrinthPersonalMine');
if (!mineEl || mineEl.style.display === 'none' || window.getComputedStyle(mineEl).display === 'none') return;
if (mineEl.querySelector('#lm-mine-settings-btn')) return;
var actionsEl = mineEl.querySelector('.labyrinth__personal-mine-actions');
if (!actionsEl) return;
var btn = document.createElement('button');
btn.id = 'lm-mine-settings-btn'; btn.type = 'button'; btn.className = 'button';
btn.innerHTML = '⚙ Авто-сбор';
btn.style.cssText = 'width:100%;margin-top:8px;background:linear-gradient(135deg,#4a5ac7,#3a4ab0);border:none;color:#fff;font-weight:700;';
btn.addEventListener('click', function(e) { e.stopImmediatePropagation(); e.preventDefault(); showMineSettings(); });
actionsEl.appendChild(btn);
}
tryInject();
[200, 500, 1000, 2000, 3000].forEach(function(ms) { setTimeout(tryInject, ms); });
var _injectScheduled = false;
var obs = new MutationObserver(function(mutations) {
if (_injectScheduled) return;
for (var i = 0; i < mutations.length; i++) {
var t = mutations[i].target;
if (t.id === 'labyrinthPersonalMine' || (t.classList && t.classList.contains('labyrinth__personal-mine-actions'))) {
_injectScheduled = true;
setTimeout(function() { _injectScheduled = false; tryInject(); }, 100);
return;
}
}
});
var mineEl = document.getElementById('labyrinthPersonalMine');
if (mineEl) obs.observe(mineEl, { attributes: true, attributeFilter: ['style'], childList: true, subtree: true });
var rightEl = document.querySelector('.labyrinth__right');
if (rightEl) obs.observe(rightEl, { childList: true, subtree: false, attributes: true, attributeFilter: ['style'] });
}
function showMineSettings() {
var existing = document.getElementById('lm-mine-modal');
if (existing) { existing.classList.add('on'); return; }
var modal = document.createElement('div'); modal.id = 'lm-mine-modal';
var enabled = isEnabled();
var interval = getInterval();
var threshold = getThreshold();
var last = parseInt(window.lmLS.get(LS_MINE_LAST) || '0', 10);
var lastStr = last ? getMoscowTimeStr(last) : 'ещё не проверялась';
modal.innerHTML =
'<div id="lm-mine-overlay"></div>' +
'<div id="lm-mine-box">' +
'<div id="lm-mine-head"><div id="lm-mine-title">⛏ Авто-збір шахти</div><button id="lm-mine-close">×</button></div>' +
'<div id="lm-mine-body">' +
'<div class="lm-mine-row">' +
'<div class="lm-mine-label">Авто-збір включено</div>' +
'<label class="lm-mine-toggle"><input type="checkbox" id="lm-mine-enabled" ' + (enabled ? 'checked' : '') + '><span class="lm-mine-slider"></span></label>' +
'</div>' +
'<div class="lm-mine-row">' +
'<div class="lm-mine-label">Збирати при заповненості</div>' +
'<div class="lm-mine-input-wrap"><input type="number" id="lm-mine-threshold" value="' + threshold + '" min="1" max="100" step="1"><span class="lm-mine-pct-label">%</span></div>' +
'</div>' +
'<div class="lm-mine-row">' +
'<div class="lm-mine-label">Резервний інтервал (хв)</div>' +
'<input type="number" id="lm-mine-interval" value="' + interval + '" min="1" max="1440" step="1">' +
'</div>' +
'<div class="lm-mine-info">Остання перевірка: <b>' + lastStr + '</b></div>' +
'<div id="lm-mine-actions">' +
'<button id="lm-mine-check-now" class="button button--primary">🔍 Перевірити зараз</button>' +
'<button id="lm-mine-save" class="button">💾 Зберегти</button>' +
'</div>' +
'<div id="lm-mine-status"></div>' +
'</div>' +
'</div>';
var style = document.createElement('style');
style.textContent = '#lm-mine-modal{display:none;position:fixed;inset:0;z-index:9999999;align-items:center;justify-content:center;}#lm-mine-modal.on{display:flex;}#lm-mine-overlay{position:absolute;inset:0;background:rgba(0,0,0,.7);}#lm-mine-box{position:relative;z-index:2;background:#1a1d2a;border:1px solid rgba(255,255,255,.12);border-radius:20px;padding:0;width:min(380px,calc(100vw - 24px));color:#fff;overflow:hidden;}#lm-mine-head{display:flex;align-items:center;justify-content:space-between;padding:16px 20px;border-bottom:1px solid rgba(255,255,255,.08);}#lm-mine-title{font-size:16px;font-weight:800;}#lm-mine-close{width:32px;height:32px;border:none;border-radius:8px;background:#c0392b;color:#fff;font-size:18px;cursor:pointer;}#lm-mine-body{padding:18px 20px;display:flex;flex-direction:column;gap:14px;}.lm-mine-row{display:flex;align-items:center;justify-content:space-between;gap:10px;}.lm-mine-label{font-size:14px;color:rgba(255,255,255,.8);flex:1;}.lm-mine-toggle{position:relative;display:inline-block;width:44px;height:24px;}.lm-mine-toggle input{opacity:0;width:0;height:0;}.lm-mine-slider{position:absolute;inset:0;background:rgba(255,255,255,.15);border-radius:24px;cursor:pointer;transition:.2s;}.lm-mine-slider:before{content:"";position:absolute;height:18px;width:18px;left:3px;bottom:3px;background:#fff;border-radius:50%;transition:.2s;}.lm-mine-toggle input:checked + .lm-mine-slider{background:#4a5ac7;}.lm-mine-toggle input:checked + .lm-mine-slider:before{transform:translateX(20px);}.lm-mine-input-wrap{display:flex;align-items:stretch;flex-shrink:0;}#lm-mine-threshold{width:52px;padding:6px 8px;border:1px solid rgba(255,255,255,.15);border-radius:10px 0 0 10px;border-right:none;background:rgba(255,255,255,.06);color:#fff;font-size:14px;text-align:center;}.lm-mine-pct-label{font-size:13px;color:rgba(255,255,255,.65);padding:0 10px;border:1px solid rgba(255,255,255,.15);border-radius:0 10px 10px 0;background:rgba(255,255,255,.04);display:flex;align-items:center;}#lm-mine-interval{width:70px;padding:6px 10px;border:1px solid rgba(255,255,255,.15);border-radius:10px;background:rgba(255,255,255,.06);color:#fff;font-size:14px;text-align:center;}.lm-mine-info{font-size:12px;color:rgba(255,255,255,.45);padding:8px 12px;border-radius:10px;background:rgba(255,255,255,.04);}.lm-mine-info b{color:rgba(255,255,255,.75);}#lm-mine-actions{display:flex;flex-direction:column;gap:8px;}#lm-mine-actions .button{width:100%;justify-content:center;}#lm-mine-status{font-size:13px;font-weight:700;text-align:center;min-height:20px;color:#6ee786;}';
document.head.appendChild(style);
document.body.appendChild(modal);
modal.classList.add('on');
document.getElementById('lm-mine-close').addEventListener('click', function() { modal.classList.remove('on'); });
document.getElementById('lm-mine-overlay').addEventListener('click', function() { modal.classList.remove('on'); });
document.getElementById('lm-mine-save').addEventListener('click', function() {
var en = document.getElementById('lm-mine-enabled').checked;
var inv = parseInt(document.getElementById('lm-mine-interval').value, 10) || DEFAULT_INTERVAL;
var thr = parseInt(document.getElementById('lm-mine-threshold').value, 10) || DEFAULT_THRESHOLD;
inv = Math.max(1, Math.min(1440, inv)); thr = Math.max(1, Math.min(100, thr));
window.lmLS.set(LS_MINE_ENABLED, String(en));
window.lmLS.set(LS_MINE_INTERVAL, String(inv));
window.lmLS.set(LS_MINE_THRESHOLD, String(thr));
var st = document.getElementById('lm-mine-status');
st.textContent = 'Сохранено!'; st.style.color = '#6ee786';
scheduleNext();
if (en) setTimeout(function() { runCheck(); }, 500);
setTimeout(function() { st.textContent = ''; modal.classList.remove('on'); }, 1200);
});
document.getElementById('lm-mine-check-now').addEventListener('click', function() {
var st = document.getElementById('lm-mine-status');
st.style.color = 'rgba(255,255,255,.6)'; st.textContent = 'Проверяем...';
checkMine(function(mine) {
if (!mine) { st.style.color = '#ff8b8b'; st.textContent = 'Не удалось получить данные'; return; }
var thr2 = parseInt((document.getElementById('lm-mine-threshold')||{}).value || getThreshold(), 10) || getThreshold();
if (mine.storage_progress >= thr2 || mine.is_storage_full) {
st.style.color = '#ffd66b'; st.textContent = mine.storage_progress + '%! Собираем...';
collectMine(function(ok, data) {
if (ok) { st.style.color = '#6ee786'; st.textContent = 'Собрано ' + (data.acc || 0) + ' АСС'; }
else { st.style.color = '#ff8b8b'; st.textContent = 'Ошибка сбора'; }
});
} else {
st.style.color = 'rgba(255,255,255,.6)'; st.textContent = 'Сейчас ' + mine.storage_progress + '% — порог ' + thr2 + '%';
}
});
});
}
scheduleNext();
if (IS_LABYRINTH) {
if (document.readyState === 'loading') { document.addEventListener('DOMContentLoaded', injectMineSettingsBtn); }
else { injectMineSettingsBtn(); }
}
lmDebug('[LabMap Mine] авто-сбор инициализирован, интервал:', getInterval(), 'min');
})();
function collectRoomExtra() {
var result = {};
var ld = window.labyrinthData || {};
var ro = ld.roomOpener;
if (ro && ro.name) {
result.first_opener = String(ro.name).trim();
if (ro.date) {
var dm = String(ro.date).match(/(\d{2})\.(\d{2})\.(\d{4})[^\d]*(\d{2}:\d{2})/);
if (dm) result.first_opened_at = dm[3]+'-'+dm[2]+'-'+dm[1]+'T'+dm[4]+':00';
}
}
if (!result.first_opener) {
var openerEl = document.querySelector('.labyrinth__opener-name, .room-opener .username, [class*="opener"] .name');
if (openerEl) {
result.first_opener = openerEl.textContent.trim();
var openerBlock = openerEl.closest('[class*="opener"], .labyrinth__opener');
if (openerBlock) {
var dateMatch = openerBlock.textContent.match(/(\d{2})\.(\d{2})\.(\d{4})[^\d]*(\d{2}:\d{2})/);
if (dateMatch) result.first_opened_at = dateMatch[3]+'-'+dateMatch[2]+'-'+dateMatch[1]+'T'+dateMatch[4]+':00';
}
}
}
var pm = ld.personalMine;
if (pm && pm.has_mine) {
result.mine = {
is_owner: true,
level: Number(pm.level || 0),
storage_pct: Number(pm.storage_progress || 0),
acc_inside: Number(pm.pending_acc || 0),
cards_current: Number(pm.pending_cards || 0),
cards_max: Number(pm.max_cards || 0),
acc_per_hour: Number(pm.acc_per_hour || 0),
storage_seconds_left: Number(pm.storage_seconds_left || 0),
is_storage_full: !!pm.is_storage_full
};
} else if (ld.foreignMine) {
var fm = ld.foreignMine;
result.mine = {
is_owner: false,
owner: fm.owner || fm.username || fm.user || null,
level: Number(fm.level || 0),
storage_pct: Number(fm.storage_progress || 0),
acc_inside: Number(fm.pending_acc || fm.acc || 0),
cards_current: Number(fm.pending_cards || fm.cards || 0),
cards_max: Number(fm.max_cards || 0)
};
}
if (ld.echoRoom) result.echo = ld.echoRoom;
if (ld.fateRoom) result.fate = ld.fateRoom;
if (ld.clubWarRoom) result.club_war = ld.clubWarRoom;
if (typeof ld.canPlaceObject !== 'undefined') result.can_place_object = !!ld.canPlaceObject;
if (ld.emission && ld.emission.last_start_at) {
result.emission = {
active: !!ld.emission.active,
last_start_at: ld.emission.last_start_at,
last_end_at: ld.emission.last_end_at || 0,
cooldown_left: ld.emission.cooldown_left || 0
};
if (ld.emission.active) result.during_emission = true;
}
return result;
}
function sendRollbackToVPS(count, place) {
lmDebug('[Синхронизация] sendRollbackToVPS:', count, place);
var VPS_API = LM_API_BASE;
var auth = getAuth();
if (!auth || count == null) return;
var username = auth.username;
window.lmFetch(VPS_API + '/sync/rollbacks', {
method: 'POST',
headers: vpsAuthHeaders(true),
body: JSON.stringify({ trap_backs: count, rollback_place: place }),
})
.then(function(r){ if (!r.ok) throw new Error('HTTP ' + r.status); return r.json(); })
.then(function(res){
lmDebug('[Синхронизация] Откаты отправлены на VPS:', count, '| Топ:', place);
})
.catch(function() {});
}
function sendAccHistoryToVPS() {
lmDebug('[Синхронизация] sendAccHistoryToVPS викликано, histCache pages:', Object.keys(histCache).length);
var VPS_API = LM_API_BASE;
var auth = getAuth();
if (!auth) { lmDebug('[Синхронизация] Нет имени пользователя или токена'); return; }
var username = auth.username;
function doSend() {
if (!histCache || !Object.keys(histCache).length) return;
var rows = [];
lmDebug('[Синхронизация] doSend histCache keys:', Object.keys(histCache));
Object.keys(histCache).forEach(function(page) {
var pageData = histCache[page];
if (!pageData || !pageData.rows) { lmDebug('[Синхронизация] page',page,'нет строк'); return; }
lmDebug('[Синхронизация] page',page,'rows count:', pageData.rows.length);
pageData.rows.forEach(function(row) {
var desc = row.desc || row.description || '';
var date = row.date || row.ts || null;
var amount = row.amount !== undefined ? row.amount : row.acc_delta;
var balance = row.balance !== undefined ? row.balance : row.acc_after;
if (!desc) return;
var isoDate = null;
if (date) {
var dm = String(date).match(/(\d{2})\.(\d{2})\.(\d{4})\s+(\d{2}:\d{2}:\d{2})/);
if (dm) isoDate = dm[3]+'-'+dm[2]+'-'+dm[1]+'T'+dm[4];
else isoDate = date;
}
var amtStr = String(amount||'0');
var isNeg = amtStr.indexOf('-') >= 0;
var delta = parseInt(amtStr.replace(/[^0-9]/g,'')) * (isNeg ? -1 : 1);
var bal = balance !== null && balance !== undefined
? parseInt(String(balance).replace(/[^0-9]/g,'')) : null;
rows.push({
ts: isoDate,
description: desc,
acc_delta: isNaN(delta) ? null : delta,
acc_after: bal,
});
});
});
lmDebug('[Синхронизация] Строк для отправки собрано:', rows.length, '| перший:', rows[0] ? JSON.stringify(rows[0]) : 'нет');
if (!rows.length) return;
window.lmFetch(VPS_API + '/sync/acc-history', {
method: 'POST',
headers: vpsAuthHeaders(true),
body: JSON.stringify({ rows: rows }),
})
.then(function(r){ if (!r.ok) throw new Error('HTTP ' + r.status); return r.json(); })
.then(function(res){
lmDebug('[Синхронизация] ACC-история отправлена:', res.saved, 'строк');
})
.catch(function() {});
}
if (!histCache || !Object.keys(histCache).length) {
loadHistPage(1, function(result) {
var total = result.totalPages || 1;
var loaded = 1;
function loadNext(page) {
if (page > total || page > 13) { doSend(); return; }
loadHistPage(page, function() { loadNext(page + 1); });
}
loadNext(2);
});
return;
}
doSend();
}
function parseRoomDetails(ev, accDelta) {
var labData = window.labyrinthData;
var evData = labData && labData.lastEventData;
var details = {};
switch(ev) {
case 'quiz': case 'quiz_result':
var qEl = document.querySelector('#labyrinthQuizQuestion, .labyrinth-quiz__question, .labyrinth__quiz-question, .quiz-question');
var aEl = document.querySelector('#labyrinthQuiz .button.is-active, .quiz-answer.selected, .labyrinth-quiz__answer.active, .labyrinth__quiz-answer.active, .answer-btn.active');
if (qEl) details.question = qEl.textContent.trim().slice(0,300);
if (aEl) details.answer_given = aEl.textContent.trim().slice(0,100);
details.is_correct = accDelta > 0;
break;
case 'personal_mine':
case 'personal_mine_created':
case 'personal_mine_collect':
var mEl = document.querySelector('#labyrinthPersonalMine, .labyrinth__personal-mine, .mine-info, #labyrinthMineInfo, .labyrinth-mine');
if (mEl) {
var t = mEl.textContent;
var lv = t.match(/уровень[^\d]*(\d+)/i)||t.match(/level[^\d]*(\d+)/i);
var fl = t.match(/(\d+)\s*%/);
var ac = t.match(/(\d+)\s*ACC/i);
if (lv) details.level = parseInt(lv[1]);
if (fl) details.fill_pct = parseInt(fl[1]);
if (ac) details.acc_inside= parseInt(ac[1]);
}
if (evData) {
details.level = details.level ||evData.level ||null;
details.fill_pct = details.fill_pct ||evData.fillPct ||null;
details.acc_inside= details.acc_inside||evData.accInside||null;
}
details.action = accDelta > 0 ? 'collected' : 'viewed';
break;
case 'foreign_mine':
var fmEl = document.querySelector('#labyrinthForeignMine .card-inline__name, #labyrinthForeignMine, .labyrinth__foreign-mine, .mine-owner, .foreign-mine .owner, .card-inline__name');
if (fmEl) details.owner = fmEl.textContent.trim();
else if (evData&&evData.owner) details.owner = evData.owner;
details.action = accDelta !== 0 ? 'collected_tribute' : 'viewed';
break;
case 'card_trader': case 'card_trader_result':
var cEl = document.querySelector('.labyrinth__trader-name, .trader-card .card-inline__name, .trade-card-name');
var pEl = document.querySelector('.labyrinth__trader-price, .trader-price, .trade-cost');
if (cEl) details.card_name = cEl.textContent.trim();
if (pEl) details.price_acc = parseInt(pEl.textContent)||null;
details.action = accDelta < 0 ? 'bought' : 'declined';
break;
case 'spiritual_teleport':
if (evData) {
details.teleport_to_x = evData.toX ||null;
details.teleport_to_y = evData.toY ||null;
details.guardian_owner = evData.owner ||null;
}
details.action = accDelta < 0 ? 'paid' : (accDelta > 0 ? 'won' : 'viewed');
break;
case 'mimic_chest': case 'mimic_chest_hit': case 'mimic_chest_killed':
case 'mimic_chest_escape': case 'mimic_chest_reward': case 'mimic_chest_back':
details.result = accDelta > 0 ? 'real_chest' : accDelta < 0 ? 'mimic_trap' : 'alive_chest';
var mhpEl = document.querySelector('#labyrinthMimicHpText, .labyrinth__mimic-hp-text');
if (mhpEl) details.hp = mhpEl.textContent.trim().slice(0,40);
break;
case 'room_gift':
details.action = 'triggered';
if (accDelta > 0) details.acc_inside = accDelta;
break;
case 'room_trap':
details.action = 'triggered';
if (accDelta < 0) details.acc_penalty = accDelta;
break;
case 'club_war_room':
var cwEl = document.querySelector('#labyrinthClubWarClubs .is-active, .labyrinth__club-war-club.is-active, .club-war-target, .attack-club');
if (cwEl) details.attacked_club = cwEl.textContent.trim();
details.won = accDelta >= 0;
details._admin_only = true;
break;
case 'dao_test': case 'dao_result':
details.result = accDelta > 0 ? 'acc' : accDelta < 0 ? 'trap' : 'nothing';
if (accDelta !== 0) details.acc_delta = accDelta;
break;
case 'relic_room':
details.piece_or_full = accDelta >= 60 ? 'full' : 'piece';
details.acc_value = accDelta;
break;
case 'fate_room': case 'fate_room_result':
var frd = window.labyrinthData && window.labyrinthData.fateRoom;
if (frd && frd.options) {
details.has_options = true;
details.option_count = Object.keys(frd.options).length;
}
details.choice_made = accDelta !== 0;
break;
}
return Object.keys(details).length > 0 ? details : null;
}
function sendFirstOpener(x, y, opener, openedAt) {
var VPS_API = LM_API_BASE;
if (!getAuth() || !opener) return;
window.lmFetch(VPS_API + '/room/first-opener', {
method: 'POST',
headers: vpsAuthHeaders(true),
body: JSON.stringify({ x: x, y: y, first_user: opener, first_at: openedAt }),
}).catch(function(){});
}
function sendMineData(x, y, mine) {
var VPS_API = LM_API_BASE;
if (!getAuth() || !mine) return;
window.lmFetch(VPS_API + '/room/mine', {
method: 'POST',
headers: vpsAuthHeaders(true),
body: JSON.stringify({ x: x, y: y, ...mine }),
}).catch(function(){});
}
(function() {
'use strict';
const VPS_WS = LM_WS_URL;
const VPS_API = LM_API_BASE;
const USERNAME = window.visitor_name || '';
const TOKEN = window.dle_login_hash || '';
if (!USERNAME || !TOKEN) return;
if (!window.location.pathname.startsWith('/labyrinth')) return;
const SS_KEY = 'lm_step_' + USERNAME;
const SS_BAL = 'lm_bal_' + USERNAME;
let ws = null;
let authed = false;
let queue = [];
let pingTimer = null;
let reconnectTimer = null;
let reconnectDelay = 2000;
let connecting = false;
let registrationPromise = null;
function ensureRegistered() {
if (registrationPromise) return registrationPromise;
registrationPromise = window.lmFetch(VPS_API + '/register', {
method: 'POST',
headers: { 'Content-Type': 'application/json', 'X-Script-Version': LM_SCRIPT_VERSION },
body: JSON.stringify({ username: USERNAME, token: TOKEN, script_version: LM_SCRIPT_VERSION })
}).then(function(response) {
if (response.ok) return true;
if (response.status === 409) { registrationPromise = null; return false; }
if (response.status === 429) throw new Error('register_rate_limited');
throw new Error('register_http_' + response.status);
}).catch(function(error) {
registrationPromise = null;
lmDebug('[Карта] Регистрация на VPS:', error && error.message);
return false;
});
return registrationPromise;
}
function startPing() {
if (pingTimer) return;
pingTimer = setInterval(function() {
if (ws && ws.readyState === WebSocket.OPEN && authed) {
try { ws.send(JSON.stringify({ type: 'ping' })); } catch(e) {}
}
}, 25000);
}
function scheduleReconnect() {
if (reconnectTimer) return;
reconnectTimer = setTimeout(function() {
reconnectTimer = null;
connect();
}, reconnectDelay);
reconnectDelay = Math.min(reconnectDelay * 2, 30000);
}
function connect() {
if (connecting) return;
if (ws && (ws.readyState === WebSocket.OPEN || ws.readyState === WebSocket.CONNECTING)) return;
connecting = true;
ensureRegistered().then(function(registered) {
if (!registered) {
connecting = false;
scheduleReconnect();
return;
}
try {
ws = new WebSocket(VPS_WS);
} catch(e) {
connecting = false;
scheduleReconnect();
return;
}
ws.onopen = function() {
connecting = false;
reconnectDelay = 2000;
startPing();
ws.send(JSON.stringify({ type: 'auth', user: USERNAME, token: TOKEN, script_version: LM_SCRIPT_VERSION }));
};
ws.onmessage = function(e) {
try {
var msg = JSON.parse(e.data);
if (msg.type === 'auth_ok') {
authed = true;
lmDebug('[Карта] Синхронизация включена');
var statusEl = document.getElementById('lm-status');
if (statusEl) statusEl.textContent = '🟢 Карта подключена';
var pending = queue.slice();
queue = [];
pending.forEach(function(ev){ sendEv(ev); });
}
} catch(err) {}
};
ws.onclose = function(event) {
connecting = false;
authed = false;
var statusEl = document.getElementById('lm-status');
if (event && event.code === 4001) {
if (statusEl) statusEl.textContent = '🟡 Активна інша вкладка';
reconnectDelay = 30000;
} else {
if (statusEl) statusEl.textContent = '🔴 Нет соединения';
}
scheduleReconnect();
};
ws.onerror = function() {
try { ws.close(); } catch(e) {}
};
});
}
function sendEv(ev) {
if (!ws || ws.readyState !== WebSocket.OPEN || !authed) {
if (queue.length < 1000) queue.push(ev);
return;
}
try { ws.send(JSON.stringify(Object.assign({ type: 'event' }, ev))); } catch(e) {}
}
function getBalance() {
var el = document.getElementById('labyrinthBank');
if (!el) return null;
return parseInt(el.textContent.replace(/[^0-9]/g, '')) || 0;
}
window._lmGetBalance = getBalance;
window._lmSendEv = sendEv;
function startPolling() {
var sessionId = PUSH_SESSION_ID;
var savedState = null;
try {
var raw = window.lmSS.get(SS_KEY);
if (raw) savedState = JSON.parse(raw);
} catch(e) {}
var lastStepCount = savedState ? savedState.count : -1;
var sentStepKeys = savedState ? new Set(savedState.keys || []) : new Set();
var prevBalance = null;
try {
var savedBal = window.lmSS.get(SS_BAL);
if (savedBal) prevBalance = parseInt(savedBal);
} catch(e) {}
function saveState() {
try {
var keysArr = Array.from(sentStepKeys).slice(-200);
window.lmSS.set(SS_KEY, JSON.stringify({
count: lastStepCount,
keys: keysArr
}));
} catch(e) {}
}
var initDone = false;
var initTimer = setInterval(function() {
var labData = window.labyrinthData;
if (!labData) return;
var steps = labData.mapData && labData.mapData.steps;
if (!steps) return;
var today = getMoscowDate();
var savedDay = null;
try { savedDay = window.lmSS.get('lm_day_' + USERNAME); } catch(e) {}
if (savedDay !== today) {
sentStepKeys.clear();
lastStepCount = -1;
try { window.lmSS.set('lm_day_' + USERNAME, today); } catch(e) {}
}
if (lastStepCount === -1) {
lastStepCount = steps.length;
steps.forEach(function(s, i) {
sentStepKeys.add(i + '_' + (s.event||'') + '_' + (s.x||0) + '_' + (s.y||0));
});
saveState();
}
prevBalance = getBalance();
try { window.lmSS.set(SS_BAL, String(prevBalance)); } catch(e) {}
clearInterval(initTimer);
initDone = true;
}, 500);
setInterval(function() {
if (!initDone) return;
var labData = window.labyrinthData;
if (!labData) return;
var cur = labData.mapData && labData.mapData.current;
var steps = labData.mapData && labData.mapData.steps;
var ev = labData.lastEvent;
if (!cur || !steps) return;
var stepCount = steps.length;
if (stepCount < lastStepCount && lastStepCount > 0) {
lastStepCount = stepCount;
saveState();
}
for (var i = Math.max(0, lastStepCount); i < stepCount; i++) {
var step = steps[i];
if (!step) continue;
var stepKey = i + '_' + (step.event||ev||'') + '_' + (step.x||cur.x||0) + '_' + (step.y||cur.y||0);
if (sentStepKeys.has(stepKey)) continue;
sentStepKeys.add(stepKey);
var stableIdx;
if (Number.isInteger(Number(step._lmServerStepIndex))) {
stableIdx = Number(step._lmServerStepIndex);
} else {
var seq = parseInt(window.lmLS.get(LS_SERVER_SEQ) || '0', 10);
if (!Number.isFinite(seq) || seq < 0) seq = 0;
step._lmServerStepIndex = seq;
window.lmLS.set(LS_SERVER_SEQ, String(seq + 1));
stableIdx = seq;
}
(function(stepIdx, stepData, isLast) {
var delay = isLast ? 800 : 0;
setTimeout(function() {
var balAfter = isLast ? getBalance() : null;
if (balAfter !== null) {
try { window.lmSS.set(SS_BAL, String(balAfter)); } catch(e) {}
}
var delta = (prevBalance !== null && balAfter !== null)
? balAfter - prevBalance : 0;
var sx = stepData.x != null ? stepData.x : cur.x;
var sy = stepData.y != null ? stepData.y : cur.y;
var sev = stepData.event || ev || 'empty';
var isCurrentRoom = isLast && sx === cur.x && sy === cur.y;
var _extra = isCurrentRoom && window._lmCollectExtra
? window._lmCollectExtra()
: {};
var details = parseRoomDetails(sev, delta) || {};
if (_extra.echo) details.echo = _extra.echo;
if (_extra.fate) details.fate = _extra.fate;
if (_extra.club_war) details.club_war = _extra.club_war;
sendEv({
x: sx,
y: sy,
ev: sev,
acc_before: prevBalance,
acc_after: balAfter != null ? balAfter : null,
acc_delta: delta,
dat: Object.keys(details).length ? details : null,
session_id: sessionId,
step_index: stepIdx
});
if (isCurrentRoom && _extra.first_opener && window._lmSendOpener) {
window._lmSendOpener(sx, sy, _extra.first_opener, _extra.first_opened_at || null);
}
if (isCurrentRoom && _extra.mine && window._lmSendMine) {
window._lmSendMine(sx, sy, _extra.mine);
}
if (isLast && balAfter !== null) prevBalance = balAfter;
}, delay);
})(stableIdx, step, i === stepCount - 1);
}
if (stepCount > lastStepCount) {
lastStepCount = stepCount;
saveState();
}
}, 1000);
}
connect();
if (document.readyState === 'complete') {
startPolling();
} else {
window.addEventListener('load', startPolling);
}
})();
window._lmCollectExtra = collectRoomExtra;
window._lmSendOpener = sendFirstOpener;
window._lmSendMine = sendMineData;
var _lmHistSteps = null;
var _lmEmissionCells = null;
function lmApplyEmissionCells() {
if (!_lmEmissionCells || !_lmEmissionCells.size) return;
var mapEl = document.getElementById('labyrinthMap');
if (!mapEl) return;
var labData = window.labyrinthData;
var cur = labData && labData.mapData && labData.mapData.current;
if (!cur) return;
var curX = parseInt(cur.x, 10) || 0;
var curY = parseInt(cur.y, 10) || 0;
var C = 12;
_lmEmissionCells.forEach(function(key) {
var parts = key.split('_');
var sx = parseInt(parts[0], 10);
var sy = parseInt(parts[1], 10);
var gx = C + (sx - curX);
var gy = C + (sy - curY);
if (gx < 0 || gx >= 25 || gy < 0 || gy >= 25) return;
var cell = mapEl.querySelector('[data-x="' + gx + '"][data-y="' + gy + '"]');
if (!cell) return;
cell.classList.add('lm-emission-cell');
if (!cell.querySelector('.lm-emission-ico')) {
var ico = document.createElement('span');
ico.className = 'lm-emission-ico';
ico.textContent = '⚡';
ico.title = 'Посещено во время выброса';
cell.appendChild(ico);
}
});
}
window._lmApplyEmissionCells = lmApplyEmissionCells;
function lmApplyHistoricalPath() {
if (!_lmHistSteps || !_lmHistSteps.length) return;
var mapEl = document.getElementById('labyrinthMap');
if (!mapEl) return;
var labData = window.labyrinthData;
var cur = labData && labData.mapData && labData.mapData.current;
if (!cur) return;
var curX = parseInt(cur.x, 10) || 0;
var curY = parseInt(cur.y, 10) || 0;
var C = 12; // center of 25x25 grid
_lmHistSteps.forEach(function(s) {
var sx = parseInt(s.x, 10);
var sy = parseInt(s.y, 10);
var ev = s.event || '';
var gx = C + (sx - curX);
var gy = C + (sy - curY);
if (gx < 0 || gx >= 25 || gy < 0 || gy >= 25) return;
var cell = mapEl.querySelector('[data-x="' + gx + '"][data-y="' + gy + '"]');
if (!cell || !cell.classList.contains('labyrinth-cell--unknown')) return;
cell.classList.remove('labyrinth-cell--unknown');
cell.classList.add('labyrinth-cell--visited', 'lm-hist');
if (ev) {
cell.classList.add('labyrinth-cell--event-' + ev);
cell.setAttribute('data-event', ev);
}
});
}
window._lmApplyHistoricalPath = lmApplyHistoricalPath;
function lmFetchHistoricalPath() {
if (!IS_LABYRINTH) return;
var u = window.visitor_name || '';
if (!u) return;
var LS_HIST = 'lm_hist_' + u;
var LS_HIST_TS = 'lm_hist_ts_' + u;
var LS_EM = 'lm_em_cells_' + u;
try {
var ts = parseInt(window.lmLS.get(LS_HIST_TS) || '0', 10);
if (Date.now() - ts < 5 * 60 * 1000) {
var cached = JSON.parse(window.lmLS.get(LS_HIST) || 'null');
var cachedEm = JSON.parse(window.lmLS.get(LS_EM) || 'null');
if (cached && cached.length && cachedEm !== null) {
_lmHistSteps = cached;
// Кеш-гілка теж має наповнити _lmMyPathData (канвас "Мой путь"),
// інакше шлях на повній карті схлопується в поточну сесію (тонка лінія)
if (!window._lmMyPathData) window._lmMyPathData = {};
cached.forEach(function(s){ window._lmMyPathData[s.x+'_'+s.y]=true; });
if (cachedEm && cachedEm.length) {
_lmEmissionCells = new Set(cachedEm);
}
setTimeout(function(){ lmApplyHistoricalPath(); lmApplyEmissionCells(); }, 100);
return;
}
}
} catch(e) {}
window.lmFetch(VPS_URL + '/my/path', { headers: vpsAuthHeaders(false) })
.then(function(r) { return r.ok ? r.json() : null; })
.then(function(data) {
if (!data || !Array.isArray(data.steps)) return;
_lmHistSteps = data.steps;
if (!window._lmMyPathData) window._lmMyPathData = {};
data.steps.forEach(function(s){ window._lmMyPathData[s.x+'_'+s.y]=true; });
var emKeys = [];
if (Array.isArray(data.emission_cells)) {
emKeys = data.emission_cells.map(function(c){ return c.x + '_' + c.y; });
_lmEmissionCells = new Set(emKeys);
}
try {
window.lmLS.set(LS_HIST, JSON.stringify(data.steps));
window.lmLS.set(LS_HIST_TS, String(Date.now()));
window.lmLS.set(LS_EM, JSON.stringify(emKeys));
} catch(e) {}
lmApplyHistoricalPath();
lmApplyEmissionCells();
})
.catch(function() {});
}
if (IS_LABYRINTH) setTimeout(lmFetchHistoricalPath, 1500);
(function loadMineSettingsFromServer() {
var u = window.visitor_name || '';
if (!u) return;
// Якщо обидва значення вже є в localStorage — не робимо зайвий запит
var lsEnabled = window.lmLS.get('lm_mine_auto_enabled' + _lmUserSuffix);
var lsThreshold = window.lmLS.get('lm_mine_threshold' + _lmUserSuffix);
if (lsEnabled !== null && lsThreshold !== null) return;
window.lmFetch(VPS_URL + '/my/profile', { headers: vpsAuthHeaders(false) })
.then(function(r) { return r.ok ? r.json() : null; })
.then(function(p) {
if (!p) return;
if (lsEnabled === null && p.mine_auto_enabled != null) {
window.lmLS.set('lm_mine_auto_enabled' + _lmUserSuffix, p.mine_auto_enabled ? 'true' : 'false');
}
if (lsThreshold === null && p.mine_auto_threshold != null) {
window.lmLS.set('lm_mine_threshold' + _lmUserSuffix, String(p.mine_auto_threshold));
}
})
.catch(function() {});
})();
scheduleProfileSync();
setInterval(function(){ syncProfileToVPS(false); }, 60000);
if (IS_CLUB_PAGE) {
setTimeout(function(){ checkAndSyncClub(true); }, 1200);
setInterval(function(){ checkAndSyncClub(false); }, 10 * 60 * 1000);
}
window._lmVPS = {
syncRollback: function() {
var tb = window._lmAllTimeTrapBacks;
var tp = window._lmRollbackPlace;
lmDebug('[Синхронизация] Manual sync rollback:', tb, tp);
if (tb) sendRollbackToVPS(tb, tp||null);
},
syncHistory: sendAccHistoryToVPS,
syncAll: function() {
window._lmVPS.syncRollback();
sendAccHistoryToVPS();
},
getHistCache: function() { return histCache; },
getHistSample: function() {
var keys = Object.keys(histCache);
if (!keys.length) return 'Кеш истории пуст';
var first = histCache[keys[0]];
console.log('pages:', keys.length, '| rows[0]:', JSON.stringify(first));
return first;
}
};
})();