// ==UserScript==
// @name Gartic Phone DrawBot
// @namespace http://tampermonkey.net/
// @version 1.1
// @description Automated drawing bot with WebSocket interception, image loading, color quantization, preview and progress indicator
// @author NotLun1x
// @match https://garticphone.com/*
// @grant none
// @run-at document-start
// ==/UserScript==
(function () {
'use strict';
// --- Event & Error Logging Helpers ---
function logToPanel(msg, isError = false) {
try {
if (typeof document !== 'undefined') {
const container = document.getElementById('db-log-container');
const content = document.getElementById('db-log-content');
if (container && content) {
container.style.display = 'block';
const color = isError ? '#ef4444' : '#cbd5e1';
content.innerHTML += `
`;
document.body.appendChild(panel);
// Load saved settings
const savedSetting = loadSettings();
if (savedSetting) {
if (document.getElementById('db-scale')) document.getElementById('db-scale').value = savedSetting.scale;
if (document.getElementById('db-colors-mode')) document.getElementById('db-colors-mode').value = savedSetting.colorsMode;
if (document.getElementById('db-draw-mode')) document.getElementById('db-draw-mode').value = savedSetting.drawMode;
if (document.getElementById('db-batch-size')) document.getElementById('db-batch-size').value = savedSetting.batchSize;
if (document.getElementById('db-delay')) document.getElementById('db-delay').value = savedSetting.delay;
if (document.getElementById('db-fill-pps')) document.getElementById('db-fill-pps').value = savedSetting.fillPPS;
if (document.getElementById('db-denoise-level')) document.getElementById('db-denoise-level').value = savedSetting.denoise;
if (document.getElementById('db-fill-bg')) document.getElementById('db-fill-bg').checked = savedSetting.fillBg;
if (document.getElementById('db-use-bridge')) document.getElementById('db-use-bridge').checked = savedSetting.useBridge;
if (document.getElementById('db-bridge-len')) document.getElementById('db-bridge-len').value = savedSetting.bridgeLen;
if (savedSetting.layoutMode) layoutMode = savedSetting.layoutMode;
if (savedSetting.customX !== undefined) customX = savedSetting.customX;
if (savedSetting.customY !== undefined) customY = savedSetting.customY;
if (savedSetting.customW !== undefined) customW = savedSetting.customW;
if (savedSetting.customH !== undefined) customH = savedSetting.customH;
}
if (document.getElementById('db-layout-mode')) {
document.getElementById('db-layout-mode').value = layoutMode;
if (layoutMode === 'custom') {
document.getElementById('db-custom-sliders').style.display = 'block';
}
}
// Make draggable
makeDraggable(panel, document.getElementById('db-header'));
// Event Listeners
const minimizeBtn = document.getElementById('db-minimize-btn');
const contentDiv = document.getElementById('db-content');
let isMinimized = false;
minimizeBtn.addEventListener('click', () => {
isMinimized = !isMinimized;
if (isMinimized) {
contentDiv.style.display = 'none';
minimizeBtn.textContent = '➕';
panel.style.width = '180px';
} else {
contentDiv.style.display = 'block';
minimizeBtn.textContent = '➖';
panel.style.width = '280px';
}
});
// File loading
const fileBtn = document.getElementById('db-file-btn');
const fileInput = document.getElementById('db-file');
fileBtn.addEventListener('click', () => fileInput.click());
fileInput.addEventListener('change', (e) => {
const file = e.target.files[0];
if (!file) return;
console.log('[DrawBot] File selected:', file.name);
const reader = new FileReader();
reader.onload = (event) => {
currentImageSrc = event.target.result;
console.log('[DrawBot] File loaded into memory, length:', currentImageSrc.length);
document.getElementById('db-url').value = file.name;
// Preload draftImg and update layout bounds
loadImage(currentImageSrc).then(img => {
draftImg = img;
const imgRatio = img.width / img.height;
const canvasRatio = 768 / 448;
if (imgRatio > canvasRatio) {
customW = 400;
customH = Math.round(400 / imgRatio);
} else {
customH = 250;
customW = Math.round(250 * imgRatio);
}
customX = Math.round((768 - customW) / 2);
customY = Math.round((448 - customH) / 2);
clampCustomBounds();
updateSliders();
if (layoutMode === 'custom') {
drawDraftPreview();
isApplied = false;
} else {
processAndPreviewImage(currentImageSrc);
isApplied = true;
}
}).catch(err => {
console.error('[DrawBot] Error preloading draft image:', err);
processAndPreviewImage(currentImageSrc);
isApplied = true;
});
updateStatus('File loaded. Click Start.', '#a78bfa');
};
reader.readAsDataURL(file);
});
// URL loading
const urlInput = document.getElementById('db-url');
urlInput.addEventListener('input', (e) => {
const val = e.target.value.trim();
if (val && (val.startsWith('http') || val.startsWith('data:'))) {
currentImageSrc = val;
loadImage(currentImageSrc).then(img => {
draftImg = img;
const imgRatio = img.width / img.height;
const canvasRatio = 768 / 448;
if (imgRatio > canvasRatio) {
customW = 400;
customH = Math.round(400 / imgRatio);
} else {
customH = 250;
customW = Math.round(250 * imgRatio);
}
customX = Math.round((768 - customW) / 2);
customY = Math.round((448 - customH) / 2);
clampCustomBounds();
updateSliders();
if (layoutMode === 'custom') {
drawDraftPreview();
isApplied = false;
} else {
processAndPreviewImage(currentImageSrc);
isApplied = true;
}
}).catch(err => {
console.error('[DrawBot] Error preloading draft image from URL:', err);
processAndPreviewImage(currentImageSrc);
isApplied = true;
});
updateStatus('Image loaded from URL.', '#a78bfa');
}
});
// Preview updating triggers
const handleTriggerUpdate = () => {
saveSettings();
if (layoutMode === 'custom') {
drawDraftPreview();
isApplied = false;
} else {
processAndPreviewImage(currentImageSrc);
isApplied = true;
}
};
document.getElementById('db-scale').addEventListener('change', () => {
handleTriggerUpdate();
});
document.getElementById('db-colors-mode').addEventListener('change', () => {
handleTriggerUpdate();
});
document.getElementById('db-denoise-level').addEventListener('change', () => {
handleTriggerUpdate();
});
document.getElementById('db-fill-bg').addEventListener('change', () => {
handleTriggerUpdate();
});
document.getElementById('db-use-bridge').addEventListener('change', () => {
handleTriggerUpdate();
});
const drawModeSelect = document.getElementById('db-draw-mode');
const batchWrapper = document.getElementById('db-batch-wrapper');
drawModeSelect.addEventListener('change', () => {
handleTriggerUpdate();
});
document.getElementById('db-batch-size').addEventListener('input', () => {
let val = parseInt(document.getElementById('db-batch-size').value, 10);
if (isNaN(val) || val < 1) val = 1;
handleTriggerUpdate();
});
const delayInput = document.getElementById('db-delay');
delayInput.addEventListener('input', () => {
let delay = parseInt(delayInput.value, 10);
if (isNaN(delay) || delay < 0) delay = 0;
CFG.packetDelay = delay;
handleTriggerUpdate();
});
const fillPPSInput = document.getElementById('db-fill-pps');
fillPPSInput.addEventListener('input', () => {
let val = parseInt(fillPPSInput.value, 10);
if (isNaN(val) || val < 1) val = 1;
if (val > 30) val = 30;
CFG.fillPPS = val;
handleTriggerUpdate();
});
const bridgeLenInput = document.getElementById('db-bridge-len');
if (bridgeLenInput) {
bridgeLenInput.addEventListener('input', () => {
let val = parseInt(bridgeLenInput.value, 10);
if (isNaN(val) || val < 1) val = 1;
if (val > 150) val = 150;
CFG.maxBridgeLength = val;
handleTriggerUpdate();
});
}
// Show/hide PPS and Delay depending on drawing mode
const fillPPSWrapper = document.getElementById('db-fill-pps-wrapper');
const delayWrapper = document.getElementById('db-delay-wrapper');
const bridgeWrapper = document.getElementById('db-bridge-wrapper');
const bridgeLenWrapper = document.getElementById('db-bridge-len-wrapper');
const updateFillPPSVisibility = () => {
const isFill = drawModeSelect.value === 'fill';
const useBridge = document.getElementById('db-use-bridge') ? document.getElementById('db-use-bridge').checked : false;
if (fillPPSWrapper) {
fillPPSWrapper.style.display = isFill ? 'flex' : 'none';
}
if (delayWrapper) {
delayWrapper.style.display = isFill ? 'none' : 'flex';
}
if (bridgeWrapper) {
bridgeWrapper.style.display = isFill ? 'flex' : 'none';
}
if (bridgeLenWrapper) {
bridgeLenWrapper.style.display = (isFill && useBridge) ? 'flex' : 'none';
}
if (batchWrapper) {
batchWrapper.style.display = isFill ? 'block' : 'none';
}
};
drawModeSelect.addEventListener('change', updateFillPPSVisibility);
if (document.getElementById('db-use-bridge')) {
document.getElementById('db-use-bridge').addEventListener('change', updateFillPPSVisibility);
}
updateFillPPSVisibility();
// Layout change handler
const layoutSelect = document.getElementById('db-layout-mode');
const customSliders = document.getElementById('db-custom-sliders');
if (layoutSelect) {
layoutSelect.addEventListener('change', () => {
layoutMode = layoutSelect.value;
saveSettings();
if (layoutMode === 'custom') {
if (customSliders) customSliders.style.display = 'block';
updateSliders();
drawDraftPreview();
isApplied = false;
} else {
if (customSliders) customSliders.style.display = 'none';
processAndPreviewImage(currentImageSrc);
isApplied = true;
}
});
}
// Sliders input handlers
const wSlider = document.getElementById('db-custom-w');
const hSlider = document.getElementById('db-custom-h');
const xSlider = document.getElementById('db-custom-x');
const ySlider = document.getElementById('db-custom-y');
const onSliderInput = () => {
if (wSlider) customW = parseInt(wSlider.value, 10);
if (hSlider) customH = parseInt(hSlider.value, 10);
if (xSlider) customX = parseInt(xSlider.value, 10);
if (ySlider) customY = parseInt(ySlider.value, 10);
clampCustomBounds();
updateSliders();
drawDraftPreview();
isApplied = false;
};
if (wSlider) wSlider.addEventListener('input', onSliderInput);
if (hSlider) hSlider.addEventListener('input', onSliderInput);
if (xSlider) xSlider.addEventListener('input', onSliderInput);
if (ySlider) ySlider.addEventListener('input', onSliderInput);
const customApplyBtn = document.getElementById('db-custom-apply');
if (customApplyBtn) {
customApplyBtn.addEventListener('click', () => {
processAndPreviewImage(currentImageSrc);
isApplied = true;
});
}
// Canvas dragging listener
const pCanvas = document.getElementById('db-preview-canvas');
if (pCanvas) {
pCanvas.addEventListener('mousedown', handleCanvasMouseDown);
pCanvas.addEventListener('mousemove', handleCanvasMouseMoveNoDrag);
}
// Drawing control triggers
document.getElementById('db-start').addEventListener('click', () => {
if (!currentImageSrc) {
alert('Please load an image first (file or URL)!');
return;
}
runDraw();
});
const pauseBtn = document.getElementById('db-pause');
pauseBtn.addEventListener('click', () => {
console.log('[DrawBot] Pause button clicked. State changes to:', !isPaused);
isPaused = !isPaused;
if (isPaused) {
pauseBtn.textContent = '▶ Resume';
pauseBtn.style.background = '#059669';
updateStatus('⏸ Pause', '#fbbf24');
} else {
pauseBtn.textContent = '⏸ Pause';
pauseBtn.style.background = '#d97706';
updateStatus('Drawing...', '#a78bfa');
}
});
document.getElementById('db-cancel').addEventListener('click', () => {
console.log('[DrawBot] Cancel button clicked. cancelFlag set to true.');
cancelFlag = true;
isPaused = false;
});
// Hook initial websocket check
if (wsInstance) {
updateStatus('Socket ready. Select a file/URL and click Start.', '#10b981');
} else {
updateStatus('Waiting for game connection...', '#94a3b8');
}
}
function makeDraggable(el, header) {
let pos1 = 0, pos2 = 0, pos3 = 0, pos4 = 0;
header.onmousedown = dragMouseDown;
function dragMouseDown(e) {
e = e || window.event;
if (['BUTTON', 'INPUT', 'SELECT', 'OPTION'].includes(e.target.tagName)) return;
e.preventDefault();
pos3 = e.clientX;
pos4 = e.clientY;
document.onmouseup = closeDragElement;
document.onmousemove = elementDrag;
}
function elementDrag(e) {
e = e || window.event;
e.preventDefault();
pos1 = pos3 - e.clientX;
pos2 = pos4 - e.clientY;
pos3 = e.clientX;
pos4 = e.clientY;
el.style.top = (el.offsetTop - pos2) + "px";
el.style.left = (el.offsetLeft - pos1) + "px";
}
function closeDragElement() {
document.onmouseup = null;
document.onmousemove = null;
}
}
// Initialize UI
function init() {
buildUI();
}
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', init);
} else {
init();
}
})();