Kawaii Helper & Drawing Bot for Gartic.io

Helper for Gartic.io with auto-guess, drawing assistance, and drawing bot

目前為 2025-03-12 提交的版本,檢視 最新版本

// ==UserScript==
// @name         Kawaii Helper & Drawing Bot for Gartic.io
// @namespace    http://tampermonkey.net/
// @version      2025-03-12
// @description  Helper for Gartic.io with auto-guess, drawing assistance, and drawing bot
// @author       anonimbiri & Gartic-Developers
// @license      MIT
// @match        https://gartic.io/*
// @icon         https://www.google.com/s2/favicons?sz=64&domain=gartic.io
// @run-at       document-start
// @grant        none
// ==/UserScript==

// I used the word list from 'https://github.com/Gartic-Developers/Gartic-WordList/'.
// Thanks to Gartic Developers for providing this resource. Also, thanks to Qwyua!

(function() {
    'use strict';

    // Script interception (unchanged)
    Node.prototype.appendChild = new Proxy(Node.prototype.appendChild, {
        apply: function(target, thisArg, argumentsList) {
            const node = argumentsList[0];
            if (node.nodeName.toLowerCase() === 'script' && node.src && node.src.includes('room')) {
                console.log('Target script detected:', node.src);
                fetch(node.src)
                    .then(response => response.text())
                    .then(scriptContent => {
                    let modifiedContent = scriptContent
                    .replace(
                        'r.created||c?Rt("input",{type:"text",name:"chat",className:"mousetrap",autoComplete:"off",autoCorrect:"off",autoCapitalize:"off",value:i,placeholder:this._lang.chatHere,maxLength:100,enterKeyHint:"send",onChange:this.handleText,ref:this._ref}):Rt("input",{type:"text",name:"chat",className:"mousetrap",autoComplete:"off",autoCorrect:"off",autoCapitalize:"off",value:this._lang.loginChat,maxLength:100,ref:this._ref,disabled:!0})',
                        'Rt("input",{type:"text",name:"chat",className:"mousetrap",autoComplete:"off",autoCorrect:"off",autoCapitalize:"off",value:i,placeholder:this._lang.chatHere,maxLength:100,enterKeyHint:"send",onChange:this.handleText,ref:this._ref})'
                    )
                    .replace(
                        'this._timerAtivo=setInterval((function(){Date.now()-e._ativo>15e4&&(O(Object(f.a)(n.prototype),"emit",e).call(e,"avisoInativo"),e._ativo=Date.now())}),1e3)',
                        'this._timerAtivo=setInterval((function(){Date.now()-e._ativo>15e4&&e.active()}),1e3)'
                    )
                    .replace(
                        'e.unlock()}',
                        'e.unlock();window.game=e;setInterval(()=>{window.game=e},1000);e.on("votekick",(t,i,o)=>{if(i.id===e.me.id){e.votekick(t.id,true);}});}'
                    );
                    let blob = new Blob([modifiedContent], { type: 'application/javascript' });
                    let blobUrl = URL.createObjectURL(blob);
                    node.src = blobUrl;
                    node.textContent = '';
                    return target.apply(thisArg, [node]);
                })
                    .catch(error => console.error('Failed to fetch/modify script:', error));
                return node;
            }
            return target.apply(thisArg, argumentsList);
        }
    });

    // Load fonts
    const fontLink = document.createElement('link');
    fontLink.rel = 'stylesheet';
    fontLink.href = 'https://fonts.googleapis.com/css2?family=M+PLUS+Rounded+1c:wght@400;700&display=swap';
    document.head.appendChild(fontLink);

    // Inject HTML
    const kawaiiHTML = `
        <div class="kawaii-cheat" id="kawaiiCheat">
            <div class="kawaii-header" id="kawaiiHeader">
                <img src="https://i.imgur.com/ptRhAHj.png" alt="Anime Girl" class="header-icon">
                <h2>✧ Kawaii Helper ✧</h2>
                <button class="minimize-btn" id="minimizeBtn">▼</button>
            </div>
            <div class="kawaii-body" id="kawaiiBody">
                <div class="kawaii-tabs">
                    <button class="kawaii-tab active" data-tab="guessing">Guessing</button>
                    <button class="kawaii-tab" data-tab="drawing">Drawing</button>
                </div>
                <div class="kawaii-content" id="guessing-tab">
                    <div class="checkbox-container">
                        <input type="checkbox" id="autoGuess">
                        <label for="autoGuess">Auto Guess</label>
                    </div>
                    <div class="slider-container" id="speedContainer" style="display: none;">
                        <div class="slider-label">Speed</div>
                        <div class="custom-slider">
                            <input type="range" id="guessSpeed" min="100" max="5000" value="1000" step="100">
                            <div class="slider-track"></div>
                            <span id="speedValue">1s</span>
                        </div>
                    </div>
                    <div class="checkbox-container">
                        <input type="checkbox" id="customWords">
                        <label for="customWords">Custom Words</label>
                    </div>
                    <div class="dropzone-container" id="wordListContainer" style="display: none;">
                        <div class="dropzone" id="wordListDropzone">
                            <input type="file" id="wordList" accept=".txt">
                            <div class="dropzone-content">
                                <div class="dropzone-icon">❀</div>
                                <p>Drop word list here or click to upload</p>
                            </div>
                        </div>
                    </div>
                    <div class="input-container">
                        <input type="text" id="guessPattern" placeholder="Enter pattern (e.g., ___e___)">
                    </div>
                    <div class="hit-list" id="hitList">
                        <div class="message">Type a pattern to see matches ✧</div>
                    </div>
                </div>
                <div class="kawaii-content" id="drawing-tab" style="display: none;">
                    <div class="dropzone-container">
                        <div class="dropzone" id="imageDropzone">
                            <input type="file" id="imageUpload" accept="image/*">
                            <div class="dropzone-content">
                                <div class="dropzone-icon">✎</div>
                                <p>Drop image here or click to upload</p>
                            </div>
                        </div>
                        <div class="image-preview" id="imagePreview" style="display: none;">
                            <img id="previewImg">
                            <div class="preview-controls">
                                <button class="cancel-btn" id="cancelImage">✕</button>
                            </div>
                        </div>
                    </div>
                    <div class="slider-container">
                        <div class="slider-label">Draw Speed</div>
                        <div class="custom-slider">
                            <input type="range" id="drawSpeed" min="20" max="5000" value="300" step="100">
                            <div class="slider-track"></div>
                            <span id="drawSpeedValue">300ms</span>
                        </div>
                    </div>
                    <div class="slider-container">
                        <div class="slider-label">Max Colors</div>
                        <div class="custom-slider">
                            <input type="range" id="maxColors" min="3" max="100" value="20" step="1">
                            <div class="slider-track"></div>
                            <span id="maxColorsValue">20</span>
                        </div>
                    </div>
                    <button class="draw-btn" id="sendDraw" disabled>Draw Now ✧</button>
                </div>
                <div class="kawaii-footer">
                    <span class="credit-text">Made with ♥ by Anonimbiri & Gartic-Developers</span>
                </div>
            </div>
        </div>
    `;

    const waitForBody = setInterval(() => {
        if (document.body) {
            clearInterval(waitForBody);
            document.body.insertAdjacentHTML('beforeend', kawaiiHTML);
            addStylesAndBehavior();
        }
    }, 100);

    function addStylesAndBehavior() {
        const style = document.createElement('style');
        style.textContent = `
            :root {
                --primary-color: #FF69B4;
                --primary-dark: #FF1493;
                --primary-light: #FFC0CB;
                --bg-color: #FFB6C1;
                --text-color: #5d004f;
                --panel-bg: rgba(255, 182, 193, 0.95);
                --panel-border: #FF69B4;
                --element-bg: rgba(255, 240, 245, 0.7);
                --element-hover: rgba(255, 240, 245, 0.9);
                --element-active: #FF69B4;
                --element-active-text: #FFF0F5;
            }

            .kawaii-cheat {
                position: fixed;
                top: 20px;
                right: 20px;
                width: 280px;
                background: var(--panel-bg);
                border-radius: 15px;
                box-shadow: 0 4px 16px rgba(0, 0, 0, 0.2);
                padding: 10px;
                display: flex;
                flex-direction: column;
                gap: 10px;
                color: var(--text-color);
                user-select: none;
                z-index: 1000;
                font-family: 'M PLUS Rounded 1c', sans-serif;
                border: 2px solid var(--panel-border);
                transition: all 0.4s ease-in-out;
                max-height: calc(100vh - 40px);
                overflow: hidden;
            }

            .kawaii-cheat.minimized {
                height: 50px;
                opacity: 0.9;
                transform: scale(0.95);
                overflow: hidden;
            }

            .kawaii-cheat:not(.minimized) {
                opacity: 1;
                transform: scale(1);
            }

            .kawaii-cheat.minimized .kawaii-body {
                opacity: 0;
                max-height: 0;
                overflow: hidden;
                transition: opacity 0.2s ease-in-out, max-height 0.4s ease-in-out;
            }

            .kawaii-cheat:not(.minimized) .kawaii-body {
                opacity: 1;
                max-height: 500px;
                transition: opacity 0.2s ease-in-out 0.2s, max-height 0.4s ease-in-out;
            }

            .kawaii-cheat.dragging {
                opacity: 0.8;
                transition: none; /* Sürükleme sırasında animasyonu devre dışı bırak */
            }

            .kawaii-header {
                display: flex;
                justify-content: space-between;
                align-items: center;
                padding: 5px 10px;
                cursor: move;
                background: var(--element-bg);
                border-radius: 10px;
                border: 2px solid var(--primary-color);
            }

            .header-icon {
                width: 30px;
                height: 30px;
                border-radius: 50%;
                margin-right: 10px;
                border: 1px dashed var(--primary-color);
            }

            .kawaii-header h2 {
                margin: 0;
                font-size: 18px;
                font-weight: 700;
                color: var(--primary-dark);
                text-shadow: 1px 1px 2px var(--primary-light);
            }

            .minimize-btn {
                background: transparent;
                border: 2px solid var(--primary-dark);
                border-radius: 6px;
                width: 24px;
                height: 24px;
                color: var(--primary-dark);
                font-size: 16px;
                line-height: 20px;
                text-align: center;
                cursor: pointer;
                transition: all 0.3s ease;
            }

            .minimize-btn:hover {
                background: var(--primary-color);
                color: var(--element-active-text);
                border-color: var(--primary-color);
                transform: rotate(180deg);
            }

            .kawaii-tabs {
                display: flex;
                gap: 8px;
                padding: 5px 0;
            }

            .kawaii-tab {
                flex: 1;
                background: var(--element-bg);
                border: 1px dashed var(--primary-color);
                padding: 6px;
                border-radius: 10px;
                font-size: 12px;
                font-weight: 700;
                color: var(--text-color);
                cursor: pointer;
                transition: background 0.3s ease, transform 0.3s ease;
                text-align: center;
            }

            .kawaii-tab.active {
                background: var(--primary-color);
                color: var(--element-active-text);
                border-color: var(--primary-dark);
            }

            .kawaii-tab:hover:not(.active) {
                background: var(--element-hover);
                transform: scale(1.05);
            }

            .kawaii-content {
                display: flex;
                flex-direction: column;
                gap: 10px;
                max-height: 55vh;
                overflow-y: auto;
                padding: 5px;
            }

            .checkbox-container {
                display: flex;
                align-items: center;
                gap: 8px;
                background: var(--element-bg);
                padding: 8px;
                border-radius: 10px;
                border: 1px dashed var(--primary-color);
                cursor: pointer;
                transition: background 0.3s ease;
            }

            .checkbox-container:hover {
                background: var(--element-hover);
            }

            .checkbox-container input[type="checkbox"] {
                appearance: none;
                width: 18px;
                height: 18px;
                background: var(--element-active-text);
                border: 1px dashed var(--primary-color);
                border-radius: 50%;
                cursor: pointer;
                position: relative;
            }

            .checkbox-container input[type="checkbox"]:checked {
                background: var(--primary-color);
                border-color: var(--primary-dark);
            }

            .checkbox-container input[type="checkbox"]:checked::after {
                content: "♥";
                position: absolute;
                top: 50%;
                left: 50%;
                transform: translate(-50%, -50%);
                color: var(--element-active-text);
                font-size: 12px;
            }

            .checkbox-container label {
                font-size: 12px;
                font-weight: 700;
                color: var(--text-color);
                cursor: pointer;
            }

            .input-container {
                background: var(--element-bg);
                padding: 8px;
                border-radius: 10px;
                border: 1px dashed var(--primary-color);
            }

            .input-container input[type="text"] {
                width: 100%;
                background: var(--element-active-text);
                border: 1px dashed var(--primary-light);
                border-radius: 8px;
                padding: 6px 10px;
                color: var(--text-color);
                font-size: 12px;
                font-weight: 500;
                box-sizing: border-box;
                transition: border-color 0.3s ease;
                outline: none;
            }

            .input-container input[type="text"]:focus {
                border-color: var(--primary-dark);
            }

            .dropzone-container {
                display: flex;
                flex-direction: column;
                gap: 10px;
            }

            .dropzone {
                position: relative;
                background: var(--element-bg);
                border: 1px dashed var(--primary-color);
                border-radius: 10px;
                padding: 15px;
                display: flex;
                flex-direction: column;
                align-items: center;
                justify-content: center;
                cursor: pointer;
                transition: background 0.3s ease, border-color 0.3s ease;
                min-height: 80px;
            }

            .dropzone:hover, .dropzone.drag-over {
                background: var(--element-hover);
                border-color: var(--primary-dark);
            }

            .dropzone input[type="file"] {
                position: absolute;
                top: 0;
                left: 0;
                width: 100%;
                height: 100%;
                opacity: 0;
                cursor: pointer;
            }

            .dropzone-content {
                display: flex;
                flex-direction: column;
                align-items: center;
                gap: 8px;
                text-align: center;
                pointer-events: none;
            }

            .dropzone-icon {
                font-size: 24px;
                color: var(--primary-color);
                animation: pulse 1.5s infinite ease-in-out;
            }

            @keyframes pulse {
                0%, 100% { transform: scale(1); }
                50% { transform: scale(1.1); }
            }

            .dropzone-content p {
                margin: 0;
                color: var(--text-color);
                font-size: 12px;
                font-weight: 500;
            }

            .slider-container {
                display: flex;
                flex-direction: column;
                gap: 6px;
                background: var(--element-bg);
                padding: 8px;
                border-radius: 10px;
                border: 1px dashed var(--primary-color);
            }

            .slider-label {
                font-size: 12px;
                color: var(--text-color);
                font-weight: 700;
                text-align: center;
            }

            .custom-slider {
                position: relative;
                height: 25px;
                padding: 0 8px;
            }

            .custom-slider input[type="range"] {
                -webkit-appearance: none;
                width: 100%;
                height: 6px;
                background: transparent;
                position: absolute;
                top: 50%;
                left: 0;
                transform: translateY(-50%);
                z-index: 2;
            }

            .custom-slider .slider-track {
                position: absolute;
                top: 50%;
                left: 0;
                width: 100%;
                height: 6px;
                background: linear-gradient(to right, var(--primary-dark) 0%, var(--primary-dark) var(--slider-progress), var(--primary-light) var(--slider-progress), var(--primary-light) 100%);
                border-radius: 3px;
                transform: translateY(-50%);
                z-index: 1;
            }

            .custom-slider input[type="range"]::-webkit-slider-thumb {
                -webkit-appearance: none;
                width: 16px;
                height: 16px;
                background: var(--primary-color);
                border-radius: 50%;
                border: 1px dashed var(--element-active-text);
                cursor: pointer;
                transition: transform 0.3s ease;
            }

            .custom-slider input[type="range"]::-webkit-slider-thumb:hover {
                transform: scale(1.2);
            }

            .custom-slider span {
                position: absolute;
                bottom: -15px;
                left: 50%;
                transform: translateX(-50%);
                font-size: 10px;
                color: var(--text-color);
                background: var(--element-active-text);
                padding: 2px 6px;
                border-radius: 8px;
                border: 1px dashed var(--primary-color);
                white-space: nowrap;
            }

            .hit-list {
                max-height: 180px;
                overflow-y: scroll;
                background: var(--element-bg);
                border: 1px dashed var(--primary-color);
                border-radius: 10px;
                padding: 8px;
                display: flex;
                flex-direction: column;
                gap: 6px;
                scrollbar-width: thin;
                scrollbar-color: var(--primary-color) var(--element-bg);
            }

            .hit-list::-webkit-scrollbar {
                width: 6px;
            }

            .hit-list::-webkit-scrollbar-thumb {
                background-color: var(--primary-color);
                border-radius: 10px;
            }

            .hit-list::-webkit-scrollbar-track {
                background: var(--element-bg);
            }

            .hit-list button {
                background: rgba(255, 240, 245, 0.8);
                border: 1px dashed var(--primary-color);
                padding: 6px 10px;
                border-radius: 8px;
                color: var(--text-color);
                font-size: 12px;
                font-weight: 700;
                cursor: pointer;
                transition: background 0.3s ease, transform 0.3s ease;
                text-align: left;
            }

            .hit-list button:hover:not(.tried) {
                background: var(--primary-color);
                color: var(--element-active-text);
                transform: scale(1.03);
            }

            .hit-list button.tried {
                background: rgba(255, 182, 193, 0.6);
                border-color: var(--primary-light);
                color: var(--primary-dark);
                opacity: 0.7;
                cursor: not-allowed;
            }

            .hit-list .tried-label {
                font-size: 10px;
                color: var(--primary-dark);
                text-align: center;
                padding: 4px;
                background: var(--element-active-text);
                border-radius: 8px;
                border: 1px dashed var(--primary-color);
            }

            .hit-list .message {
                font-size: 12px;
                color: var(--text-color);
                text-align: center;
                padding: 8px;
            }

            .image-preview {
                position: relative;
                margin-top: 10px;
                background: var(--element-bg);
                padding: 8px;
                border-radius: 10px;
                border: 1px dashed var(--primary-color);
            }

            .image-preview img {
                max-width: 100%;
                max-height: 120px;
                border-radius: 8px;
                display: block;
                margin: 0 auto;
            }

            .preview-controls {
                position: absolute;
                top: 12px;
                right: 12px;
                display: flex;
                gap: 6px;
            }

            .cancel-btn {
                background: transparent;
                border: 2px solid var(--primary-dark);
                border-radius: 6px;
                width: 24px;
                height: 24px;
                color: var(--primary-dark);
                font-size: 16px;
                line-height: 20px;
                text-align: center;
                cursor: pointer;
                transition: all 0.3s ease;
            }

            .cancel-btn:hover {
                background: var(--primary-dark);
                color: var(--element-active-text);
                transform: scale(1.1);
            }

            .draw-btn {
                background: var(--primary-color);
                border: 1px dashed var(--primary-dark);
                padding: 8px;
                border-radius: 10px;
                color: var(--element-active-text);
                font-size: 14px;
                font-weight: 700;
                cursor: pointer;
                transition: background 0.3s ease, transform 0.3s ease;
                text-align: center;
            }

            .draw-btn:hover:not(:disabled) {
                background: var(--primary-dark);
                transform: scale(1.05);
            }

            .draw-btn:disabled {
                background: rgba(255, 105, 180, 0.5);
                cursor: not-allowed;
            }

            .kawaii-footer {
                display: flex;
                justify-content: center;
                align-items: center;
                margin-top: 10px;
                padding: 6px;
                background: var(--element-bg);
                border-radius: 10px;
                border: 2px solid var(--primary-color);
            }

            .credit-text {
                font-size: 10px;
                color: var(--text-color);
                font-weight: 700;
            }
        `;
        document.head.appendChild(style);

        // DOM Elements
        const kawaiiCheat = document.getElementById('kawaiiCheat');
        const kawaiiHeader = document.getElementById('kawaiiHeader');
        const minimizeBtn = document.getElementById('minimizeBtn');
        const tabButtons = document.querySelectorAll('.kawaii-tab');
        const tabContents = document.querySelectorAll('.kawaii-content');
        const autoGuessCheckbox = document.getElementById('autoGuess');
        const speedContainer = document.getElementById('speedContainer');
        const guessSpeed = document.getElementById('guessSpeed');
        const speedValue = document.getElementById('speedValue');
        const customWordsCheckbox = document.getElementById('customWords');
        const wordListContainer = document.getElementById('wordListContainer');
        const wordListDropzone = document.getElementById('wordListDropzone');
        const wordListInput = document.getElementById('wordList');
        const guessPattern = document.getElementById('guessPattern');
        const hitList = document.getElementById('hitList');
        const imageDropzone = document.getElementById('imageDropzone');
        const imageUpload = document.getElementById('imageUpload');
        const imagePreview = document.getElementById('imagePreview');
        const previewImg = document.getElementById('previewImg');
        const cancelImage = document.getElementById('cancelImage');
        const drawSpeed = document.getElementById('drawSpeed');
        const drawSpeedValue = document.getElementById('drawSpeedValue');
        const maxColors = document.getElementById('maxColors');
        const maxColorsValue = document.getElementById('maxColorsValue');
        const sendDraw = document.getElementById('sendDraw');

        // Variables
        let isDragging = false;
        let initialX, initialY;
        let xOffset = 0, yOffset = 0;
        let rafId = null; // requestAnimationFrame ID
        let autoGuessInterval = null;
        let wordList = { "Custom": [] };
        let triedLabelAdded = false;

        const wordListURLs = {
            "General (en)": "https://cdn.jsdelivr.net/gh/Gartic-Developers/Gartic-WordList@master/languages/English/general.json",
            "General (tr)": "https://cdn.jsdelivr.net/gh/Gartic-Developers/Gartic-WordList@master/languages/Turkish/general.json"
        };

        // Utility Functions
        function updateSliderTrack(slider) {
            const min = parseInt(slider.min);
            const max = parseInt(slider.max);
            const value = parseInt(slider.value);
            const progress = ((value - min) / (max - min)) * 100;
            slider.parentElement.querySelector('.slider-track').style.setProperty('--slider-progress', `${progress}%`);
        }

        function preventDefaults(e) {
            e.preventDefault();
            e.stopPropagation();
        }

        // Initial Setup
        updateSliderTrack(guessSpeed);
        updateSliderTrack(drawSpeed);
        updateSliderTrack(maxColors);

        // Dragging Functionality with Optimization
        kawaiiHeader.addEventListener('mousedown', (e) => {
            if (e.target !== minimizeBtn) {
                initialX = e.clientX - xOffset;
                initialY = e.clientY - yOffset;
                isDragging = true;
                kawaiiCheat.classList.add('dragging');
                if (rafId) cancelAnimationFrame(rafId); // Önceki frame'i iptal et
            }
        });

        document.addEventListener('mousemove', (e) => {
            if (isDragging) {
                e.preventDefault();
                const newX = e.clientX - initialX;
                const newY = e.clientY - initialY;

                if (rafId) cancelAnimationFrame(rafId); // Tekrarlanan frame'leri önle
                rafId = requestAnimationFrame(() => {
                    kawaiiCheat.style.transform = `translate(${newX}px, ${newY}px)`;
                    xOffset = newX;
                    yOffset = newY;
                });
            }
        });

        document.addEventListener('mouseup', () => {
            if (isDragging) {
                isDragging = false;
                kawaiiCheat.classList.remove('dragging');
                if (rafId) cancelAnimationFrame(rafId); // Son frame'i temizle
            }
        });

        // Minimize Button
        minimizeBtn.addEventListener('click', () => {
            kawaiiCheat.classList.toggle('minimized');
            minimizeBtn.textContent = kawaiiCheat.classList.contains('minimized') ? '▲' : '▼';
        });

        // Tab Switching
        tabButtons.forEach(btn => {
            btn.addEventListener('click', () => {
                tabButtons.forEach(b => b.classList.remove('active'));
                tabContents.forEach(c => c.style.display = 'none');
                btn.classList.add('active');
                document.getElementById(`${btn.dataset.tab}-tab`).style.display = 'flex';
            });
        });

        // Checkbox Container Click
        document.querySelectorAll('.checkbox-container').forEach(container => {
            container.addEventListener('click', (e) => {
                const checkbox = container.querySelector('input[type="checkbox"]');
                if (e.target !== checkbox) {
                    checkbox.checked = !checkbox.checked;
                    checkbox.dispatchEvent(new Event('change'));
                }
            });
        });

        // Auto Guess Checkbox
        autoGuessCheckbox.addEventListener('change', (e) => {
            speedContainer.style.display = e.target.checked ? 'flex' : 'none';
            if (!e.target.checked) stopAutoGuess();
            else if (guessPattern.value) startAutoGuess();
        });

        // Guess Speed Slider
        guessSpeed.addEventListener('input', (e) => {
            updateSliderTrack(e.target);
            speedValue.textContent = e.target.value >= 1000 ? `${e.target.value / 1000}s` : `${e.target.value}ms`;
            if (autoGuessCheckbox.checked && autoGuessInterval) {
                stopAutoGuess();
                startAutoGuess();
            }
        });

        // Custom Words Checkbox
        customWordsCheckbox.addEventListener('change', (e) => {
            wordListContainer.style.display = e.target.checked ? 'block' : 'none';
            updateHitList(guessPattern.value.trim());
        });

        // Word List Dropzone
        ['dragenter', 'dragover', 'dragleave', 'drop'].forEach(eventName => {
            wordListDropzone.addEventListener(eventName, preventDefaults, false);
        });

        wordListDropzone.addEventListener('dragenter', () => wordListDropzone.classList.add('drag-over'));
        wordListDropzone.addEventListener('dragover', () => wordListDropzone.classList.add('drag-over'));
        wordListDropzone.addEventListener('dragleave', () => wordListDropzone.classList.remove('drag-over'));
        wordListDropzone.addEventListener('drop', (e) => {
            wordListDropzone.classList.remove('drag-over');
            const file = e.dataTransfer.files[0];
            if (file && file.type === 'text/plain') handleWordListFile(file);
        });

        wordListInput.addEventListener('change', (e) => {
            const file = e.target.files[0];
            if (file) {
                handleWordListFile(file);
                e.target.value = '';
            }
        });

        function handleWordListFile(file) {
            const reader = new FileReader();
            reader.onload = function(event) {
                wordList["Custom"] = event.target.result.split('\n').map(word => word.trim()).filter(word => word.length > 0);
                alert(`Loaded ${wordList["Custom"].length} words from ${file.name}`);
                updateHitList(guessPattern.value.trim());
            };
            reader.readAsText(file);
        }

        // Guess Pattern Input
        guessPattern.addEventListener('input', (e) => updateHitList(e.target.value.trim()));

        // Hit List Functionality
        hitList.addEventListener('click', (e) => {
            if (e.target.tagName === 'BUTTON' && !e.target.classList.contains('tried')) {
                const button = e.target;
                button.classList.add('tried');
                if (!triedLabelAdded && hitList.querySelectorAll('button.tried').length === 1) {
                    const triedLabel = document.createElement('div');
                    triedLabel.classList.add('tried-label');
                    triedLabel.textContent = 'Tried Words';
                    hitList.appendChild(triedLabel);
                    triedLabelAdded = true;
                }
                if (window.game && window.game._socket) {
                    window.game._socket.emit(13, window.game._codigo, button.textContent);
                }
                hitList.appendChild(button);
            }
        });

        function startAutoGuess() {
            if (!autoGuessCheckbox.checked) return;
            stopAutoGuess();
            const speed = parseInt(guessSpeed.value);
            autoGuessInterval = setInterval(() => {
                const buttons = hitList.querySelectorAll('button:not(.tried)');
                if (buttons.length > 0 && window.game && window.game._socket) {
                    const word = buttons[0].textContent;
                    buttons[0].classList.add('tried');
                    window.game._socket.emit(13, window.game._codigo, word);
                    if (!triedLabelAdded && hitList.querySelectorAll('button.tried').length === 1) {
                        const triedLabel = document.createElement('div');
                        triedLabel.classList.add('tried-label');
                        triedLabel.textContent = 'Tried Words';
                        hitList.appendChild(triedLabel);
                        triedLabelAdded = true;
                    }
                    hitList.appendChild(buttons[0]);
                }
            }, speed);
        }

        function stopAutoGuess() {
            if (autoGuessInterval) {
                clearInterval(autoGuessInterval);
                autoGuessInterval = null;
            }
        }

        function updateHitList(pattern) {
            hitList.innerHTML = '';
            triedLabelAdded = false;
            const activeTheme = customWordsCheckbox.checked || !window.game || !window.game._dadosSala || !window.game._dadosSala.tema
            ? "Custom" : window.game._dadosSala.tema;
            const activeList = wordList[activeTheme] || [];

            if (!pattern) {
                if (activeList.length === 0) {
                    hitList.innerHTML = `<div class="message">${customWordsCheckbox.checked ? 'Upload a custom word list ✧' : 'No words available ✧'}</div>`;
                } else {
                    activeList.forEach(word => {
                        const button = document.createElement('button');
                        button.textContent = word;
                        hitList.appendChild(button);
                    });
                }
                return;
            }

            const regex = new RegExp(`^${pattern.split('').map(char => char === '_' ? '.' : char).join('')}$`, 'i');
            const matches = activeList.filter(word => regex.test(word));

            if (matches.length === 0) {
                hitList.innerHTML = '<div class="message">No matches found ✧</div>';
            } else {
                matches.forEach(word => {
                    const button = document.createElement('button');
                    button.textContent = word;
                    hitList.appendChild(button);
                });
            }
        }

        async function fetchWordList(theme) {
            if (!wordList[theme] && wordListURLs[theme]) {
                try {
                    const response = await fetch(wordListURLs[theme]);
                    if (!response.ok) throw new Error(`Failed to fetch ${theme} word list`);
                    const data = await response.json();
                    wordList[theme] = data.words || data;
                    console.log(`Loaded ${wordList[theme].length} words for ${theme}`);
                } catch (error) {
                    console.error(`Error fetching word list for ${theme}:`, error);
                    wordList[theme] = [];
                }
            }
        }

        // Image Upload
        ['dragenter', 'dragover', 'dragleave', 'drop'].forEach(eventName => {
            imageDropzone.addEventListener(eventName, preventDefaults, false);
        });

        imageDropzone.addEventListener('dragenter', () => imageDropzone.classList.add('drag-over'));
        imageDropzone.addEventListener('dragover', () => imageDropzone.classList.add('drag-over'));
        imageDropzone.addEventListener('dragleave', () => imageDropzone.classList.remove('drag-over'));
        imageDropzone.addEventListener('drop', (e) => {
            imageDropzone.classList.remove('drag-over');
            const file = e.dataTransfer.files[0];
            if (file && file.type.startsWith('image/')) handleImageFile(file);
        });

        imageUpload.addEventListener('change', (e) => {
            const file = e.target.files[0];
            if (file) {
                handleImageFile(file);
                e.target.value = '';
            }
        });

        function handleImageFile(file) {
            const reader = new FileReader();
            reader.onload = function(event) {
                previewImg.src = event.target.result;
                imageDropzone.style.display = 'none';
                imagePreview.style.display = 'block';
                sendDraw.disabled = false;
            };
            reader.readAsDataURL(file);
        }

        cancelImage.addEventListener('click', () => {
            previewImg.src = '';
            imageDropzone.style.display = 'flex';
            imagePreview.style.display = 'none';
            sendDraw.disabled = true;
            imageUpload.value = '';
        });

        drawSpeed.addEventListener('input', (e) => {
            updateSliderTrack(e.target);
            drawSpeedValue.textContent = e.target.value >= 1000 ? `${e.target.value / 1000}s` : `${e.target.value}ms`;
        });

        maxColors.addEventListener('input', (e) => {
            updateSliderTrack(e.target);
            maxColorsValue.textContent = e.target.value;
        });

        sendDraw.addEventListener('click', () => {
            if (previewImg.src) {
                if (!window.game || !window.game.turn) {
                    alert('Not your turn or game not loaded! ✧');
                    return;
                }
                sendDraw.disabled = true;
                processAndDrawImage(previewImg.src);
            }
        });

        // Socket Integration
        const checkGame = setInterval(() => {
            if (window.game && window.game._socket) {
                clearInterval(checkGame);
                const currentTheme = window.game._dadosSala.tema || "Custom";
                if (currentTheme !== "Custom") {
                    fetchWordList(currentTheme).then(() => updateHitList(guessPattern.value.trim()));
                }

                window.game._socket.on(30, (hint) => {
                    hint = String(hint).replace(/,/g, '');
                    guessPattern.value = hint;
                    updateHitList(hint);
                    if (autoGuessCheckbox.checked) startAutoGuess();
                });

                window.game._socket.on(19, () => {
                    guessPattern.value = '';
                    stopAutoGuess();
                    updateHitList('');
                });

                window.game._socket.on(15, (playerId) => {
                    if (playerId === window.game.me.id) {
                        guessPattern.value = '';
                        stopAutoGuess();
                        updateHitList('');
                    }
                });

                let lastTheme = currentTheme;
                setInterval(() => {
                    const newTheme = window.game._dadosSala.tema || "Custom";
                    if (newTheme !== lastTheme && newTheme !== "Custom") {
                        lastTheme = newTheme;
                        fetchWordList(newTheme).then(() => updateHitList(guessPattern.value.trim()));
                    }
                }, 1000);
            }
        }, 100);
    }

function processAndDrawImage(imageSrc) {
    if (!window.game || !window.game._socket || !window.game._desenho || !window.game.turn) {
        alert('Game not ready or not your turn! ✧');
        return;
    }

    const img = new Image();
    img.crossOrigin = "Anonymous";
    img.onload = async function() {
        const gameCanvas = window.game._desenho._canvas.canvas;
        if (!gameCanvas || !gameCanvas.width || !gameCanvas.height) {
            alert('Canvas not accessible! ✧');
            sendDraw.disabled = false;
            return;
        }

        const ctx = gameCanvas.getContext('2d');
        if (!ctx) {
            alert('Canvas context not available! ✧');
            sendDraw.disabled = false;
            return;
        }

        const canvasWidth = Math.floor(gameCanvas.width);
        const canvasHeight = Math.floor(gameCanvas.height);

        const tempCanvas = document.createElement('canvas');
        const tempCtx = tempCanvas.getContext('2d');
        if (!tempCtx) {
            alert('Temp canvas context failed! ✧');
            sendDraw.disabled = false;
            return;
        }

        const scale = Math.min(canvasWidth / img.width, canvasHeight / img.height);
        const newWidth = Math.floor(img.width * scale);
        const newHeight = Math.floor(img.height * scale);

        tempCanvas.width = canvasWidth;
        tempCanvas.height = canvasHeight;

        const offsetX = Math.floor((canvasWidth - newWidth) / 2);
        const offsetY = Math.floor((canvasHeight - newHeight) / 2);

        tempCtx.drawImage(img, offsetX, offsetY, newWidth, newHeight);

        let imageData;
        try {
            imageData = tempCtx.getImageData(0, 0, canvasWidth, canvasHeight);
        } catch (e) {
            alert('Image data error: ' + e.message + ' ✧');
            sendDraw.disabled = false;
            return;
        }

        const data = imageData.data;
        const drawSpeedValue = parseInt(drawSpeed.value) || 300;

        // Get maxColors from menu
        const maxColorsValue = parseInt(maxColors.value) || 20;

        // Image bounds
        const imgLeft = offsetX;
        const imgRight = offsetX + newWidth - 1;
        const imgTop = offsetY;
        const imgBottom = offsetY + newHeight - 1;

        // Background detection
        const colorCounts = new Map();
        let backgroundColor = [255, 255, 255];
        const sampleStep = Math.max(1, Math.floor(newWidth / 50));

        for (let x = imgLeft; x <= imgRight; x += sampleStep) {
            for (let y = imgTop; y <= imgBottom; y += sampleStep) {
                const index = (y * canvasWidth + x) * 4;
                const r = Math.round(data[index] / 20) * 20;
                const g = Math.round(data[index+1] / 20) * 20;
                const b = Math.round(data[index+2] / 20) * 20;
                const key = `${r},${g},${b}`;
                colorCounts.set(key, (colorCounts.get(key) || 0) + 1);
            }
        }

        let maxCount = 0;
        for (const [key, count] of colorCounts) {
            if (count > maxCount) {
                maxCount = count;
                backgroundColor = key.split(',').map(Number);
            }
        }

        const bgHex = 'x' + backgroundColor.map(c =>
            c.toString(16).padStart(2, '0').toUpperCase()
        ).join('');

        // Clear and set background (both socket and local)
        window.game._socket.emit(10, window.game._codigo, [4]);
        ctx.clearRect(0, 0, canvasWidth, canvasHeight);
        await new Promise(resolve => setTimeout(resolve, drawSpeedValue));

        window.game._socket.emit(10, window.game._codigo, [5, bgHex]);
        ctx.fillStyle = `#${bgHex.slice(1)}`;
        await new Promise(resolve => setTimeout(resolve, drawSpeedValue));

        window.game._socket.emit(10, window.game._codigo, [3, 0, 0, canvasWidth, canvasHeight]);
        ctx.fillRect(0, 0, canvasWidth, canvasHeight);
        await new Promise(resolve => setTimeout(resolve, drawSpeedValue));

        // Color clustering for foreground
        const colorClusters = new Map();
        for (let y = imgTop; y < imgBottom; y += sampleStep) {
            for (let x = imgLeft; x < imgRight; x += sampleStep) {
                const index = (y * canvasWidth + x) * 4;
                const r = Math.round(data[index] / 20) * 20;
                const g = Math.round(data[index+1] / 20) * 20;
                const b = Math.round(data[index+2] / 20) * 20;
                const key = `${r},${g},${b}`;

                if (colorDistance([r, g, b], backgroundColor) > 60) {
                    colorClusters.set(key, (colorClusters.get(key) || 0) + 1);
                }
            }
        }

        const topColors = [...colorClusters.entries()]
            .sort((a, b) => b[1] - a[1])
            .slice(0, maxColorsValue)
            .map(([key]) => ({
                rgb: key.split(',').map(Number),
                hex: 'x' + key.split(',').map(c =>
                    Number(c).toString(16).padStart(2, '0').toUpperCase()
                ).join('')
            }));

        // Fill regions by color (horizontal and vertical passes)
        const fillsByColor = {};
        const visited = new Set();
        const stripHeight = 1;
        const stripWidth = 1;
        const minStripSize = 1; // Single pixels now included

        // Horizontal fills
        for (let y = imgTop; y < imgBottom; y += stripHeight) {
            let startX = null;
            let currentColor = null;
            let stripLength = 0;

            for (let x = imgLeft; x < imgRight; x += 1) {
                const index = (y * canvasWidth + x) * 4;
                const pixelColor = [data[index], data[index+1], data[index+2]];
                const bgDist = colorDistance(pixelColor, backgroundColor);

                if (bgDist > 60 && !visited.has(`${x},${y}`)) {
                    const nearestColor = topColors.reduce((prev, curr) =>
                        colorDistance(pixelColor, prev.rgb) < colorDistance(pixelColor, curr.rgb) ? prev : curr
                    );

                    if (startX === null || currentColor?.hex !== nearestColor.hex) {
                        if (startX !== null && stripLength >= minStripSize) {
                            if (!fillsByColor[currentColor.hex]) fillsByColor[currentColor.hex] = [];
                            fillsByColor[currentColor.hex].push([startX, y, stripLength, stripHeight]);
                            for (let dx = 0; dx < stripLength; dx++) {
                                visited.add(`${startX + dx},${y}`);
                            }
                        }
                        startX = x;
                        currentColor = nearestColor;
                        stripLength = 1;
                    } else {
                        stripLength++;
                    }
                } else if (startX !== null && bgDist <= 60) {
                    if (stripLength >= minStripSize) {
                        if (!fillsByColor[currentColor.hex]) fillsByColor[currentColor.hex] = [];
                        fillsByColor[currentColor.hex].push([startX, y, stripLength, stripHeight]);
                        for (let dx = 0; dx < stripLength; dx++) {
                            visited.add(`${startX + dx},${y}`);
                        }
                    }
                    startX = null;
                    currentColor = null;
                    stripLength = 0;
                }
            }

            if (startX !== null && stripLength >= minStripSize) {
                if (!fillsByColor[currentColor.hex]) fillsByColor[currentColor.hex] = [];
                fillsByColor[currentColor.hex].push([startX, y, stripLength, stripHeight]);
                for (let dx = 0; dx < stripLength; dx++) {
                    visited.add(`${startX + dx},${y}`);
                }
            }
        }

        // Vertical fills
        for (let x = imgLeft; x < imgRight; x += stripWidth) {
            let startY = null;
            let currentColor = null;
            let stripLength = 0;

            for (let y = imgTop; y < imgBottom; y += 1) {
                const index = (y * canvasWidth + x) * 4;
                const pixelColor = [data[index], data[index+1], data[index+2]];
                const bgDist = colorDistance(pixelColor, backgroundColor);

                if (bgDist > 60 && !visited.has(`${x},${y}`)) {
                    const nearestColor = topColors.reduce((prev, curr) =>
                        colorDistance(pixelColor, prev.rgb) < colorDistance(pixelColor, curr.rgb) ? prev : curr
                    );

                    if (startY === null || currentColor?.hex !== nearestColor.hex) {
                        if (startY !== null && stripLength >= minStripSize) {
                            if (!fillsByColor[currentColor.hex]) fillsByColor[currentColor.hex] = [];
                            fillsByColor[currentColor.hex].push([x, startY, stripWidth, stripLength]);
                            for (let dy = 0; dy < stripLength; dy++) {
                                visited.add(`${x},${startY + dy}`);
                            }
                        }
                        startY = y;
                        currentColor = nearestColor;
                        stripLength = 1;
                    } else {
                        stripLength++;
                    }
                } else if (startY !== null && bgDist <= 60) {
                    if (stripLength >= minStripSize) {
                        if (!fillsByColor[currentColor.hex]) fillsByColor[currentColor.hex] = [];
                        fillsByColor[currentColor.hex].push([x, startY, stripWidth, stripLength]);
                        for (let dy = 0; dy < stripLength; dy++) {
                            visited.add(`${x},${startY + dy}`);
                        }
                    }
                    startY = null;
                    currentColor = null;
                    stripLength = 0;
                }
            }

            if (startY !== null && stripLength >= minStripSize) {
                if (!fillsByColor[currentColor.hex]) fillsByColor[currentColor.hex] = [];
                fillsByColor[currentColor.hex].push([x, startY, stripWidth, stripLength]);
                for (let dy = 0; dy < stripLength; dy++) {
                    visited.add(`${x},${startY + dy}`);
                }
            }
        }

        // Draw fills grouped by color (socket and local)
        for (const color in fillsByColor) {
            const fillCommand = [3];
            fillsByColor[color].forEach(([x, y, width, height]) => {
                fillCommand.push(x, y, width, height);
            });

            window.game._socket.emit(10, window.game._codigo, [5, color]);
            ctx.fillStyle = `#${color.slice(1)}`;
            await new Promise(resolve => setTimeout(resolve, drawSpeedValue));

            window.game._socket.emit(10, window.game._codigo, fillCommand);
            fillsByColor[color].forEach(([x, y, width, height]) => {
                ctx.fillRect(x, y, width, height);
            });
            await new Promise(resolve => setTimeout(resolve, drawSpeedValue));
        }

        // Straight lines for remaining details
        const lines = [];
        const lineStep = 1; // Still pixel-by-pixel
        const minLineLength = 1; // Draw even single pixels

        for (let y = imgTop; y < imgBottom; y += lineStep) {
            for (let x = imgLeft; x < imgRight; x += lineStep) {
                if (visited.has(`${x},${y}`)) continue;

                const index = (y * canvasWidth + x) * 4;
                const pixelColor = [data[index], data[index+1], data[index+2]];
                if (colorDistance(pixelColor, backgroundColor) <= 60) continue;

                const nearestColor = topColors.reduce((prev, curr) =>
                    colorDistance(pixelColor, prev.rgb) < colorDistance(pixelColor, curr.rgb) ? prev : curr
                );

                // Check horizontal vs vertical preference (even for 1 pixel)
                let horizontalScore = 0;
                let verticalScore = 0;

                // Check right (horizontal)
                for (let dx = 1; dx <= minLineLength; dx++) {
                    const nx = x + dx;
                    if (nx >= imgRight) break;
                    const ni = (y * canvasWidth + nx) * 4;
                    const nextColor = [data[ni], data[ni+1], data[ni+2]];
                    if (colorDistance(nextColor, pixelColor) < 50 && !visited.has(`${nx},${y}`)) {
                        horizontalScore++;
                    } else {
                        break;
                    }
                }

                // Check down (vertical)
                for (let dy = 1; dy <= minLineLength; dy++) {
                    const ny = y + dy;
                    if (ny >= imgBottom) break;
                    const ni = (ny * canvasWidth + x) * 4;
                    const nextColor = [data[ni], data[ni+1], data[ni+2]];
                    if (colorDistance(nextColor, pixelColor) < 50 && !visited.has(`${x},${ny}`)) {
                        verticalScore++;
                    } else {
                        break;
                    }
                }

                // Decide direction and length (default to 1 if no longer segment)
                let lineLength, points;
                if (horizontalScore >= verticalScore && horizontalScore >= 1) {
                    lineLength = Math.min(horizontalScore, imgRight - x);
                    points = [[x, y], [x + lineLength - 1, y]];
                } else if (verticalScore >= 1) {
                    lineLength = Math.min(verticalScore, imgBottom - y);
                    points = [[x, y], [x, y + lineLength - 1]];
                } else {
                    // Single pixel case
                    lineLength = 1;
                    points = [[x, y], [x, y]]; // Same point, will draw as 1x1 fill
                }

                lines.push({
                    points: points,
                    color: nearestColor.hex
                });
                for (let dx = 0; dx < lineLength && points[0][0] === points[1][0]; dx++) {
                    visited.add(`${x},${y + dx}`);
                }
                for (let dy = 0; dy < lineLength && points[0][1] === points[1][1]; dy++) {
                    visited.add(`${x + dy},${y}`);
                }
            }
        }

        // Draw straight lines (socket and local)
        for (const { points, color } of lines) {
            window.game._socket.emit(10, window.game._codigo, [5, color]);
            ctx.strokeStyle = `#${color.slice(1)}`;
            await new Promise(resolve => setTimeout(resolve, drawSpeedValue));

            window.game._socket.emit(10, window.game._codigo, [6, 4]);
            ctx.lineWidth = 4;
            await new Promise(resolve => setTimeout(resolve, drawSpeedValue));

            const drawCommand = [1, 6, points[0][0], points[0][1], points[1][0], points[1][1]];
            window.game._socket.emit(10, window.game._codigo, drawCommand);

            ctx.beginPath();
            ctx.moveTo(points[0][0], points[0][1]);
            ctx.lineTo(points[1][0], points[1][1]);
            ctx.stroke();
            await new Promise(resolve => setTimeout(resolve, drawSpeedValue));
        }

        alert('Drawing completed! ✧');
        sendDraw.disabled = false;
    };
    img.onerror = function() {
        alert('Failed to load image! ✧');
        sendDraw.disabled = false;
    };
    img.src = imageSrc;
}

// Color distance helper
function colorDistance(color1, color2) {
    return Math.sqrt(
        Math.pow(color1[0] - color2[0], 2) +
        Math.pow(color1[1] - color2[1], 2) +
        Math.pow(color1[2] - color2[2], 2)
    );
}
})();

QingJ © 2025

镜像随时可能失效,请加Q群300939539或关注我们的公众号极客氢云获取最新地址