// ==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, '"')})">
${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);
}
})();