Pixel Battles Bot

Бот для автоматического рисования на Pixel Battles

// ==UserScript==
// @license MIT
// @name         Pixel Battles Bot
// @namespace    http://tampermonkey.net/
// @version      3.0
// @description  Бот для автоматического рисования на Pixel Battles
// @author       .hilkach.
// @match        https://pixelbattles.ru/*
// @grant        GM_addStyle
// @grant        GM_xmlhttpRequest
// @connect      pixelbattles.ru
// ==/UserScript==

(function() {
    'use strict';

    // ===== ГЛОБАЛЬНЫЕ НАСТРОЙКИ ===== //
const SETTINGS = {
    modes: {
        "top-down": "⬇️ Сверху вниз",
        "bottom-up": "⬆️ Снизу вверх",
        "left-right": "➡️ Слева направо",
        "right-left": "⬅️ Справа налево",
        "random": "🎲 Случайные точки",
        "diagonal-lt-rb": "↘️ Лев.верх → прав.низ",
        "diagonal-rt-lb": "↙️ Прав.верх → лев.низ",
        "diagonal-lb-rt": "↗️ Лев.низ → прав.верх",
        "diagonal-rb-lt": "↖️ Прав.низ → лев.верх"
    },
    colors: {
        "deepcarmine": "Красный",
        "flame": "Оранжево-красный",
        "yelloworange": "Жёлто-оранжевый",
        "naplesyellow": "Жёлтый",
        "mediumseagreen": "Зелёный",
        "emerald": "Изумрудный",
        "inchworm": "Салатовый",
        "myrtlegreen": "Тёмно-зелёный",
        "verdigris": "Бирюзовый",
        "cyancobaltblue": "Синий кобальт",
        "unitednationsblue": "Синий",
        "mediumskyblue": "Голубой",
        "oceanblue": "Морской волны",
        "VeryLightBlue": "Светло-голубой",
        "grape": "Фиолетовый",
        "purpleplum": "Сливовый",
        "darkpink": "Розовый",
        "mauvelous": "Розоватый",
        "coffee": "Коричневый",
        "coconut": "Бежевый",
        "black": "Чёрный",
        "philippinegray": "Серый",
        "lightsilver": "Серебристый",
        "white": "Белый"
    },
    colorHexes: [
        "#ae233d", "#ec5427", "#f4ab3c", "#f9d759", "#48a06d",
        "#5cc87f", "#9ae96c", "#317270", "#469ca8", "#2d519e",
        "#4d90e3", "#7ee6f2", "#4440ba", "#6662f6", "#772b99",
        "#a754ba", "#eb4e81", "#f19eab", "#684a34", "#956a34",
        "#000000", "#898d90", "#d5d7d9", "#ffffff"
    ],
    color: "black",
    delay: 1000,
    isRunning: false,
    abortController: null,
    configs: []
};

// ===== ФУНКЦИИ РИСОВАНИЯ ===== //
async function placePixel(x, y, color, signal) {
    try {
        if (signal?.aborted) throw new Error("Операция прервана");

        const response = await fetch("https://api.pixelbattles.ru/pix", {
            method: "PUT",
            headers: { "Content-Type": "application/json" },
            body: JSON.stringify({ x, y, color }),
            credentials: "include",
            signal
        });

        console.log(`✅ (${x}, ${y})`);
        return true;
    } catch (error) {
        if (error.name !== "AbortError") console.error(`❌ (${x}, ${y}):`, error.message);
        return false;
    }
}

// ===== ФУНКЦИИ РИСОВАНИЯ (РЕЖИМЫ) ===== //
const drawingModes = {
    "top-down": async function*(x, y, w, h) {
        for (let row = y; row < y + h; row++) {
            for (let col = x; col < x + w; col++) {
                yield {x: col, y: row};
            }
        }
    },
    "bottom-up": async function*(x, y, w, h) {
        for (let row = y + h - 1; row >= y; row--) {
            for (let col = x; col < x + w; col++) {
                yield {x: col, y: row};
            }
        }
    },
    "left-right": async function*(x, y, w, h) {
        for (let col = x; col < x + w; col++) {
            for (let row = y; row < y + h; row++) {
                yield {x: col, y: row};
            }
        }
    },
    "right-left": async function*(x, y, w, h) {
        for (let col = x + w - 1; col >= x; col--) {
            for (let row = y; row < y + h; row++) {
                yield {x: col, y: row};
            }
        }
    },
    "random": async function*(x, y, w, h) {
        const pixels = [];
        for (let row = y; row < y + h; row++) {
            for (let col = x; col < x + w; col++) {
                pixels.push({x: col, y: row});
            }
        }
        for (let i = pixels.length - 1; i > 0; i--) {
            const j = Math.floor(Math.random() * (i + 1));
            [pixels[i], pixels[j]] = [pixels[j], pixels[i]];
        }
        for (const pixel of pixels) yield pixel;
    },
    "diagonal-lt-rb": async function*(x, y, w, h) {
        for (let d = 0; d < w + h - 1; d++) {
            const startCol = Math.max(0, d - h + 1);
            const endCol = Math.min(d, w - 1);
            for (let col = startCol; col <= endCol; col++) {
                yield {x: x + col, y: y + (d - col)};
            }
        }
    },
    "diagonal-rt-lb": async function*(x, y, w, h) {
        for (let d = 0; d < w + h - 1; d++) {
            const startCol = Math.max(0, (w - 1) - d);
            const endCol = Math.min(w - 1, (w + h - 2) - d);
            for (let col = startCol; col <= endCol; col++) {
                yield {x: x + col, y: y + (d - ((w - 1) - col))};
            }
        }
    },
    "diagonal-lb-rt": async function*(x, y, w, h) {
        for (let d = 0; d < w + h - 1; d++) {
            const startCol = Math.max(0, d - h + 1);
            const endCol = Math.min(d, w - 1);
            for (let col = startCol; col <= endCol; col++) {
                yield {x: x + col, y: y + (h - 1) - (d - col)};
            }
        }
    },
    "diagonal-rb-lt": async function*(x, y, w, h) {
        for (let d = 0; d < w + h - 1; d++) {
            const startCol = Math.max(0, (w - 1) - d);
            const endCol = Math.min(w - 1, (w + h - 2) - d);
            for (let col = startCol; col <= endCol; col++) {
                yield {x: x + col, y: y + (h - 1) - (d - ((w - 1) - col))};
            }
        }
    }
};

// ===== СИСТЕМА КОНФИГУРАЦИЙ ===== //
function saveConfig(name) {
    if (!name.trim()) {
        alert("Введите название конфига!");
        return;
    }

    const config = {
        name: name.trim(),
        date: new Date().toISOString(),
        settings: {
            mode: document.querySelector('#mode-select').value,
            color: SETTINGS.color,
            x: parseInt(document.querySelector('#start-x').value),
            y: parseInt(document.querySelector('#start-y').value),
            w: parseInt(document.querySelector('#width').value),
            h: parseInt(document.querySelector('#height').value),
            delay: parseInt(document.querySelector('#delay').value)
        }
    };

    const existingIndex = SETTINGS.configs.findIndex(c => c.name === config.name);
    if (existingIndex >= 0) {
        if (!confirm(`Конфиг "${config.name}" уже существует. Перезаписать?`)) return;
        SETTINGS.configs[existingIndex] = config;
    } else {
        SETTINGS.configs.push(config);
    }

    localStorage.setItem('pixelBotConfigs', JSON.stringify(SETTINGS.configs));
    updateConfigsList();
    alert(`Конфиг "${config.name}" сохранён!`);
}

function applyConfig(config) {
    const { settings } = config;
    document.querySelector('#mode-select').value = settings.mode;
    document.querySelector('#color-select').value = settings.color;
    document.querySelector('#start-x').value = settings.x;
    document.querySelector('#start-y').value = settings.y;
    document.querySelector('#width').value = settings.w;
    document.querySelector('#height').value = settings.h;
    document.querySelector('#delay').value = settings.delay;
    document.querySelector('#config-name').value = config.name;

    SETTINGS.color = settings.color;
    const colorIndex = Object.keys(SETTINGS.colors).indexOf(SETTINGS.color);
    document.querySelector('#current-color').style.background = SETTINGS.colorHexes[colorIndex];
}

function renameConfig(oldName, newName) {
    if (!newName || !newName.trim()) {
        alert("Введите новое название!");
        return;
    }

    newName = newName.trim();
    if (oldName === newName) return;

    if (SETTINGS.configs.some(c => c.name === newName)) {
        alert("Конфиг с таким именем уже существует!");
        return;
    }

    const configIndex = SETTINGS.configs.findIndex(c => c.name === oldName);
    if (configIndex >= 0) {
        SETTINGS.configs[configIndex].name = newName;
        SETTINGS.configs[configIndex].date = new Date().toISOString();
        localStorage.setItem('pixelBotConfigs', JSON.stringify(SETTINGS.configs));
        updateConfigsList();
    }
}

function deleteConfig(name) {
    if (!confirm(`Удалить конфиг "${name}"?`)) return;

    SETTINGS.configs = SETTINGS.configs.filter(c => c.name !== name);
    localStorage.setItem('pixelBotConfigs', JSON.stringify(SETTINGS.configs));
    updateConfigsList();
}

function exportConfig() {
    const configName = document.querySelector('#config-name').value.trim() || "pixelbot_config";
    const config = {
        name: configName,
        date: new Date().toISOString(),
        settings: {
            mode: document.querySelector('#mode-select').value,
            color: SETTINGS.color,
            x: parseInt(document.querySelector('#start-x').value),
            y: parseInt(document.querySelector('#start-y').value),
            w: parseInt(document.querySelector('#width').value),
            h: parseInt(document.querySelector('#height').value),
            delay: parseInt(document.querySelector('#delay').value)
        }
    };

    const blob = new Blob([JSON.stringify(config, null, 2)], {type: 'application/json'});
    const url = URL.createObjectURL(blob);

    const a = document.createElement('a');
    a.href = url;
    a.download = `pixelbot_${configName.replace(/\s+/g, '_')}.json`;
    document.body.appendChild(a);
    a.click();
    document.body.removeChild(a);
    URL.revokeObjectURL(url);
}

function importConfig(event) {
    const file = event.target.files[0];
    if (!file) return;

    const reader = new FileReader();
    reader.onload = (e) => {
        try {
            const config = JSON.parse(e.target.result);
            if (!config.name || !config.settings) {
                throw new Error("Некорректный формат конфига");
            }

            if (confirm(`Импортировать конфиг "${config.name}"?`)) {
                const existingIndex = SETTINGS.configs.findIndex(c => c.name === config.name);
                if (existingIndex >= 0) {
                    if (!confirm(`Конфиг "${config.name}" уже существует. Перезаписать?`)) return;
                    SETTINGS.configs[existingIndex] = config;
                } else {
                    SETTINGS.configs.push(config);
                }

                localStorage.setItem('pixelBotConfigs', JSON.stringify(SETTINGS.configs));
                updateConfigsList();
                document.querySelector('#config-name').value = config.name;
                alert(`Конфиг "${config.name}" успешно импортирован!`);
            }
        } catch (error) {
            console.error("Ошибка импорта:", error);
            alert("Ошибка при импорте конфига: " + error.message);
        }
    };
    reader.readAsText(file);
    event.target.value = '';
}

function updateConfigsList() {
    const configsList = document.querySelector('#configs-list');
    if (!configsList) {
        console.warn('Элемент #configs-list не найден');
        return;
    }

    // Сортируем конфиги по дате (новые сверху)
    const sortedConfigs = [...SETTINGS.configs].sort((a, b) =>
        new Date(b.date) - new Date(a.date)
    );

    configsList.innerHTML = sortedConfigs.map(config => `
        <div class="config-item" style="display: flex; justify-content: space-between; align-items: center; padding: 5px; margin: 2px 0; background: #f9f9f9; border-radius: 3px;">
            <span style="flex: 1; cursor: pointer;"
                  onclick="applyConfig(${JSON.stringify(config).replace(/"/g, '&quot;')})">
                ${config.name}
                <span style="font-size: 0.8em; color: #666; margin-left: 5px;">
                    ${new Date(config.date).toLocaleString()}
                </span>
            </span>
            <div>
                <button onclick="promptRenameConfig('${config.name.replace(/'/g, "\\'")}')"
                        style="background: none; border: none; cursor: pointer; font-size: 0.9em; margin-left: 5px;" title="Переименовать">
                    ✏️
                </button>
                <button onclick="deleteConfig('${config.name.replace(/'/g, "\\'")}')"
                        style="background: none; border: none; cursor: pointer; color: red; font-size: 0.9em; margin-left: 5px;" title="Удалить">
                    ✖
                </button>
            </div>
        </div>
    `).join('');
}

function promptRenameConfig(oldName) {
    const newName = prompt("Введите новое название конфига:", oldName);
    if (newName && newName !== oldName) {
        renameConfig(oldName, newName);
    }
}

function loadConfigs() {
    try {
        const savedConfigs = localStorage.getItem('pixelBotConfigs');
        if (savedConfigs) {
            SETTINGS.configs = JSON.parse(savedConfigs);
            // Добавляем небольшую задержку для гарантированного обновления DOM
            setTimeout(updateConfigsList, 50);
        }
    } catch (e) {
        console.error("Ошибка загрузки конфигов:", e);
    }
}

// ===== ПАНЕЛЬ УПРАВЛЕНИЯ ===== //
function createControlPanel() {
    const panel = document.createElement('div');
    panel.style.cssText = `
        position: fixed;
        top: 20px;
        right: 20px;
        background: white;
        padding: 15px;
        border: 1px solid #ddd;
        border-radius: 10px;
        z-index: 9999;
        font-family: Arial, sans-serif;
        font-size: 13px;
        width: 280px;
        box-shadow: 0 2px 10px rgba(0,0,0,0.1);
        user-select: none;
    `;

    panel.innerHTML = `
    <div id="panel-header" style="cursor: move; padding: 8px 10px; margin: -15px -15px 15px -15px; background: #f5f5f5; border-radius: 8px 8px 0 0; font-size: 14px; display: flex; justify-content: space-between; align-items: center;">
        <strong>🎨 PixelBot v3.0</strong>
        <span id="current-color" style="display: inline-block; width: 18px; height: 18px; background: ${SETTINGS.colorHexes[Object.keys(SETTINGS.colors).indexOf(SETTINGS.color)]}; border: 1px solid #ccc; border-radius: 3px;"></span>
    </div>

    <div style="margin-bottom: 12px;">
        <label style="display: block; margin-bottom: 3px; font-weight: bold; color: #555;">Режим рисования:</label>
        <select id="mode-select" style="width: 100%; padding: 6px; border: 1px solid #ddd; border-radius: 4px; font-size: 13px; background: white;">
            ${Object.entries(SETTINGS.modes).map(([key, desc]) =>
                `<option value="${key}">${desc}</option>`).join('')}
        </select>
    </div>

    <div style="margin-bottom: 12px;">
        <label style="display: block; margin-bottom: 3px; font-weight: bold; color: #555;">Цвет:</label>
        <select id="color-select" style="width: 100%; padding: 6px; border: 1px solid #ddd; border-radius: 4px; font-size: 13px; background: white;">
            ${Object.keys(SETTINGS.colors).map((key, index) =>
                `<option value="${key}" ${key === SETTINGS.color ? 'selected' : ''}>
                    ${SETTINGS.colors[key]}
                    <span style="float: right; display: inline-block; width: 12px; height: 12px; background: ${SETTINGS.colorHexes[index]}; border: 1px solid #ccc; border-radius: 2px;"></span>
                </option>`).join('')}
        </select>
    </div>

    <div style="margin-bottom: 12px;">
        <label style="display: block; margin-bottom: 3px; font-weight: bold; color: #555;">Область рисования:</label>
        <div style="display: grid; grid-template-columns: 1fr 1fr; gap: 8px; font-size: 13px;">
            <div>
                <label style="font-size: 0.9em; color: #666;">X:</label>
                <input type="number" id="start-x" value="0" style="width: 100%; padding: 6px; border: 1px solid #ddd; border-radius: 4px;">
            </div>
            <div>
                <label style="font-size: 0.9em; color: #666;">Y:</label>
                <input type="number" id="start-y" value="0" style="width: 100%; padding: 6px; border: 1px solid #ddd; border-radius: 4px;">
            </div>
            <div>
                <label style="font-size: 0.9em; color: #666;">Ширина:</label>
                <input type="number" id="width" value="10" style="width: 100%; padding: 6px; border: 1px solid #ddd; border-radius: 4px;">
            </div>
            <div>
                <label style="font-size: 0.9em; color: #666;">Высота:</label>
                <input type="number" id="height" value="10" style="width: 100%; padding: 6px; border: 1px solid #ddd; border-radius: 4px;">
            </div>
        </div>
    </div>

    <div style="margin-bottom: 15px;">
        <label style="display: block; margin-bottom: 3px; font-weight: bold; color: #555;">Задержка (мс):</label>
        <input type="number" id="delay" value="1000" style="width: 100%; padding: 6px; border: 1px solid #ddd; border-radius: 4px;">
    </div>

    <div style="margin: 15px 0; border-top: 1px solid #eee; padding-top: 15px;">
        <div style="margin-bottom: 10px;">
            <label style="display: block; margin-bottom: 5px; font-weight: bold; color: #555;">Управление конфигами:</label>
            <div style="display: flex; gap: 8px; margin-bottom: 8px;">
                <input type="text" id="config-name" placeholder="Название конфига" style="flex: 1; padding: 6px; border: 1px solid #ddd; border-radius: 4px;">
                <button id="save-config" style="padding: 6px 10px; background: #4CAF50; color: white; border: none; border-radius: 4px; cursor: pointer; font-size: 13px;">Сохранить</button>
            </div>

            <div id="configs-list" style="max-height: 150px; overflow-y: auto; border: 1px solid #ddd; border-radius: 4px; padding: 5px; background: #fafafa;"></div>

            <div style="display: flex; gap: 8px; margin-top: 10px;">
                <button id="export-config" style="flex: 1; padding: 6px; background: #2196F3; color: white; border: none; border-radius: 4px; cursor: pointer; font-size: 13px;">Экспорт</button>
                <label for="import-file" style="flex: 1; padding: 6px; background: #FF9800; color: white; border: none; border-radius: 4px; cursor: pointer; font-size: 13px; text-align: center;">
                    Импорт
                    <input type="file" id="import-file" accept=".json" style="display: none;">
                </label>
            </div>
        </div>
    </div>

    <div style="margin-bottom: 15px;">
        <div id="progress-text" style="font-size: 12px; text-align: center; margin-bottom: 5px; color: #555;">Готов к работе</div>
        <div id="progress-bar" style="height: 6px; background: #eee; border-radius: 3px;">
            <div id="progress-fill" style="height: 100%; width: 0%; background: #4CAF50; border-radius: 3px; transition: width 0.3s;"></div>
        </div>
    </div>

    <div style="display: flex; gap: 8px;">
        <button id="start-btn" style="flex: 1; padding: 8px; background: #2196F3; color: white; border: none; border-radius: 4px; cursor: pointer; font-size: 13px; font-weight: bold;">
            ▶ Начать рисование
        </button>
        <button id="stop-btn" style="flex: 1; padding: 8px; background: #f44336; color: white; border: none; border-radius: 4px; cursor: pointer; font-size: 13px; display: none;">
            ⏹ Остановить
        </button>
    </div>
    `;

    // Элементы управления
    const startBtn = panel.querySelector('#start-btn');
    const stopBtn = panel.querySelector('#stop-btn');
    const colorSelect = panel.querySelector('#color-select');
    const currentColorIndicator = panel.querySelector('#current-color');
    const saveConfigBtn = panel.querySelector('#save-config');
    const configNameInput = panel.querySelector('#config-name');
    const exportBtn = panel.querySelector('#export-config');
    const importInput = panel.querySelector('#import-file');

    // Обработчики событий
    colorSelect.addEventListener('change', (e) => {
        SETTINGS.color = e.target.value;
        const colorIndex = Object.keys(SETTINGS.colors).indexOf(SETTINGS.color);
        currentColorIndicator.style.background = SETTINGS.colorHexes[colorIndex];
    });

    saveConfigBtn.addEventListener('click', () => {
        saveConfig(configNameInput.value);
    });

    exportBtn.addEventListener('click', exportConfig);

    importInput.addEventListener('change', importConfig);

    // Перетаскивание панели
    const header = panel.querySelector('#panel-header');
    let isDragging = false;
    let offsetX, offsetY;

    header.addEventListener('mousedown', (e) => {
        isDragging = true;
        offsetX = e.clientX - panel.getBoundingClientRect().left;
        offsetY = e.clientY - panel.getBoundingClientRect().top;
        panel.style.cursor = 'grabbing';
        e.preventDefault();
    });

    document.addEventListener('mousemove', (e) => {
        if (!isDragging) return;
        panel.style.left = `${e.clientX - offsetX}px`;
        panel.style.top = `${e.clientY - offsetY}px`;
    });

    document.addEventListener('mouseup', () => {
        isDragging = false;
        panel.style.cursor = 'default';
    });

    // Управление рисованием
    startBtn.addEventListener('click', async () => {
        if (SETTINGS.isRunning) return;

        const mode = panel.querySelector('#mode-select').value;
        const config = {
            x: parseInt(panel.querySelector('#start-x').value),
            y: parseInt(panel.querySelector('#start-y').value),
            w: parseInt(panel.querySelector('#width').value),
            h: parseInt(panel.querySelector('#height').value),
            delay: parseInt(panel.querySelector('#delay').value)
        };

        if (isNaN(config.x) || isNaN(config.y) || isNaN(config.w) || isNaN(config.h) || isNaN(config.delay)) {
            alert('Пожалуйста, введите корректные значения!');
            return;
        }

        const totalPixels = config.w * config.h;
        let processedPixels = 0;

        SETTINGS.delay = config.delay;
        SETTINGS.isRunning = true;
        startBtn.style.display = 'none';
        stopBtn.style.display = 'block';
        SETTINGS.abortController = new AbortController();

        try {
            const generator = drawingModes[mode](config.x, config.y, config.w, config.h);

            for await (const pixel of generator) {
                if (SETTINGS.abortController.signal.aborted) break;

                await placePixel(pixel.x, pixel.y, SETTINGS.color, SETTINGS.abortController.signal);

                processedPixels++;
                const progress = Math.round((processedPixels / totalPixels) * 100);
                panel.querySelector('#progress-fill').style.width = `${progress}%`;
                panel.querySelector('#progress-text').textContent = `Прогресс: ${progress}% (${processedPixels}/${totalPixels})`;

                await new Promise(r => setTimeout(r, SETTINGS.delay));
            }

            panel.querySelector('#progress-text').textContent = "Рисование завершено!";
        } catch (err) {
            if (err.name !== 'AbortError') {
                console.error(err);
                panel.querySelector('#progress-text').textContent = "Ошибка: " + err.message;
            } else {
                panel.querySelector('#progress-text').textContent = "Рисование прервано";
            }
        } finally {
            SETTINGS.isRunning = false;
            startBtn.style.display = 'block';
            stopBtn.style.display = 'none';
        }
    });

    stopBtn.addEventListener('click', () => {
        if (SETTINGS.abortController) {
            SETTINGS.abortController.abort();
        }
    });

    // Добавляем глобальные функции
    window.applyConfig = applyConfig;
    window.deleteConfig = deleteConfig;
    window.promptRenameConfig = promptRenameConfig;

    document.body.appendChild(panel);

    // Загрузка конфигов после добавления панели в DOM
    loadConfigs();
}

// ===== ЗАПУСК ПРИЛОЖЕНИЯ ===== //
if (document.readyState === 'complete') {
    createControlPanel();
} else {
    document.addEventListener('DOMContentLoaded', createControlPanel);
}
})();