Drawaria World Generator

Drawaria World Generator with improved UI, image fitting, optimization controls, and spiral drawing for ~5s rendering.

K instalaci tototo skriptu si budete muset nainstalovat rozšíření jako Tampermonkey, Greasemonkey nebo Violentmonkey.

K instalaci tohoto skriptu si budete muset nainstalovat rozšíření jako Tampermonkey nebo Violentmonkey.

K instalaci tohoto skriptu si budete muset nainstalovat rozšíření jako Tampermonkey nebo Violentmonkey.

K instalaci tohoto skriptu si budete muset nainstalovat rozšíření jako Tampermonkey nebo Userscripts.

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

K instalaci tohoto skriptu si budete muset nainstalovat manažer uživatelských skriptů.

(Už mám manažer uživatelských skriptů, nechte mě ho nainstalovat!)

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.

(Už mám manažer uživatelských stylů, nechte mě ho nainstalovat!)

// ==UserScript==
// @name         Drawaria World Generator
// @namespace    http://tampermonkey.net/
// @version      1.0
// @description  Drawaria World Generator with improved UI, image fitting, optimization controls, and spiral drawing for ~5s rendering.
// @author       YouTubeDrawaria
// @include      https://drawaria.online*
// @icon         https://www.google.com/s2/favicons?sz=64&domain=drawaria.online/room/
// @grant        none
// @license      MIT
// ==/UserScript==

