LZT counter

Универсальный счётчик LZT

You will need to install an extension such as Tampermonkey, Greasemonkey or Violentmonkey to install this script.

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

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

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

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

You will need to install a user script manager extension to install this script.

(I already have a user script manager, let me install it!)

You will need to install an extension such as Stylus to install this style.

You will need to install an extension such as Stylus to install this style.

You will need to install an extension such as Stylus to install this style.

You will need to install a user style manager extension to install this style.

You will need to install a user style manager extension to install this style.

You will need to install a user style manager extension to install this style.

(I already have a user style manager, let me install it!)

// ==UserScript==
// @name         LZT counter
// @namespace    Счётчик кликов
// @author       Plarq
// @version      1.0
// @description  Универсальный счётчик LZT
// @license      Apache 2.0
// @match        https://zelenka.guru/*
// @match        https://lolz.live/*
// @icon         https://lolz.live/styles/brand/download/avatars/three_avatar.svg
// @grant        GM.getValue
// @grant        GM.setValue
// @grant        GM.xmlHttpRequest
// @grant        unsafeWindow
// @run-at       document-end
// @require      https://cdnjs.cloudflare.com/ajax/libs/lodash.js/4.17.21/lodash.min.js
// ==/UserScript==

/* global _ */

(function() {
    'use strict';

    const CONFIG = {
        COUNTER_ID: 'lzt-counter-visible',
        SETTINGS_BTN_ID: 'lzt-counter-settings-btn',
        ACHIEVEMENTS_BTN_ID: 'lzt-counter-achievements-btn',
        SETTINGS_MENU_ID: 'lzt-counter-settings-menu',
        ACHIEVEMENTS_MENU_ID: 'lzt-counter-achievements-menu',
        STORAGE_KEY: 'lztGlobalCounterV2',
        STORAGE_MAX_KEY: 'lztGlobalCounterMaxV2',
        STYLES_KEY: 'lztCounterStyles',
        FONT_SIZE_KEY: 'lztCounterFontSize',
        BACKGROUND_OPACITY_KEY: 'lztCounterBgOpacity',
        BUTTON_SELECTORS: {
            'zelenka.guru': '[data-t="update"]',
            'lzt.market': '.feed__refresh-button',
            'lolz.market': '.refresh-feed',
            'lolz.live': '.UpdateFeedButton'
        },
        ACHIEVEMENTS: [
            { threshold: 100, title: 'Новокек' },
            { threshold: 500, title: 'Местный' },
            { threshold: 1000, title: 'Постоялец' },
            { threshold: 2500, title: 'Эксперт' },
            { threshold: 5000, title: 'Гуру' },
            { threshold: 10000, title: 'Искусственный интеллект' }
        ],
        INITIAL_POSITION: { x: 260, y: 950 },
        DEFAULT_FONT_SIZE: 18,
        DEFAULT_BG_OPACITY: 1,
        NOTIFICATION_DURATION: 5000,
        NOTIFICATION_OFFSET: 20,
        DEBUG: false
    };

    let isDragging = false;
    let animationFrameId = null;

    function log(...args) {
        if (CONFIG.DEBUG) console.log('[LZT Counter]', ...args);
    }

    function isFeedPage() {
        const path = window.location.pathname;
        return path.includes('/feed') || path === '/';
    }

    function hexToRgba(hex, alpha) {
        const r = parseInt(hex.slice(1, 3), 16);
        const g = parseInt(hex.slice(3, 5), 16);
        const b = parseInt(hex.slice(5, 7), 16);
        return `rgba(${r}, ${g}, ${b}, ${alpha})`;
    }

    async function applyStyles(counterElement) {
        const savedStyles = await GM.getValue(CONFIG.STYLES_KEY, {});
        const fontSize = await GM.getValue(CONFIG.FONT_SIZE_KEY, CONFIG.DEFAULT_FONT_SIZE);
        const bgOpacity = await GM.getValue(CONFIG.BACKGROUND_OPACITY_KEY, CONFIG.DEFAULT_BG_OPACITY);

        counterElement.style.cssText = `
            position: fixed;
            color: ${savedStyles.color || '#ffffff'};
            background: ${savedStyles.background === false ?
                'transparent' :
                hexToRgba('#1A1A1A', bgOpacity)};
            box-shadow: ${savedStyles.background === false ? 'none' : '0 2px 5px rgba(0,0,0,0.2)'};
            padding: ${savedStyles.background === false ? '0' : '8px 15px'};
            border-radius: 5px;
            z-index: 9998;
            cursor: pointer;
            font-size: ${fontSize}px;
            font-weight: bold;
            user-select: none;
            min-width: 60px;
            text-align: center;
            white-space: nowrap;
            transition: all 0.2s ease;
            box-sizing: border-box;
            left: ${savedStyles.customPosition?.x || CONFIG.INITIAL_POSITION.x}px;
            top: ${savedStyles.customPosition?.y || CONFIG.INITIAL_POSITION.y}px;
        `;
    }

    function createButton(text, color) {
        const btn = document.createElement('button');
        btn.textContent = text;
        btn.style.cssText = `
            flex: 1;
            background: ${color};
            color: white;
            border: none;
            padding: 8px;
            border-radius: 4px;
            cursor: pointer;
            font-size: 14px;
            white-space: nowrap;
            transition: opacity 0.2s;
        `;
        btn.addEventListener('mouseover', () => btn.style.opacity = '0.8');
        btn.addEventListener('mouseout', () => btn.style.opacity = '1');
        return btn;
    }

    function createSettingsMenu() {
        const menu = document.createElement('div');
        menu.id = CONFIG.SETTINGS_MENU_ID;
        menu.style.cssText = `
            position: fixed;
            bottom: 60px;
            left: 20px;
            background: #1A1A1A;
            color: white;
            padding: 15px;
            border-radius: 8px;
            box-shadow: 0 2px 10px rgba(0,0,0,0.1);
            z-index: 9999;
            display: none;
            width: 300px;
            box-sizing: border-box;
        `;

        const createRow = () => {
            const row = document.createElement('div');
            row.style.cssText = 'display: flex; gap: 8px; margin-bottom: 12px;';
            return row;
        };

        const row1 = createRow();
        const colorBtn = createButton('Цвет', '#228E5D');
        const bgBtn = createButton('Фон', '#228E5D');
        row1.appendChild(colorBtn);
        row1.appendChild(bgBtn);

        const row2 = createRow();
        const moveBtn = createButton('Переместить', '#228E5D');
        const resetBtn = createButton('Сброс позиции', '#228E5D');
        row2.appendChild(moveBtn);
        row2.appendChild(resetBtn);

        const createSlider = (config) => {
            const container = document.createElement('div');
            container.style.cssText = 'margin: 15px 0;';

            const label = document.createElement('div');
            label.textContent = config.label;
            label.style.cssText = `
                color: rgba(255,255,255,0.7);
                font-size: 12px;
                margin-bottom: 8px;
                display: flex;
                justify-content: space-between;
            `;

            const value = document.createElement('span');
            value.textContent = config.valueText;

            const slider = document.createElement('input');
            slider.type = 'range';
            Object.assign(slider, config.sliderProps);
            slider.style.cssText = `
                width: 100%;
                height: 4px;
                background: #333;
                border-radius: 2px;
                -webkit-appearance: none;
                outline: none;

                &::-webkit-slider-thumb {
                    -webkit-appearance: none;
                    width: 16px;
                    height: 16px;
                    background: #228E5D;
                    border-radius: 50%;
                    cursor: pointer;
                }
            `;

            slider.addEventListener('input', _.throttle(async (e) => {
                const newValue = config.parser(e.target.value);
                value.textContent = config.formatter(newValue);
                await GM.setValue(config.key, newValue);
                applyStyles(document.getElementById(CONFIG.COUNTER_ID));
            }, 100));

            label.appendChild(value);
            container.appendChild(label);
            container.appendChild(slider);
            return container;
        };

        const fontSizeSlider = createSlider({
            label: 'Размер текста:',
            key: CONFIG.FONT_SIZE_KEY,
            sliderProps: {
                min: 12,
                max: 36,
                step: 1,
                value: CONFIG.DEFAULT_FONT_SIZE
            },
            parser: parseInt,
            formatter: v => `${v}px`,
            valueText: `${CONFIG.DEFAULT_FONT_SIZE}px`
        });

        const opacitySlider = createSlider({
            label: 'Прозрачность фона:',
            key: CONFIG.BACKGROUND_OPACITY_KEY,
            sliderProps: {
                min: 0,
                max: 1,
                step: 0.1,
                value: CONFIG.DEFAULT_BG_OPACITY
            },
            parser: parseFloat,
            formatter: v => `${Math.round(v * 100)}%`,
            valueText: `${Math.round(CONFIG.DEFAULT_BG_OPACITY * 100)}%`
        });

        const hint = document.createElement('div');
        hint.style.cssText = `
            color: rgba(255,255,255,0.7);
            font-size: 12px;
            margin-top: 10px;
            padding-top: 10px;
            border-top: 1px solid rgba(255,255,255,0.2);
        `;
        hint.innerHTML = 'Для сохранения позиции<br>нажмите ЛКМ во время перемещения';

        menu.appendChild(row1);
        menu.appendChild(row2);
        menu.appendChild(fontSizeSlider);
        menu.appendChild(opacitySlider);
        menu.appendChild(hint);

        const colorInput = document.createElement('input');
        colorInput.type = 'color';
        colorInput.style.cssText = 'position: absolute; opacity: 0; pointer-events: none;';

        colorBtn.addEventListener('click', () => {
            colorInput.value = document.getElementById(CONFIG.COUNTER_ID).style.color;
            colorInput.click();
        });

        colorInput.addEventListener('input', async (e) => {
            const styles = await GM.getValue(CONFIG.STYLES_KEY, {});
            styles.color = e.target.value;
            await GM.setValue(CONFIG.STYLES_KEY, styles);
            applyStyles(document.getElementById(CONFIG.COUNTER_ID));
        });

        bgBtn.addEventListener('click', async () => {
            const styles = await GM.getValue(CONFIG.STYLES_KEY, {});
            styles.background = !styles.background;
            await GM.setValue(CONFIG.STYLES_KEY, styles);
            applyStyles(document.getElementById(CONFIG.COUNTER_ID));
            bgBtn.textContent = styles.background ? 'Убрать фон' : 'Вернуть фон';
        });

        moveBtn.addEventListener('click', () => startDragging());

        resetBtn.addEventListener('click', async () => {
            const styles = await GM.getValue(CONFIG.STYLES_KEY, {});
            delete styles.customPosition;
            await GM.setValue(CONFIG.STYLES_KEY, styles);
            applyStyles(document.getElementById(CONFIG.COUNTER_ID));
        });

        Promise.all([
            GM.getValue(CONFIG.FONT_SIZE_KEY, CONFIG.DEFAULT_FONT_SIZE),
            GM.getValue(CONFIG.BACKGROUND_OPACITY_KEY, CONFIG.DEFAULT_BG_OPACITY)
        ]).then(([fontSize, opacity]) => {
            fontSizeSlider.querySelector('input').value = fontSize;
            fontSizeSlider.querySelector('span').textContent = `${fontSize}px`;
            opacitySlider.querySelector('input').value = opacity;
            opacitySlider.querySelector('span').textContent = `${Math.round(opacity * 100)}%`;
        });

        document.body.appendChild(menu);
        document.body.appendChild(colorInput);
        return menu;
    }

    function createAchievementsMenu() {
        const menu = document.createElement('div');
        menu.id = CONFIG.ACHIEVEMENTS_MENU_ID;
        menu.style.cssText = `
            position: fixed;
            bottom: 60px;
            left: 20px;
            background: #1A1A1A;
            color: white;
            padding: 15px;
            border-radius: 8px;
            box-shadow: 0 2px 10px rgba(0,0,0,0.1);
            z-index: 9999;
            display: none;
            width: 300px;
            box-sizing: border-box;
        `;

        const title = document.createElement('div');
        title.textContent = 'Достижения';
        title.style.cssText = 'font-weight: bold; margin-bottom: 15px; font-size: 16px;';

        const progressContainer = document.createElement('div');
        progressContainer.style.marginBottom = '15px';

        const progressBar = document.createElement('div');
        progressBar.style.cssText = `
            background: #333;
            height: 10px;
            border-radius: 5px;
            overflow: hidden;
        `;

        const progressFill = document.createElement('div');
        progressFill.style.cssText = `
            background: #228E5D;
            height: 100%;
            width: 0%;
            transition: width 0.3s ease;
        `;

        progressBar.appendChild(progressFill);
        progressContainer.appendChild(progressBar);

        const achievementsList = document.createElement('div');
        achievementsList.style.cssText = 'max-height: 300px; overflow-y: auto;';

        menu.appendChild(title);
        menu.appendChild(progressContainer);
        menu.appendChild(achievementsList);

        async function updateMenu() {
            const maxCounter = await GM.getValue(CONFIG.STORAGE_MAX_KEY, 0);
            const achievements = CONFIG.ACHIEVEMENTS;

            let currentAchievement = null;
            let nextAchievement = null;
            for (let i = 0; i < achievements.length; i++) {
                if (maxCounter >= achievements[i].threshold) {
                    currentAchievement = achievements[i];
                    if (i < achievements.length - 1) {
                        nextAchievement = achievements[i + 1];
                    }
                } else {
                    if (!nextAchievement) nextAchievement = achievements[i];
                    break;
                }
            }

            if (currentAchievement && nextAchievement) {
                const progress = ((maxCounter - currentAchievement.threshold) / (nextAchievement.threshold - currentAchievement.threshold)) * 100;
                progressFill.style.width = `${Math.min(progress, 100)}%`;
            } else if (currentAchievement && !nextAchievement) {
                progressFill.style.width = '100%';
            } else if (!currentAchievement && nextAchievement) {
                const progress = (maxCounter / nextAchievement.threshold) * 100;
                progressFill.style.width = `${progress}%`;
            } else {
                progressFill.style.width = '0%';
            }

            achievementsList.innerHTML = '';
            achievements.forEach(ach => {
                const isUnlocked = maxCounter >= ach.threshold;
                const item = document.createElement('div');
                item.style.cssText = `
                    display: flex;
                    justify-content: space-between;
                    align-items: center;
                    padding: 8px 0;
                    border-bottom: 1px solid rgba(255,255,255,0.1);
                `;

                const titleSpan = document.createElement('span');
                titleSpan.textContent = ach.title;
                titleSpan.style.color = isUnlocked ? '#fff' : 'rgba(255,255,255,0.5)';

                const thresholdSpan = document.createElement('span');
                thresholdSpan.textContent = ach.threshold;
                thresholdSpan.style.color = isUnlocked ? '#228E5D' : 'rgba(255,255,255,0.5)';
                thresholdSpan.style.fontSize = '14px';

                item.appendChild(titleSpan);
                item.appendChild(thresholdSpan);
                achievementsList.appendChild(item);
            });
        }

        menu.updateMenu = updateMenu;
        menu.addEventListener('click', (e) => e.stopPropagation());
        document.body.appendChild(menu);
        return menu;
    }

    function showAchievementNotification(achievement) {
        const notification = document.createElement('div');
        notification.className = 'lzt-achievement-notification';
        notification.innerHTML = `
            <div style="font-size: 16px; color: #228E5D; margin-bottom: 4px;">✓ Достижение получено!</div>
            <div style="font-size: 14px;"> Вы теперь ${achievement.title}</div>
        `;

        notification.style.cssText = `
            position: fixed;
            left: ${CONFIG.NOTIFICATION_OFFSET}px;
            top: 50%;
            transform: translateY(-50%);
            background: #1A1A1A;
            color: white;
            padding: 16px;
            border-radius: 8px;
            box-shadow: 0 4px 12px rgba(0,0,0,0.2);
            z-index: 10000;
            cursor: pointer;
            opacity: 0;
            transition: opacity 0.3s ease, transform 0.3s ease;
            max-width: 250px;
        `;

        document.body.appendChild(notification);

        requestAnimationFrame(() => {
            notification.style.opacity = '1';
            notification.style.transform = `translate(10px, -50%)`;
        });

        setTimeout(() => {
            notification.style.opacity = '0';
            setTimeout(() => notification.remove(), 300);
        }, CONFIG.NOTIFICATION_DURATION);

        notification.addEventListener('click', () => {
            notification.style.opacity = '0';
            setTimeout(() => notification.remove(), 300);
        });
    }

    async function checkAchievements(newMax) {
        const shownAchievements = await GM.getValue('shownAchievements', {});
        const newAchievements = [];

        CONFIG.ACHIEVEMENTS.forEach(ach => {
            if (newMax >= ach.threshold && !shownAchievements[ach.threshold]) {
                newAchievements.push(ach);
                shownAchievements[ach.threshold] = true;
            }
        });

        if (newAchievements.length > 0) {
            await GM.setValue('shownAchievements', shownAchievements);
            newAchievements.forEach(ach => showAchievementNotification(ach));
        }
    }

    function startDragging() {
        const counter = document.getElementById(CONFIG.COUNTER_ID);
        const menu = document.getElementById(CONFIG.SETTINGS_MENU_ID);
        isDragging = true;
        menu.style.display = 'none';
        counter.style.cursor = 'grabbing';

        let lastX = 0, lastY = 0;
        let posX = 0, posY = 0;

        const pointerMoveHandler = e => {
            if (!isDragging) return;
            lastX = e.clientX;
            lastY = e.clientY;

            if (!animationFrameId) {
                animationFrameId = requestAnimationFrame(updatePosition);
            }
        };

        const updatePosition = () => {
            if (!isDragging) return;

            const counterRect = counter.getBoundingClientRect();
            const maxX = window.innerWidth - counterRect.width;
            const maxY = window.innerHeight - counterRect.height;

            posX = Math.min(Math.max(lastX - counterRect.width/2, 0), maxX);
            posY = Math.min(Math.max(lastY - counterRect.height/2, 0), maxY);

            counter.style.left = `${posX}px`;
            counter.style.top = `${posY}px`;

            animationFrameId = requestAnimationFrame(updatePosition);
        };

        const pointerUpHandler = async () => {
            isDragging = false;
            counter.style.cursor = 'pointer';
            cancelAnimationFrame(animationFrameId);
            animationFrameId = null;

            const styles = await GM.getValue(CONFIG.STYLES_KEY, {});
            styles.customPosition = {x: posX, y: posY};
            await GM.setValue(CONFIG.STYLES_KEY, styles);

            document.removeEventListener('pointermove', pointerMoveHandler);
            document.removeEventListener('pointerup', pointerUpHandler);
        };

        document.addEventListener('pointermove', pointerMoveHandler);
        document.addEventListener('pointerup', pointerUpHandler, {once: true});
    }

    async function main() {
        if (!isFeedPage()) return;

        const domain = window.location.hostname.replace('www.', '');
        log('Domain:', domain);

        let counter = await GM.getValue(CONFIG.STORAGE_KEY, 0);
        let maxCounter = await GM.getValue(CONFIG.STORAGE_MAX_KEY, 0);
        if (maxCounter < counter) {
            await GM.setValue(CONFIG.STORAGE_MAX_KEY, counter);
        }

        let counterElement = document.getElementById(CONFIG.COUNTER_ID);
        if (!counterElement) {
            counterElement = document.createElement('div');
            counterElement.id = CONFIG.COUNTER_ID;
            counterElement.textContent = counter;

            counterElement.addEventListener('dblclick', async () => {
                await GM.setValue(CONFIG.STORAGE_KEY, 0);
                counterElement.textContent = '0';
            });

            document.body.appendChild(counterElement);
            await applyStyles(counterElement);
        }

        if (!document.getElementById(CONFIG.SETTINGS_BTN_ID)) {
            const buttonsContainer = document.createElement('div');
            buttonsContainer.id = 'lzt-counter-buttons-container';
            buttonsContainer.style.cssText = `
                position: fixed;
                bottom: 20px;
                left: 20px;
                z-index: 9999;
                display: flex;
                gap: 8px;
            `;

            const settingsBtn = document.createElement('button');
            settingsBtn.id = CONFIG.SETTINGS_BTN_ID;
            settingsBtn.textContent = 'Настройки';
            settingsBtn.style.cssText = `
                padding: 8px 16px;
                background: #1A1A1A;
                color: white;
                border: none;
                border-radius: 5px;
                cursor: pointer;
                font-size: 14px;
                white-space: nowrap;
                box-shadow: 0 2px 5px rgba(0,0,0,0.2);
            `;

            const achievementsBtn = document.createElement('button');
            achievementsBtn.id = CONFIG.ACHIEVEMENTS_BTN_ID;
            achievementsBtn.textContent = 'Достижения';
            achievementsBtn.style.cssText = settingsBtn.style.cssText;

            buttonsContainer.appendChild(settingsBtn);
            buttonsContainer.appendChild(achievementsBtn);
            document.body.appendChild(buttonsContainer);

            const settingsMenu = createSettingsMenu();
            const achievementsMenu = createAchievementsMenu();

            settingsBtn.addEventListener('click', (e) => {
                e.stopPropagation();
                settingsMenu.style.display = settingsMenu.style.display === 'block' ? 'none' : 'block';
                achievementsMenu.style.display = 'none';
            });

            achievementsBtn.addEventListener('click', (e) => {
                e.stopPropagation();
                achievementsMenu.style.display = achievementsMenu.style.display === 'block' ? 'none' : 'block';
                settingsMenu.style.display = 'none';
                achievementsMenu.updateMenu();
            });

            document.addEventListener('click', (e) => {
                if (!settingsMenu.contains(e.target) && !achievementsMenu.contains(e.target) &&
                    !settingsBtn.contains(e.target) && !achievementsBtn.contains(e.target)) {
                    settingsMenu.style.display = 'none';
                    achievementsMenu.style.display = 'none';
                }
            });
        }

        const buttonSelector = CONFIG.BUTTON_SELECTORS[domain];
        const handleClick = _.throttle(async () => {
            const current = await GM.getValue(CONFIG.STORAGE_KEY, 0);
            const newCounter = current + 1;
            const maxCounter = await GM.getValue(CONFIG.STORAGE_MAX_KEY, 0);
            const newMax = Math.max(maxCounter, newCounter);

            await GM.setValue(CONFIG.STORAGE_KEY, newCounter);
            await GM.setValue(CONFIG.STORAGE_MAX_KEY, newMax);

            document.getElementById(CONFIG.COUNTER_ID).textContent = newCounter;

            if (newMax > maxCounter) {
                await checkAchievements(newMax);
            }
        }, 1000);

        new MutationObserver(() => {
            const button = document.querySelector(buttonSelector);
            if (button && !button.dataset.listener) {
                button.addEventListener('click', handleClick);
                button.dataset.listener = 'true';
            }
        }).observe(document.documentElement, {
            childList: true,
            subtree: true
        });
    }

    main().catch(e => log('Error:', e));
})();