// ==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)
);
}
})();