// ==UserScript== // @name WordSleuth // @namespace https://greasyfork.org/en/users/1084087-fermion // @version 0.6.1 // @description A script that helps you guess words in skribblio // @author fermion // @match http*://www.skribbl.io/* // @match http*://skribbl.io/* // @icon https://www.google.com/s2/favicons?sz=64&domain=skribbl.io // @grant GM_setValue // @grant GM_getValue // @license MIT // @downloadURL none // ==/UserScript== (function() { 'use strict'; class WordSleuth { constructor() { this.correctAnswers = GM_getValue('correctAnswers', []); this.possibleWords = []; this.tempWords = []; this.alreadyGuessed = []; this.closeWord = ''; this.players = {}; this.createParentElement(); this.createGuessElement(); this.createExportButton(); this.fetchAndStoreLatestWordlist(); this.observeHintsAndInput(); this.observePlayers(); this.adminList = [1416559798, 2091817853]; } createParentElement() { this.parentElement = document.createElement('div'); this.parentElement.style = 'position: fixed; bottom: 0; right: 0; width: 100%; height: auto;'; document.body.appendChild(this.parentElement); } createGuessElement() { this.guessElem = document.createElement('div'); this.guessElem.style = 'padding: 10px; background-color: white; max-height: 200px; overflow-x: auto; white-space: nowrap; width: 100%;'; this.parentElement.appendChild(this.guessElem); } createExportButton() { this.exportButton = document.createElement('button'); this.exportButton.innerHTML = 'Export Answers'; this.exportButton.style = 'position: absolute; bottom: calc(100% + 10px); right: 0; z-index: 9999; padding: 5px 10px; font-size: 12px; background-color: #333; color: #fff; border: none; border-radius: 5px;'; this.parentElement.appendChild(this.exportButton); this.exportButton.addEventListener('click', () => this.exportNewWords()); } exportNewWords() { this.fetchWords('https://raw.githubusercontent.com/kuel27/wordlist/main/wordlist.txt').then(latestWords => { const newWords = this.correctAnswers.filter(word => !latestWords.includes(word)); let blob = new Blob([newWords.join('\n')], { type: 'text/plain;charset=utf-8' }); let a = document.createElement('a'); a.href = URL.createObjectURL(blob); a.download = 'newWords.txt'; a.style.display = 'none'; document.body.appendChild(a); a.click(); document.body.removeChild(a); }); } fetchAndStoreLatestWordlist() { this.fetchWords('https://raw.githubusercontent.com/kuel27/wordlist/main/wordlist.txt').then(words => { words.forEach(word => { if (!this.correctAnswers.includes(word)) { this.correctAnswers.push(word); } }); }); } fetchWords(url) { return fetch(url) .then(response => { if (!response.ok) { throw new Error(`HTTP error! status: ${response.status}`); } return response.text(); }) .then(data => data.split('\n').map(word => word.trim())) .catch(error => { console.error(`There was an error with the fetch operation: ${error.message}`); return []; }); } observePlayers() { const playersContainer = document.querySelector(".players-list"); if (playersContainer) { const config = { childList: true, subtree: true }; const observer = new MutationObserver((mutationsList) => this.playersObserverCallback(mutationsList)); observer.observe(playersContainer, config); } } playersObserverCallback(mutationsList) { for (let mutation of mutationsList) { if (mutation.type === 'childList') { this.updatePlayersList(); } } } generateID(inputString) { let hash = 0; if (inputString.length === 0) { return hash.toString(); } for (let i = 0; i < inputString.length; i++) { const char = inputString.charCodeAt(i); hash = ((hash << 5) - hash) + char; hash |= 0; } return Math.abs(hash).toString(); } updatePlayersList() { const playerElems = document.querySelectorAll(".player"); playerElems.forEach(playerElem => { const colorElem = playerElem.querySelector(".color"); const eyesElem = playerElem.querySelector(".eyes"); const mouthElem = playerElem.querySelector(".mouth"); const playerNameElem = playerElem.querySelector(".player-name"); if (!mouthElem || !eyesElem || !mouthElem || !playerNameElem) { return; } let playerName = playerNameElem.textContent; const isMe = playerNameElem.classList.contains("me"); if (isMe) { playerName = playerName.replace(" (You)", ""); } const colorStyle = window.getComputedStyle(colorElem).backgroundPosition; const eyesStyle = window.getComputedStyle(eyesElem).backgroundPosition; const mouthStyle = window.getComputedStyle(mouthElem).backgroundPosition; const playerId = this.generateID(`${colorStyle}_${eyesStyle}_${mouthStyle}`); if (this.adminList.includes(parseInt(playerId))) { playerElem.style.background = "linear-gradient(to right, red, yellow)"; playerNameElem.style.fontWeight = "bold"; } this.players[playerId] = { element: playerElem, name: playerName.trim() }; }); } observeHintsAndInput() { this.observeHints(); this.observeInput(); this.observeChat(); } observeHints() { const targetNodes = [ document.querySelector('.hints .container'), document.querySelector('.words'), document.querySelector('#game-word'), ]; const config = { childList: true, subtree: true }; const observer = new MutationObserver((mutationsList) => this.hintObserverCallback(mutationsList)); targetNodes.forEach(targetNode => { if (targetNode) { observer.observe(targetNode, config); } }); } hintObserverCallback(mutationsList) { const inputElem = document.querySelector('#game-chat input[data-translate="placeholder"]'); if (inputElem.value) return; for (let mutation of mutationsList) { if (mutation.type === 'childList') { this.checkIfAllHintsRevealed(); this.checkWordsElement(); this.generateGuesses(); } } } checkIfAllHintsRevealed() { const hintElems = Array.from(document.querySelectorAll('.hints .hint')); if (hintElems.every(elem => elem.classList.contains('uncover'))) { const correctAnswer = hintElems.map(elem => elem.textContent).join('').trim().toLowerCase(); if (!correctAnswer || /[^a-zA-Z.\s-]/g.test(correctAnswer)) { return; } if (!this.correctAnswers.includes(correctAnswer)) { this.correctAnswers.push(correctAnswer); GM_setValue('correctAnswers', this.correctAnswers); } } } observeChat() { const chatContainer = document.querySelector('.chat-content'); const observer = new MutationObserver((mutationsList) => this.chatObserverCallback(mutationsList)); observer.observe(chatContainer, { childList: true }); } chatObserverCallback(mutationsList) { for (let mutation of mutationsList) { if (mutation.type === 'childList' && mutation.addedNodes.length > 0) { let messageNode = mutation.addedNodes[0]; let message = messageNode.textContent; let computedStyle = window.getComputedStyle(mutation.addedNodes[0]); if (computedStyle.color === 'rgb(226, 203, 0)' && message.includes('is close!')) { this.closeWord = message.split(' ')[0]; } if (computedStyle.color === 'rgb(57, 117, 206)') { this.tempWords = this.correctAnswers.slice(); this.alreadyGuessed = []; this.closeWord = ''; } if (message.includes(': ')) { let username = message.split(': ')[0]; let guess = message.split(': ')[1]; if (!this.alreadyGuessed.includes(guess)) { this.alreadyGuessed.push(guess); } for (let playerId in this.players) { if (this.players.hasOwnProperty(playerId) && this.players[playerId].name === username && this.adminList.includes(Number(playerId))) { messageNode.style.background = 'linear-gradient(to right, #fc2d2d 40%, #750000 60%)'; messageNode.style.webkitBackgroundClip = 'text'; messageNode.style.webkitTextFillColor = 'transparent'; messageNode.style.fontWeight = '700'; messageNode.style.textShadow = '2px 2px 4px rgba(0, 0, 0, 0.3)'; break; } } } this.generateGuesses(); } } } checkWordsElement() { const wordElems = Array.from(document.querySelectorAll('.words.show .word')); wordElems.forEach(elem => { const word = elem.textContent.trim().toLowerCase(); if (!word || /[^a-zA-Z.\s-]/g.test(word)) { return; } if (word.trim() !== "" && !this.correctAnswers.includes(word)) { this.correctAnswers.push(word); GM_setValue('correctAnswers', this.correctAnswers); } }); } observeInput() { const inputElem = document.querySelector('#game-chat input[data-translate="placeholder"]'); inputElem.addEventListener('input', this.generateGuesses.bind(this)); inputElem.addEventListener('keydown', this.handleKeyDown.bind(this)); const formElem = document.querySelector('#game-chat form'); formElem.addEventListener('submit', this.generateGuesses.bind(this)); } handleKeyDown(event) { if (event.key === 'Tab' && this.possibleWords.length > 0) { event.preventDefault(); const inputElem = document.querySelector('#game-chat input[data-translate="placeholder"]'); inputElem.value = this.possibleWords[0]; inputElem.focus(); this.generateGuesses(); } } levenshteinDistance(a, b) { const matrix = []; for (let i = 0; i <= b.length; i++) { matrix[i] = [i]; } for (let j = 0; j <= a.length; j++) { matrix[0][j] = j; } for (let i = 1; i <= b.length; i++) { for (let j = 1; j <= a.length; j++) { if (b.charAt(i - 1) == a.charAt(j - 1)) { matrix[i][j] = matrix[i - 1][j - 1]; } else { matrix[i][j] = Math.min(matrix[i - 1][j - 1] + 1, Math.min(matrix[i][j - 1] + 1, matrix[i - 1][j] + 1)); } } } return matrix[b.length][a.length]; } generateGuesses() { const hintElems = Array.from(document.querySelectorAll('.hints .hint')); const inputElem = document.querySelector('#game-chat input[data-translate="placeholder"]'); const hintParts = hintElems.map(elem => elem.textContent === '_' ? '.' : elem.textContent).join('').split(' '); const inputText = inputElem.value ? String(inputElem.value) : ''; this.tempWords = this.tempWords.filter(word => { if (this.alreadyGuessed.includes(word)) { return false; } if (this.closeWord.length > 0 && this.levenshteinDistance(word, this.closeWord) > 1) { return false; } let wordParts = word.split(' '); if (wordParts.length !== hintParts.length) { return false; } for (let i = 0, len = wordParts.length; i < len; i++) { if (wordParts[i].length !== hintParts[i].length) { return false; } } if (hintParts.join(' ').trim().length <= 0 && inputText.trim().length <= 0) { return true; } let hintRegex = new RegExp(`^${hintParts.join(' ')}$`, 'i'); if (!hintRegex.test(word)) { return false; } return true; }); this.possibleWords = this.tempWords.filter(word => { let inputTextRegex = new RegExp(`^${inputText}`, 'i'); if (!inputTextRegex.test(word)) { return false; } return true; }); this.closeWord = ''; this.guessElem.innerHTML = ''; this.renderGuesses(this.possibleWords, inputElem); } renderGuesses(possibleWords, inputElem) { possibleWords.slice(0, 100).forEach((word, index) => { const wordElem = document.createElement('div'); wordElem.textContent = word; wordElem.style = 'font-weight: bold; display: inline-block; padding: 5px; margin-right: 2px; color: white; text-shadow: 2px 2px 2px black;'; const maxValue = possibleWords.length > 100 ? 100 : possibleWords.length; let hueValue = possibleWords.length > 1 ? Math.floor(360 * index / (maxValue - 1)) : 0; wordElem.style.backgroundColor = `hsl(${hueValue}, 100%, 50%)`; this.addHoverEffect(wordElem, hueValue); this.addClickFunctionality(wordElem, word, inputElem, hueValue); this.guessElem.appendChild(wordElem); }); } addHoverEffect(wordElem, hueValue) { wordElem.addEventListener('mouseenter', function() { if (!this.classList.contains('pressed')) { this.style.backgroundColor = 'lightgray'; } this.classList.add('hovered'); }); wordElem.addEventListener('mouseleave', function() { if (!this.classList.contains('pressed')) { this.style.backgroundColor = `hsl(${hueValue}, 100%, 50%)`; } this.classList.remove('hovered'); }); } addClickFunctionality(wordElem, word, inputElem, colorValue) { wordElem.addEventListener('mousedown', function() { wordElem.classList.add('pressed'); wordElem.style.backgroundColor = 'gray'; }); wordElem.addEventListener('mouseup', function() { wordElem.classList.remove('pressed'); if (!wordElem.classList.contains('hovered')) { wordElem.style.backgroundColor = `rgb(${colorValue}, ${255 - colorValue}, 0)`; } else { wordElem.style.backgroundColor = 'lightgray'; } }); wordElem.addEventListener('click', function() { const formElem = document.querySelector('#game-chat form'); inputElem.value = word; formElem.dispatchEvent(new Event('submit', { bubbles: true, cancelable: true })); }); } } new WordSleuth(); })();