(() => {
    'use strict';

    const EL = (sel) => document.querySelector(sel);
    const ELL = (sel) => document.querySelectorAll(sel);

    // Drawing variables
    let drawing_active = false; // Flag to stop drawing
    let previewCanvas = document.createElement('canvas');
    let originalCanvas = null; // Will be set when canvas is found
    let cw = 0; // Canvas width
    let ch = 0; // Canvas height
    var data; // Image pixel data from previewCanvas
    let executionLine = []; // Array to store drawing commands

    // Room & Socket Control (Keep existing logic)
    window.myRoom = {};
    window.sockets = [];

    const originalSend = WebSocket.prototype.send;
    WebSocket.prototype.send = function (...args) {
        if (window.sockets.indexOf(this) === -1) {
            window.sockets.push(this);
            // Listen only to the first socket connection established by the game itself
            if (window.sockets.indexOf(this) === 0) {
                this.addEventListener('message', (event) => {
                    let message = String(event.data);
                    if (message.startsWith('42')) {
                        let payload = JSON.parse(message.slice(2));
                        // Update room info on relevant messages
                        if (payload[0] == 'bc_uc_freedrawsession_changedroom') {
                            if (payload.length > 3) window.myRoom.players = payload[3];
                            if (payload.length > 4) window.myRoom.id = payload[4]; // Room ID might be here
                        }
                        if (payload[0] == 'mc_roomplayerschange') {
                            if (payload.length > 3) window.myRoom.players = payload[3];
                        }
                        // Additional checks for potential room ID updates
                        if (payload[0] === 'gamestart' && payload[1]?.roomid) {
                            window.myRoom.id = payload[1].roomid;
                            if (payload[1].players) window.myRoom.players = payload[1].players;
                        }
                        // Listen for the drawing canvas to become available
                        if (originalCanvas === null) {
                            originalCanvas = document.getElementById('canvas');
                            if (originalCanvas) {
                                cw = originalCanvas.width;
                                ch = originalCanvas.height;
                                console.log(`Drawaria canvas found: ${cw}x${ch}`);
                            }
                        }
                    } else if (message.startsWith('41')) {
                        // Server acknowledges connection upgrade
                    } else if (message.startsWith('430')) {
                        // Initial room configuration upon connection
                        let configs = JSON.parse(message.slice(3))[0];
                        if (configs) {
                            window.myRoom.players = configs.players;
                            window.myRoom.id = configs.roomid;
                        }
                        // Check for canvas again after initial connection
                        if (originalCanvas === null) {
                            originalCanvas = document.getElementById('canvas');
                            if (originalCanvas) {
                                cw = originalCanvas.width;
                                ch = originalCanvas.height;
                                console.log(`Drawaria canvas found: ${cw}x${ch}`);
                            }
                        }
                    }
                });
            }
        }
        return originalSend.call(this, ...args);
    };

    // Add Boxicons Stylesheet
    function addBoxIcons() {
        let boxicons = document.createElement('link');
        boxicons.href = 'https://unpkg.com/[email protected]/css/boxicons.min.css';
        boxicons.rel = 'stylesheet';
        document.head.appendChild(boxicons);
    }

    // Add Custom Stylesheet
    function CreateStylesheet() {
        let container = document.createElement('style');
        container.innerHTML =
            '#world-generator { position: absolute; top: 10px; left: 10px; background-color: #ffffff; border: 2px solid #555; border-radius: 8px; padding: 15px; cursor: grab; z-index: 1000; font-family: sans-serif; box-shadow: 3px 3px 8px rgba(0,0,0,0.3); } ' +
            '#world-generator.dragging { cursor: grabbing; }' +
            '#world-generator h2 { margin-top: 0; margin-bottom: 15px; color: #333; text-align: center; border-bottom: 1px solid #eee; padding-bottom: 10px; } ' +
            '#world-generator .world-list { max-height: 200px; overflow-y: auto; margin BOTTOM: 15px; padding-right: 5px; }' +
            '#world-generator .world-item { margin: 5px 0; padding: 10px; background-color: #eef; border: 1px solid #ccf; border-radius: 5px; cursor: pointer; transition: background-color 0.2s ease, transform 0.1s ease; color: #333; } ' +
            '#world-generator .world-item:hover { background-color: #ddf; transform: translateY(-1px); } ' +
            '#world-generator .controls { margin-bottom: 15px; padding: 10px; background-color: #f9f9f9; border: 1px solid #eee; border-radius: 5px; } ' +
            '#world-generator .control-group { display: grid; grid-template-columns: repeat(auto-fit, minmax(100px, 1fr)); gap: 10px; margin-bottom: 10px; }' +
            '#world-generator .control-item label { display: block; margin-bottom: 3px; font-size: 0.9em; color: #555; } ' +
            '#world-generator .control-item input[type="number"] { width: 100%; padding: 5px; border: 1px solid #ccc; border-radius: 3px; box-sizing: border-box; text-align: center; -webkit-appearance: none; -moz-appearance: textfield; } ' +
            '#world-generator .control-item input[type="number"]::-webkit-outer-spin-button, #world-generator .control-item input[type="number"]::-webkit-inner-spin-button { -webkit-appearance: none; margin: 0; } ' +
            '#start-button, #stop-button { width: 100%; padding: 10px; border: none; border-radius: 5px; cursor: pointer; font-size: 1em; transition: background-color 0.2s ease, transform 0.1s ease; color: white; text-align: center; } ' +
            '#start-button { background-color: #5cb85c; } ' +
            '#start-button:hover { background-color: #4cae4c; transform: translateY(-1px); } ' +
            '#stop-button { background-color: #d9534f; margin-top: 10px; } ' +
            '#stop-button:hover { background-color: #c9302c; transform: translateY(-1px); } ' +
            '#status { margin-top: 10px; text-align: center; font-size: 0.9em; color: #555; min-height: 1.2em; }';
        document.head.appendChild(container);
    }

    // Add World Generator Menu
    function CreateWorldGenerator() {
        originalCanvas = document.getElementById('canvas');
        if (!originalCanvas) {
            console.warn("Drawaria canvas not found. World Generator will wait for it.");
            const observer = new MutationObserver((mutations, obs) => {
                originalCanvas = document.getElementById('canvas');
                if (originalCanvas) {
                    cw = originalCanvas.width;
                    ch = originalCanvas.height;
                    console.log(`Drawaria canvas found: ${cw}x${ch}`);
                    createUI();
                    obs.disconnect();
                }
            });
            observer.observe(document.body, { childList: true, subtree: true });
            return;
        } else {
            cw = originalCanvas.width;
            ch = originalCanvas.height;
            console.log(`Drawaria canvas found immediately: ${cw}x${ch}`);
            createUI();
        }
    }

    function createUI() {
        if (document.getElementById('world-generator')) {
            console.log("World Generator UI already exists.");
            return;
        }

        let worldGenerator = document.createElement('div');
        worldGenerator.id = 'world-generator';
        worldGenerator.innerHTML = `
            <h2>Drawaria World Generator</h2>
            <div class="world-list">
                <div class="world-item" data-world="Superflat">Superflat</div>
                <div class="world-item" data-world="Lava Cave">Lava Cave</div>
                <div class="world-item" data-world="Desert Vista">Desert Vista</div>
                <div class="world-item" data-world="Night Landscape">Night Landscape</div>
                <div class="world-item" data-world="House">House</div>
                <div class="world-item" data-world="Classroom">Classroom</div>
                <div class="world-item" data-world="Castle">Castle</div>
                <div class="world-item" data-world="School">School</div>
            </div>
            <div class="controls">
                <div class="control-group">
                    <div class="control-item">
                        <label for="draw-size">Step Size</label>
                        <input type="number" id="draw-size" value="4" min="1" step="1">
                    </div>
                    <div class="control-item">
                        <label for="draw-modifier">Step Modifier</label>
                        <input type="number" id="draw-modifier" value="3" min="1" step="1">
                    </div>
                    <div class="control-item">
                        <label for="draw-thickness">Thickness</label>
                        <input type="number" id="draw-thickness" value="50" min="1" step="1">
                    </div>
                    <div class="control-item">
                        <label for="draw-delay">Delay (ms)</label>
                        <input type="number" id="draw-delay" value="1" min="0" step="1"> <!-- Reduced to 1ms -->
                    </div>
                    <div class="control-item">
                        <label for="speed-factor">Speed Factor (1-10)</label>
                        <input type="number" id="speed-factor" value="2" min="1" max="10" step="1"> <!-- New control -->
                    </div>
                </div>
                <div class="control-item">
                    <label for="ignore-colors">Ignore Colors (hex/rgb, comma-separated)</label>
                    <input type="text" id="ignore-colors" value="">
                </div>
            </div>
            <button id="start-button">Load & Generate</button>
            <button id="stop-button" class="hidden">Stop Drawing</button>
            <div id="status">Select a world to load.</div>
        `;
        document.body.appendChild(worldGenerator);

        const startButton = document.getElementById('start-button');
        const stopButton = document.getElementById('stop-button');
        const statusDiv = document.getElementById('status');

        // Make the menu draggable
        let pos1 = 0, pos2 = 0, pos3 = 0, pos4 = 0;
        worldGenerator.onmousedown = dragMouseDown;

        function dragMouseDown(e) {
            e = e || window.event;
            if (e.target.tagName === 'INPUT' || e.target.tagName === 'BUTTON' || e.target.classList.contains('world-item') || e.target.closest('.controls')) {
                return;
            }
            e.preventDefault();
            worldGenerator.classList.add('dragging');
            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;
            worldGenerator.style.top = (worldGenerator.offsetTop - pos2) + "px";
            worldGenerator.style.left = (worldGenerator.offsetLeft - pos1) + "px";
        }

        function closeDragElement() {
            worldGenerator.classList.remove('dragging');
            document.onmouseup = null;
            document.onmousemove = null;
        }

        // Add event listeners to world items
        let worldItems = worldGenerator.querySelectorAll('.world-item');
        worldItems.forEach(item => {
            item.addEventListener('click', async () => {
                if (drawing_active) {
                    alert("Please stop the current drawing first.");
                    return;
                }
                const worldName = item.getAttribute('data-world');
                const worldUrl = WORLDS[worldName];
                if (worldUrl) {
                    statusDiv.textContent = `Loading "${worldName}"...`;
                    startButton.disabled = true;
                    stopButton.classList.add('hidden');
                    drawing_active = false;

                    try {
                        await loadImage(worldUrl);
                        statusDiv.textContent = `"${worldName}" loaded. Ready to draw (${executionLine.length} commands).`;
                        startButton.textContent = "Start Drawing";
                        startButton.disabled = false;
                    } catch (error) {
                        console.error("Error loading image:", error);
                        statusDiv.textContent = `Error loading "${worldName}". See console.`;
                        startButton.textContent = "Load & Generate";
                        startButton.disabled = false;
                    }
                } else {
                    statusDiv.textContent = `Error: World "${worldName}" not found.`;
                }
            });
        });

        // Start button
        startButton.addEventListener('click', async () => {
            if (executionLine.length === 0) {
                statusDiv.textContent = 'Please load an image first.';
                return;
            }
            if (window['___BOT'] && window['___BOT'].conn && window['___BOT'].conn.socket && window['___BOT'].conn.socket.readyState === WebSocket.OPEN) {
                drawing_active = true;
                startButton.classList.add('hidden');
                stopButton.classList.remove('hidden');
                statusDiv.textContent = `Drawing... (0/${executionLine.length})`;
                await execute(window['___BOT'].conn.socket);
                drawing_active = false;
                startButton.classList.remove('hidden');
                stopButton.classList.add('hidden');
                startButton.textContent = "Load & Generate";
                statusDiv.textContent = `Drawing finished (${executionLine.length}/${executionLine.length}).`;
            } else {
                statusDiv.textContent = 'Error: Bot socket not available or not open.';
                console.error("Bot socket not available or not open.", window['___BOT']);
            }
        });

        // Stop button
        stopButton.addEventListener('click', () => {
            drawing_active = false;
            stopButton.disabled = true;
            statusDiv.textContent = 'Stopping drawing...';
        });

        // Initial status
        statusDiv.textContent = originalCanvas ? 'Select a world to load.' : 'Waiting for game canvas...';
        if (!originalCanvas) startButton.disabled = true;
    }

    const WORLDS = {
        "Superflat": "https://static.vecteezy.com/system/resources/thumbnails/027/879/859/small/side-view-land-and-cloud-in-pixel-art-style-vector.jpg",
        "Lava Cave": "https://pics.craiyon.com/2023-09-23/40aa7363eec448ea836d2fb2cd4cce6a.webp",
        "Desert Vista": "https://i.ibb.co/pv9sVTtP/9e14aa6d-2348-4654-b235-c41ebbe678f2.png",
        "Night Landscape": "https://images.stockcake.com/public/2/b/c/2bc47920-6b02-49b8-aac8-81d965e2ab70_large/moonlit-pixel-city-stockcake.jpg",
        "House": "https://static.vecteezy.com/system/resources/previews/010/963/629/non_2x/house-pixel-icon-free-vector.jpg",
        "Classroom": "https://i.ibb.co/hFHRGT3j/download.jpg",
        "Castle": "https://i.ibb.co/8gRg8rtZ/cwtdyva7.png",
        "School": "https://static.vecteezy.com/system/resources/previews/045/712/354/non_2x/school-building-in-pixel-art-style-vector.jpg"
    };

    // Convert hex to RGB
    function hexToRgb(hex) {
        const bigint = parseInt(hex.replace('#', ''), 16);
        const r = (bigint >> 16) & 255;
        const g = (bigint >> 8) & 255;
        const b = bigint & 255;
        return `rgb(${r},${g},${b})`;
    }

    // Parse ignore colors input
    function parseIgnoreColors(input) {
        const colors = input.split(',').map(c => c.trim()).filter(c => c);
        const rgbColors = [];
        colors.forEach(color => {
            if (color.startsWith('#')) {
                try {
                    rgbColors.push(hexToRgb(color));
                } catch (e) {
                    console.warn(`Invalid hex color: ${color}`);
                }
            } else if (color.startsWith('rgb(') && color.endsWith(')')) {
                const rgbValues = color.substring(4, color.length - 1).split(',').map(Number);
                if (rgbValues.length === 3 && rgbValues.every(v => v >= 0 && v <= 255)) {
                    rgbColors.push(color);
                } else {
                    console.warn(`Invalid rgb color format or values: ${color}`);
                }
            } else {
                console.warn(`Unrecognized color format: ${color}`);
            }
        });
        return rgbColors;
    }

    // Load and process image
    async function loadImage(url) {
        return new Promise((resolve, reject) => {
            if (!originalCanvas) {
                reject("Drawaria canvas not found.");
                return;
            }

            const img = new Image();
            img.crossOrigin = 'Anonymous';

            img.addEventListener('load', () => {
                previewCanvas.width = cw;
                previewCanvas.height = ch;
                const ctx = previewCanvas.getContext('2d');

                // Scale and center image
                const scaleW = cw / img.width;
                const scaleH = ch / img.height;
                const scale = Math.max(scaleW, scaleH);
                const scaledWidth = img.width * scale;
                const scaledHeight = img.height * scale;
                const offsetX = (cw - scaledWidth) / 2;
                const offsetY = (ch - scaledHeight) / 2;

                ctx.clearRect(0, 0, cw, ch);
                ctx.fillStyle = '#FFFFFF';
                ctx.fillRect(0, 0, cw, ch);
                ctx.drawImage(img, 0, 0, img.width, img.height, offsetX, offsetY, scaledWidth, scaledHeight);

                const imgData = ctx.getImageData(0, 0, cw, ch);
                data = imgData.data;
                ctx.clearRect(0, 0, cw, ch);

                // Generate commands with spiral pattern
                const size = parseInt(document.getElementById('draw-size').value, 10) || 4;
                const modifier = parseInt(document.getElementById('draw-modifier').value, 10) || 4;
                const thickness = parseInt(document.getElementById('draw-thickness').value, 10) || 27;
                const speedFactor = parseInt(document.getElementById('speed-factor').value, 10) || 2;
                const ignoreInput = document.getElementById('ignore-colors').value;
                const ignoreColors = parseIgnoreColors(ignoreInput);

                generateDrawingCommands(size, modifier, thickness, speedFactor, ignoreColors);

                console.log('Image loaded and commands generated.');
                resolve();
            });

            img.addEventListener('error', (e) => {
                console.error("Failed to load image:", url, e);
                reject(`Failed to load image: ${url}`);
            });

            img.src = url;
        });
    }

// Generate drawing commands using rectangular spiral pattern
function generateDrawingCommands(size, modifier, thickness, speedFactor, ignoreColors) {
    executionLine = [];
    const step = Math.max(1, size * modifier * speedFactor); // Scale step with speedFactor
    if (step <= 0) {
        console.error("Invalid step size:", step);
        alert("Error generating commands: Invalid step size.");
        return;
    }

    // Spiral traversal: outer to inner rectangular spiral
    let left = 0, right = cw - 1, top = 0, bottom = ch - 1;
    let currentSegmentStart = null; // Start pixel coordinate [x, y]
    let currentSegmentColor = null; // Current segment color

    // Create reference objects to pass to processPixel
    const segmentRefs = {
        currentSegmentStart: { value: null },
        currentSegmentColor: { value: null }
    };

    while (left <= right && top <= bottom) {
        // Traverse top row (left to right)
        for (let x = left; x <= right; x += step) {
            processPixel(x, top, step, thickness, ignoreColors, segmentRefs.currentSegmentStart, segmentRefs.currentSegmentColor);
        }
        top += step;

        // Traverse right column (top to bottom)
        for (let y = top; y <= bottom && left <= right; y += step) {
            processPixel(right, y, step, thickness, ignoreColors, segmentRefs.currentSegmentStart, segmentRefs.currentSegmentColor);
        }
        right -= step;

        // Traverse bottom row (right to left)
        for (let x = right; x >= left && top <= bottom; x -= step) {
            processPixel(x, bottom, step, thickness, ignoreColors, segmentRefs.currentSegmentStart, segmentRefs.currentSegmentColor);
        }
        bottom -= step;

        // Traverse left column (bottom to top)
        for (let y = bottom; y >= top && left <= right; y -= step) {
            processPixel(left, y, step, thickness, ignoreColors, segmentRefs.currentSegmentStart, segmentRefs.currentSegmentColor);
        }
        left += step;
    }

    // End any active segment
    if (segmentRefs.currentSegmentStart.value !== null) {
        executionLine.push({
            pos1: recalc(segmentRefs.currentSegmentStart.value),
            pos2: recalc(segmentRefs.currentSegmentStart.value), // End at same point for single pixel
            color: segmentRefs.currentSegmentColor.value,
            thickness: thickness,
        });
    }

    console.log(`Generated ${executionLine.length} drawing commands.`);
}

// Helper to process a pixel and manage segments
function processPixel(x, y, step, thickness, ignoreColors, currentSegmentStartRef, currentSegmentColorRef) {
    if (x < 0 || x >= cw || y < 0 || y >= ch) return;

    const pixelIndex = (y * cw + x) * 4;
    const alpha = data[pixelIndex + 3];

    if (alpha > 20) {
        const r = data[pixelIndex + 0];
        const g = data[pixelIndex + 1];
        const b = data[pixelIndex + 2];
        const color = `rgb(${r},${g},${b})`;

        if (!ignoreColors.includes(color)) {
            if (currentSegmentStartRef.value === null) {
                currentSegmentStartRef.value = [x, y];
                currentSegmentColorRef.value = color;
            } else if (color !== currentSegmentColorRef.value) {
                executionLine.push({
                    pos1: recalc(currentSegmentStartRef.value),
                    pos2: recalc([x, y]),
                    color: currentSegmentColorRef.value,
                    thickness: thickness,
                });
                currentSegmentStartRef.value = [x, y];
                currentSegmentColorRef.value = color;
            }
            // Continue segment if color is the same
        } else if (currentSegmentStartRef.value !== null) {
            executionLine.push({
                pos1: recalc(currentSegmentStartRef.value),
                pos2: recalc([x, y]),
                color: currentSegmentColorRef.value,
                thickness: thickness,
            });
            currentSegmentStartRef.value = null;
            currentSegmentColorRef.value = null;
        }
    } else if (currentSegmentStartRef.value !== null) {
        executionLine.push({
            pos1: recalc(currentSegmentStartRef.value),
            pos2: recalc([x, y]),
            color: currentSegmentColorRef.value,
            thickness: thickness,
        });
        currentSegmentStartRef.value = null;
        currentSegmentColorRef.value = null;
    }
}

    // Helper to process a pixel and manage segments
    function processPixel(x, y, step, thickness, ignoreColors, currentSegmentStartRef, currentSegmentColorRef) {
        if (x < 0 || x >= cw || y < 0 || y >= ch) return;

        const pixelIndex = (y * cw + x) * 4;
        const alpha = data[pixelIndex + 3];

        if (alpha > 20) {
            const r = data[pixelIndex + 0];
            const g = data[pixelIndex + 1];
            const b = data[pixelIndex + 2];
            const color = `rgb(${r},${g},${b})`;

            if (!ignoreColors.includes(color)) {
                if (currentSegmentStartRef.value === null) {
                    currentSegmentStartRef.value = [x, y];
                    currentSegmentColorRef.value = color;
                } else if (color !== currentSegmentColorRef.value) {
                    executionLine.push({
                        pos1: recalc(currentSegmentStartRef.value),
                        pos2: recalc([x, y]),
                        color: currentSegmentColorRef.value,
                        thickness: thickness,
                    });
                    currentSegmentStartRef.value = [x, y];
                    currentSegmentColorRef.value = color;
                }
            } else if (currentSegmentStartRef.value !== null) {
                executionLine.push({
                    pos1: recalc(currentSegmentStartRef.value),
                    pos2: recalc([x, y]),
                    color: currentSegmentColorRef.value,
                    thickness: thickness,
                });
                currentSegmentStartRef.value = null;
                currentSegmentColorRef.value = null;
            }
        } else if (currentSegmentStartRef.value !== null) {
            executionLine.push({
                pos1: recalc(currentSegmentStartRef.value),
                pos2: recalc([x, y]),
                color: currentSegmentColorRef.value,
                thickness: thickness,
            });
            currentSegmentStartRef.value = null;
            currentSegmentColorRef.value = null;
        }
    }

    // Execute drawing commands with optimized timing
    async function execute(socket) {
        const delayMs = parseInt(document.getElementById('draw-delay').value, 10) || 1;
        const statusDiv = document.getElementById('status');
        const stopButton = document.getElementById('stop-button');
        stopButton.disabled = false;
        const maxCommandsPerSecond = 500; // Prevent server overload
        const batchSize = Math.floor(1000 / maxCommandsPerSecond / delayMs) || 1;

        for (let i = 0; i < executionLine.length; i += batchSize) {
            if (!drawing_active) {
                console.log("Drawing stopped by user.");
                statusDiv.textContent = `Drawing stopped (${i}/${executionLine.length}).`;
                break;
            }

            // Send batch of commands
            for (let j = 0; j < batchSize && i + j < executionLine.length; j++) {
                let currentLine = executionLine[i + j];
                let p1 = currentLine.pos1;
                let p2 = currentLine.pos2;
                let color = currentLine.color;
                let thickness = currentLine.thickness;

                if (p1 && p2 && typeof p1[0] === 'number' && typeof p1[1] === 'number' && typeof p2[0] === 'number' && typeof p2[1] === 'number' && !isNaN(p1[0]) && !isNaN(p1[1]) && !isNaN(p2[0]) && !isNaN(p2[1])) {
                    drawcmd(socket, p1, p2, color, thickness);
                } else {
                    console.warn("Skipping invalid command:", currentLine);
                }
            }

            // Update status every 50 commands or at end
            if ((i + batchSize) % 50 === 0 || i + batchSize >= executionLine.length) {
                statusDiv.textContent = `Drawing... (${Math.min(i + batchSize, executionLine.length)}/${executionLine.length})`;
            }

            // Check WebSocket buffer to prevent overload
            if (socket.bufferedAmount > 100000) {
                console.warn("WebSocket buffer high, pausing...");
                await delay(100);
            }

            await delay(delayMs);
        }
        stopButton.disabled = true;
    }

    // Send drawing command
    function drawcmd(s, start, end, color, thickness) {
        const cmd = `42["drawcmd",0,[${start[0].toFixed(4)},${start[1].toFixed(4)},${end[0].toFixed(4)},${end[1].toFixed(4)},false,${0 - thickness},"${color}",0,0,{}]]`;
        s.send(cmd);
    }

    // Delay function
    function delay(ms) {
        return new Promise((resolve) => {
            if (ms > 0) {
                setTimeout(resolve, ms);
            } else {
                resolve();
            }
        });
    }

    // Recalculate coordinates to normalized
    function recalc(pixelCoords) {
        if (cw === 0 || ch === 0) {
            console.error("Canvas dimensions not set!", cw, ch);
            return [NaN, NaN];
        }
        return [
            pixelCoords[0] / cw,
            pixelCoords[1] / ch
        ];
    }

    // Helper from original bot
    var nullify = (value = null) => {
        return value == null ? null : String().concat('"', value, '"');
    };

    // Initialize script
    function init() {
        if (!document.getElementById('world-generator')) {
            addBoxIcons();
            CreateStylesheet();
            CreateWorldGenerator();

            if (!window['___BOT']) {
                window['___BOT'] = new Player('bot');
            }
            if (!window['___ENGINE']) {
                window['___ENGINE'] = { loadImage: loadImage, drawImage: generateDrawingCommands, execute: execute, recalc: recalc };
            }
        } else {
            console.log("World Generator script already running.");
        }
    }

    window.addEventListener('load', init);
    if (document.readyState === 'complete' || document.readyState === 'interactive') {
        init();
    } else {
        document.addEventListener('DOMContentLoaded', init);
    }

    // Player/Connection/Room/Actions classes
    const Player = function (name = undefined) {
        this.name = name;
        this.sid1 = null;
        this.uid = '';
        this.wt = '';
        this.conn = new Connection(this);
        this.room = new Room(this.conn);
        this.action = new Actions(this.conn);
    };

    Player.prototype.annonymize = function (name) {
        this.name = name;
        this.uid = undefined;
        this.wt = undefined;
    };

    const Connection = function (player) {
        this.player = player;
        this.socket = null;
    };

    Connection.prototype.onopen = function (event) {
        this.Heartbeat(25000);
    };

    Connection.prototype.onclose = function (event) {
    };

    Connection.prototype.onerror = function (event) {
        console.error("Socket error:", event);
    };

    Connection.prototype.onmessage = function (event) {
        let message = String(event.data);
        if (message.startsWith('42')) {
            this.onbroadcast(message.slice(2));
        } else if (message.startsWith('40')) {
            this.onrequest();
        } else if (message.startsWith('41')) {
        } else if (message.startsWith('430')) {
            let configs = JSON.parse(message.slice(3))[0];
            if (configs) {
                this.player.room.players = configs.players;
                this.player.room.id = configs.roomid;
                console.log(`Bot joined room ${this.player.room.id}`);
            }
        }
    };

    Connection.prototype.onbroadcast = function (payload) {
        try {
            payload = JSON.parse(payload);
            if (payload[0] == 'bc_uc_freedrawsession_changedroom') {
                if (payload.length > 3) this.player.room.players = payload[3];
                if (payload.length > 4) this.player.room.id = payload[4];
            }
            if (payload[0] == 'mc_roomplayerschange') {
                if (payload.length > 3) this.player.room.players = payload[3];
            }
        } catch(e) {
            console.error("Error parsing broadcast payload:", payload, e);
        }
    };

    Connection.prototype.onrequest = function () {
    };

    Connection.prototype.open = function (url) {
        if (this.socket && this.socket.readyState !== WebSocket.CLOSED) {
            console.log("Socket already open or connecting.");
            return;
        }
        this.socket = new WebSocket(url);
        this.socket.onopen = this.onopen.bind(this);
        this.socket.onclose = this.onclose.bind(this);
        this.socket.onerror = this.onerror.bind(this);
        this.socket.onmessage = this.onmessage.bind(this);
        console.log("Attempting to open WebSocket:", url);
    };

    Connection.prototype.close = function (code, reason) {
        if (this.socket) {
            console.log("Closing WebSocket:", code, reason);
            this.socket.close(code, reason);
            this.socket = null;
        }
    };

    Connection.prototype.Heartbeat = function (interval) {
        this.heartbeatTimeout = setTimeout(() => {
            if (this.socket && this.socket.readyState == this.socket.OPEN) {
                this.socket.send(2);
                this.Heartbeat(interval);
            } else {
                console.log("Heartbeat stopped, socket not open.");
            }
        }, interval);
    };

    Connection.prototype.stopHeartbeat = function() {
        if (this.heartbeatTimeout) {
            clearTimeout(this.heartbeatTimeout);
            this.heartbeatTimeout = null;
        }
    };

    Connection.prototype.serverconnect = function (server, connectstring) {
        this.stopHeartbeat();
        this.close();
        this.open(server);
        this.onrequest = () => {
            if (this.socket && this.socket.readyState === WebSocket.OPEN) {
                this.socket.send(connectstring);
                this.onrequest = function() {};
            } else {
                console.error("Socket not open when onrequest was called.");
            }
        };
    };

    const Room = function (conn) {
        this.conn = conn;
        this.id = null;
        this.players = [];
    };

    Room.prototype.join = function (invitelink) {
        let gamemode = 2;
        let server = '';
        let roomIdToSend = null;

        if (invitelink != null) {
            if (invitelink.startsWith('http')) {
                const urlParts = invitelink.split('/');
                this.id = urlParts.pop();
            } else {
                this.id = invitelink;
            }
            roomIdToSend = nullify(this.id);
            if (this.id && typeof this.id === 'string') {
                if (this.id.endsWith('.3')) {
                    server = 'sv3.';
                    gamemode = 2;
                } else if (this.id.endsWith('.2')) {
                    server = 'sv2.';
                    gamemode = 2;
                } else {
                    server = '';
                    gamemode = 2;
                }
            } else {
                console.warn("Could not parse room ID from invitelink:", invitelink);
                roomIdToSend = null;
                server = 'sv3.';
                gamemode = 2;
                this.id = null;
            }
} else {
    this.id = null;
    server = 'sv3.';
    gamemode = 2;
    roomIdToSend = null;
}

let serverurl = `wss://${server}drawaria.online/socket.io/?sid1=undefined&hostname=drawaria.online&EIO=3&transport=websocket`;
let player = this.conn.player;
let connectstring = `420["startplay",${nullify(player.name)},${gamemode},"en",${roomIdToSend},null,[${nullify(player.sid1)},${nullify(player.uid)},${nullify(player.wt)}],null]]`;

console.log("Attempting to connect to server:", serverurl, "with command:", connectstring);
this.conn.serverconnect(serverurl, connectstring);
};

Room.prototype.next = function () {
    if (this.conn.socket && this.conn.socket.readyState === this.conn.socket.OPEN) {
        this.conn.socket.send('42["pgswtichroom"]');
    } else {
        console.warn("Socket not open, cannot switch room.");
        this.join(null);
    }
};

const Actions = function (conn) {
    this.conn = conn;
};

Actions.prototype.DrawLine = function (bx = 50, by = 50, ex = 50, ey = 50, thickness = 50, color = '#FFFFFF', algo = 0) {
    bx = bx / 100;
    by = by / 100;
    ex = ex / 100;
    ey = ey / 100;
    if (this.conn.socket && this.conn.socket.readyState === this.conn.socket.OPEN) {
        this.conn.socket.send(`42["drawcmd",0,[${bx},${by},${ex},${ey},true,${0 - thickness},"${color}",0,0,{"2":${algo},"3":0.5,"4":0.5}]]`);
        this.conn.socket.send(`42["drawcmd",0,[${bx},${by},${ex},${ey},false,${0 - thickness},"${color}",0,0,{"2":${algo},"3":0.5,"4":0.5}]]`);
    } else {
        console.warn("Socket not open, cannot draw line.");
    }
};

})();