diff --git a/Extension/Script-manager.js b/Extension/Script-manager.js index 3c3c391e..a7c33255 100644 --- a/Extension/Script-manager.js +++ b/Extension/Script-manager.js @@ -1,642 +1,1285 @@ -// ==UserScript== -// @name WPlace AutoBOT Script Manager -// @namespace http://tampermonkey.net/ -// @version 2025-09-08.1 -// @description Script manager and launcher for WPlace AutoBOT -// @author TH3C0D3R -// @match https://wplace.live/* -// @grant none -// @icon -// ==/UserScript==l - -; (async () => { - console.log('%c๐Ÿš€ WPlace AutoBOT Script Manager Loading...', 'color: #00ff41; font-weight: bold; font-size: 16px;'); - - // Available scripts configuration - const AVAILABLE_SCRIPTS = [ - { - name: 'Auto-Farm.js', - displayName: '๐ŸŒพ Auto Farm', - description: 'Automated farming and pixel painting', - icon: '๐ŸŒพ', - category: 'automation' - }, - { - name: 'Auto-Image.js', - displayName: '๐Ÿ–ผ๏ธ Auto Image', - description: 'Automated image processing and placement', - icon: '๐Ÿ–ผ๏ธ', - category: 'automation' - }, - { - name: 'Auto-Repair.js', - displayName: '๐Ÿ”ง Auto Repair', - description: 'Automated repair and maintenance tasks', - icon: '๐Ÿ”ง', - category: 'utility' - }, - { - name: 'Art-Extractor.js', - displayName: '๐ŸŽจ Art Extractor', - description: 'Extract artwork areas to JSON for auto-repair', - icon: '๐ŸŽจ', - category: 'utility' - } - ]; // Neon theme styling - const NEON_STYLES = ` - @import url('https://fonts.googleapis.com/css2?family=Press+Start+2P&display=swap'); - - .script-manager-container { - position: fixed !important; - top: 50% !important; - left: 50% !important; - transform: translate(-50%, -50%) !important; - background: #1a1a2e; - border: 3px solid #00ff41; - border-radius: 0; - box-shadow: - 0 0 30px rgb(0 255 65 / 50%), - inset 0 0 30px rgb(0 255 65 / 10%), - 0 0 0 1px #00ff41; - font-family: 'Press Start 2P', monospace, 'Courier New'; - z-index: 10001 !important; - min-width: 600px; - max-width: 800px; - max-height: 80vh; - overflow: hidden; - color: #00ff41; - animation: neon-pulse 2s ease-in-out infinite alternate; - } - - .script-manager-container::before { - content: ''; - position: absolute; - top: 0; - left: 0; - right: 0; - height: 2px; - background: linear-gradient(90deg, transparent, #00ff41, transparent); - z-index: 1; - pointer-events: none; - animation: scanline 3s linear infinite; - opacity: 0.7; - } - - .script-manager-header { - background: #16213e; - border-bottom: 2px solid #00ff41; - padding: 15px 20px; - position: relative; - display: flex; - justify-content: space-between; - align-items: center; - } - - .header-content { - display: flex; - align-items: center; - gap: 15px; - } - - .header-icon { - width: 32px; - height: 32px; - border-radius: 6px; - box-shadow: 0 0 15px rgba(0, 255, 65, 0.4); - transition: all 0.3s ease; - animation: pixel-blink 3s infinite; - } - - .header-icon:hover { - transform: scale(1.1); - box-shadow: 0 0 25px rgba(0, 255, 65, 0.6); - } - - .script-manager-title { - color: #00ff41; - font-size: 14px; - text-shadow: 0 0 15px #00ff41; - margin: 0; - text-transform: uppercase; - letter-spacing: 2px; - animation: text-glow 2s ease-in-out infinite alternate; - } - - .script-manager-close { - background: #16213e; - border: 2px solid #ff073a; - border-radius: 0; - color: #ff073a; - width: 30px; - height: 30px; - cursor: pointer; - font-family: 'Press Start 2P', monospace; - font-size: 12px; - display: flex; - align-items: center; - justify-content: center; - transition: all 0.3s ease; - text-shadow: 0 0 10px #ff073a; - } - - .script-manager-close:hover { - background: #ff073a; - color: #1a1a2e; - box-shadow: 0 0 20px #ff073a; - animation: pixel-blink 0.5s infinite; - } - - .script-manager-content { - padding: 20px; - max-height: 60vh; - overflow-y: auto; - background: linear-gradient(45deg, rgba(0,255,65,0.03) 0%, rgba(22,33,62,0.05) 100%); - } - - .script-grid { - display: grid; - grid-template-columns: repeat(auto-fit, minmax(250px, 1fr)); - gap: 15px; - margin-bottom: 20px; - } - - .script-card { - background: #16213e; - border: 2px solid #00ff41; - border-radius: 0; - padding: 15px; - cursor: pointer; - transition: all 0.3s ease; - position: relative; - overflow: hidden; - } - - .script-card::before { - content: ''; - position: absolute; - top: -50%; - left: -50%; - width: 200%; - height: 200%; - background: linear-gradient(45deg, transparent, rgba(0,255,65,0.1), transparent); - transform: rotate(45deg); - transition: all 0.5s ease; - opacity: 0; - } - - .script-card:hover::before { - animation: neon-sweep 1s ease-in-out; - } - - .script-card:hover { - background: rgba(0,255,65,0.1); - box-shadow: - 0 0 25px rgb(0 255 65 / 60%), - inset 0 0 25px rgb(0 255 65 / 15%); - transform: translateY(-3px); - animation: card-glow 0.5s ease-in-out infinite alternate; - } - - .script-card-header { - display: flex; - align-items: center; - margin-bottom: 10px; - } - - .script-icon { - font-size: 24px; - margin-right: 10px; - filter: drop-shadow(0 0 10px #00ff41); - } - - .script-title { - color: #00ff41; - font-size: 11px; - text-shadow: 0 0 10px #00ff41; - text-transform: uppercase; - letter-spacing: 1px; - margin: 0; - } - - .script-description { - color: #00ff41dd; - font-size: 8px; - line-height: 1.4; - text-shadow: 0 0 5px #00ff41; - margin-bottom: 15px; - } - - .script-category { - background: rgba(255, 107, 53, 0.2); - border: 1px solid #ff6b35; - color: #ff6b35; - padding: 3px 8px; - font-size: 7px; - text-transform: uppercase; - letter-spacing: 1px; - text-shadow: 0 0 5px #ff6b35; - display: inline-block; - } - - .script-manager-footer { - background: #16213e; - border-top: 2px solid #00ff41; - padding: 15px 20px; - display: flex; - justify-content: space-between; - align-items: center; - } - - .status-text { - color: #00ff41dd; - font-size: 8px; - text-shadow: 0 0 5px #00ff41; - } - - .action-buttons { - display: flex; - gap: 10px; - } - - .neon-btn { - background: #16213e; - border: 2px solid #00ff41; - border-radius: 0; - color: #00ff41; - padding: 8px 15px; - font-family: 'Press Start 2P', monospace; - font-size: 8px; - text-transform: uppercase; - cursor: pointer; - transition: all 0.3s ease; - text-shadow: 0 0 8px #00ff41; - letter-spacing: 1px; - } - - .neon-btn:hover { - background: rgba(0,255,65,0.1); - box-shadow: 0 0 20px rgb(0 255 65 / 60%); - animation: pixel-blink 0.5s infinite; - } - - .neon-btn.secondary { - border-color: #ff6b35; - color: #ff6b35; - text-shadow: 0 0 8px #ff6b35; - } - - .neon-btn.secondary:hover { - background: rgba(255, 107, 53, 0.1); - box-shadow: 0 0 20px rgb(255 107 53 / 60%); - } - - .script-manager-backdrop { - position: fixed !important; - top: 0 !important; - left: 0 !important; - width: 100% !important; - height: 100% !important; - background: rgba(0, 0, 0, 0.8); - z-index: 10000 !important; - backdrop-filter: blur(5px); - animation: backdrop-fade-in 0.3s ease-out; - } - - /* Loading animation */ - .loading-container { - display: flex; - flex-direction: column; - align-items: center; - justify-content: center; - height: 200px; - } - - .loading-spinner { - width: 40px; - height: 40px; - border: 3px solid #16213e; - border-top: 3px solid #00ff41; - border-radius: 0; - animation: neon-spin 1s linear infinite; - margin-bottom: 15px; - box-shadow: 0 0 20px rgb(0 255 65 / 50%); - } - - .loading-text { - color: #00ff41; - font-size: 8px; - text-shadow: 0 0 10px #00ff41; - text-transform: uppercase; - letter-spacing: 2px; - animation: text-pulse 1.5s ease-in-out infinite; - } - - /* Custom scrollbar */ - .script-manager-content::-webkit-scrollbar { - width: 12px; - } - - .script-manager-content::-webkit-scrollbar-track { - background: #16213e; - border: 1px solid #00ff41; - } - - .script-manager-content::-webkit-scrollbar-thumb { - background: #00ff41; - border-radius: 0; - box-shadow: 0 0 10px #00ff41; - } - - .script-manager-content::-webkit-scrollbar-thumb:hover { - background: #39ff14; - box-shadow: 0 0 15px #39ff14; - } - - /* Animations */ - @keyframes neon-pulse { - 0% { box-shadow: 0 0 30px rgb(0 255 65 / 50%), inset 0 0 30px rgb(0 255 65 / 10%), 0 0 0 1px #00ff41; } - 100% { box-shadow: 0 0 40px rgb(0 255 65 / 70%), inset 0 0 40px rgb(0 255 65 / 15%), 0 0 0 1px #00ff41; } - } - - @keyframes text-glow { - 0% { text-shadow: 0 0 15px #00ff41; } - 100% { text-shadow: 0 0 25px #00ff41, 0 0 35px #00ff41; } - } - - @keyframes pixel-blink { - 0%, 50% { opacity: 1; } - 51%, 100% { opacity: 0.7; } - } - - @keyframes scanline { - 0% { transform: translateY(-100%); } - 100% { transform: translateY(400px); } - } - - @keyframes neon-sweep { - 0% { opacity: 0; transform: translateX(-100%) translateY(-100%) rotate(45deg); } - 50% { opacity: 1; } - 100% { opacity: 0; transform: translateX(100%) translateY(100%) rotate(45deg); } - } - - @keyframes card-glow { - 0% { box-shadow: 0 0 25px rgb(0 255 65 / 60%), inset 0 0 25px rgb(0 255 65 / 15%); } - 100% { box-shadow: 0 0 35px rgb(0 255 65 / 80%), inset 0 0 35px rgb(0 255 65 / 20%); } - } - - @keyframes neon-spin { - 0% { transform: rotate(0deg); } - 100% { transform: rotate(360deg); } - } - - @keyframes text-pulse { - 0%, 100% { opacity: 1; } - 50% { opacity: 0.6; } - } - - @keyframes backdrop-fade-in { - 0% { opacity: 0; } - 100% { opacity: 1; } - } - - /* Responsive design */ - @media (max-width: 768px) { - .script-manager-container { - min-width: 90vw; - max-width: 95vw; - } - - .script-grid { - grid-template-columns: 1fr; - } - - .script-manager-title { - font-size: 10px; - } - } - `; - - // Execute script function - Fixed to work like extension popup - async function executeScript(scriptName) { - console.group(`%c๐Ÿš€ Executing ${scriptName}`, 'color: #00ff41; font-weight: bold;'); - - try { - // Show loading in the UI - showLoading(`Launching ${scriptName}...`); - - // The Script Manager runs in MAIN world context and doesn't have direct Chrome API access - // Instead, we need to communicate back to the content script which can use Chrome APIs - console.log('%c๐Ÿ”„ Script Manager context - delegating to content script', 'color: #ff6b35;'); - - // Create a custom event to communicate with the content script - const executeEvent = new CustomEvent('autobot-execute-script', { - detail: { scriptName: scriptName } - }); - - // Dispatch the event to the content script - window.dispatchEvent(executeEvent); - - // Show success immediately since we're delegating - console.log(`%cโœ… ${scriptName} execution delegated to content script`, 'color: #39ff14; font-weight: bold;'); - showSuccess(`${scriptName} execution started!`); - - // Auto-close after success - setTimeout(() => { - closeScriptManager(); - }, 1500); - - } catch (error) { - console.error(`%cโŒ Failed to execute ${scriptName}:`, 'color: #ff073a; font-weight: bold;', error); - showError(`Failed to launch ${scriptName}: ${error.message}`); - } finally { - console.groupEnd(); - } - } - - // UI Management functions - function showLoading(message) { - const container = document.getElementById('script-manager-content'); - if (!container) return; - - container.innerHTML = ` -
-
-
${message}
-
- `; - } - - function showSuccess(message) { - const statusText = document.querySelector('.status-text'); - if (statusText) { - statusText.textContent = `โœ… ${message}`; - statusText.style.color = '#39ff14'; - statusText.style.textShadow = '0 0 10px #39ff14'; - } - } - - function showError(message) { - const statusText = document.querySelector('.status-text'); - if (statusText) { - statusText.textContent = `โŒ ${message}`; - statusText.style.color = '#ff073a'; - statusText.style.textShadow = '0 0 10px #ff073a'; - } - - // Reset the content to show scripts again - setTimeout(() => { - renderScripts(); - }, 3000); - } - - function renderScripts() { - const container = document.getElementById('script-manager-content'); - if (!container) return; - - const scriptGrid = AVAILABLE_SCRIPTS.map(script => ` -
-
-
${script.icon}
-

${script.displayName}

-
-

${script.description}

- ${script.category} -
- `).join(''); - - container.innerHTML = ` -
- ${scriptGrid} -
- `; - } - - // Close script manager - function closeScriptManager() { - const container = document.getElementById('script-manager-container'); - const backdrop = document.getElementById('script-manager-backdrop'); - - if (container) { - container.style.animation = 'neon-fade-out 0.3s ease-in forwards'; - setTimeout(() => { - container.remove(); - }, 300); - } - - if (backdrop) { - backdrop.style.animation = 'backdrop-fade-out 0.3s ease-in forwards'; - setTimeout(() => { - backdrop.remove(); - }, 300); - } - - // Remove ESC key listener - document.removeEventListener('keydown', handleEscKey); - - console.log('%c๐Ÿ‘‹ Script Manager closed', 'color: #ff6b35;'); - } - - // ESC key handler - function handleEscKey(event) { - if (event.key === 'Escape') { - closeScriptManager(); - } - } - - // Main function to show script manager - function showScriptManager() { - // Remove any existing manager - const existing = document.getElementById('script-manager-container'); - if (existing) existing.remove(); - - const existingBackdrop = document.getElementById('script-manager-backdrop'); - if (existingBackdrop) existingBackdrop.remove(); - - console.log('%c๐ŸŽฎ Opening Script Manager with Neon Theme', 'color: #00ff41; font-weight: bold;'); - - // Get icon URL for display - let iconUrl = ''; - try { - if (chrome && chrome.runtime && chrome.runtime.getURL) { - iconUrl = chrome.runtime.getURL('icons/icon32.png'); - console.log('๐Ÿ“ท Icon URL:', iconUrl); - } - } catch (e) { - console.log('Extension context not available for icon'); - } - - // Inject styles - if (!document.getElementById('script-manager-styles')) { - const styleElement = document.createElement('style'); - styleElement.id = 'script-manager-styles'; - styleElement.textContent = NEON_STYLES; - document.head.appendChild(styleElement); - } - - // Create backdrop - const backdrop = document.createElement('div'); - backdrop.id = 'script-manager-backdrop'; - backdrop.className = 'script-manager-backdrop'; - backdrop.addEventListener('click', closeScriptManager); - - // Create container - const container = document.createElement('div'); - container.id = 'script-manager-container'; - container.className = 'script-manager-container'; - - container.innerHTML = ` -
-
- ${iconUrl ? `AutoBOT` : ''} -

โšก WPlace AutoBOT Script Manager โšก

-
- -
-
- -
- - `; - - // Add to page - document.body.appendChild(backdrop); - document.body.appendChild(container); - - // Debug: Check positioning - console.log('%c๐Ÿ” Script Manager Positioning Debug:', 'color: #ff6b35; font-weight: bold;'); - console.log(` - Container position: ${getComputedStyle(container).position}`); - console.log(` - Container top: ${getComputedStyle(container).top}`); - console.log(` - Container left: ${getComputedStyle(container).left}`); - console.log(` - Container transform: ${getComputedStyle(container).transform}`); - console.log(` - Container z-index: ${getComputedStyle(container).zIndex}`); - console.log(` - Backdrop z-index: ${getComputedStyle(backdrop).zIndex}`); - - // Render scripts - renderScripts(); - - // Add ESC key listener - document.addEventListener('keydown', handleEscKey); - - // Focus container for accessibility - container.focus(); - - console.log('%cโœ… Script Manager opened successfully', 'color: #39ff14; font-weight: bold;'); - } - - // Make functions globally available - window.executeScript = executeScript; - window.closeScriptManager = closeScriptManager; - window.showScriptManager = showScriptManager; - - // Auto-start the script manager - console.log('%c๐ŸŽฏ Auto-launching Script Manager...', 'color: #00ff41; font-weight: bold;'); - showScriptManager(); - - console.log('%c๐Ÿš€ WPlace AutoBOT Script Manager Ready!', 'color: #39ff14; font-weight: bold; font-size: 16px;'); -})(); +// ==UserScript== +// @name WPlace AutoBOT Script Manager +// @namespace http://tampermonkey.net/ +// @version 2025-09-08.1 +// @description Script manager and launcher for WPlace AutoBOT +// @author TH3C0D3R +// @match https://wplace.live/* +// @grant none +// @icon +// ==/UserScript==l + +; (async () => { + console.log('%c๐Ÿš€ WPlace AutoBOT Script Manager Loading...', 'color: #00ff41; font-weight: bold; font-size: 16px;'); + + // Available scripts configuration + const AVAILABLE_SCRIPTS = [ + { + name: 'Auto-Farm.js', + displayName: '๐ŸŒพ Auto Farm', + description: 'Automated farming and pixel painting', + icon: '๐ŸŒพ', + category: 'automation' + }, + { + name: 'Auto-Image.js', + displayName: '๐Ÿ–ผ๏ธ Auto Image', + description: 'Automated image processing and placement', + icon: '๐Ÿ–ผ๏ธ', + category: 'automation' + }, + { + name: 'Auto-Repair.js', + displayName: '๐Ÿ”ง Auto Repair', + description: 'Automated repair and maintenance tasks', + icon: '๐Ÿ”ง', + category: 'utility' + }, + { + name: 'Art-Extractor.js', + displayName: '๐ŸŽจ Art Extractor', + description: 'Extract artwork areas to JSON for auto-repair', + icon: '๐ŸŽจ', + category: 'utility' + } + ]; // Neon theme styling + const NEON_STYLES = ` + @import url('https://fonts.googleapis.com/css2?family=Press+Start+2P&display=swap'); + + .script-manager-container { + position: fixed !important; + top: 50% !important; + left: 50% !important; + transform: translate(-50%, -50%) !important; + background: #1a1a2e; + border: 3px solid #00ff41; + border-radius: 0; + box-shadow: + 0 0 30px rgb(0 255 65 / 50%), + inset 0 0 30px rgb(0 255 65 / 10%), + 0 0 0 1px #00ff41; + font-family: 'Press Start 2P', monospace, 'Courier New'; + z-index: 10001 !important; + min-width: 600px; + max-width: 800px; + max-height: 80vh; + overflow: hidden; + color: #00ff41; + animation: neon-pulse 2s ease-in-out infinite alternate; + } + + .script-manager-container::before { + content: ''; + position: absolute; + top: 0; + left: 0; + right: 0; + height: 2px; + background: linear-gradient(90deg, transparent, #00ff41, transparent); + z-index: 1; + pointer-events: none; + animation: scanline 3s linear infinite; + opacity: 0.7; + } + + .script-manager-header { + background: #16213e; + border-bottom: 2px solid #00ff41; + padding: 15px 20px; + position: relative; + display: flex; + justify-content: space-between; + align-items: center; + } + + .header-content { + display: flex; + align-items: center; + gap: 15px; + } + + .header-icon { + width: 32px; + height: 32px; + border-radius: 6px; + box-shadow: 0 0 15px rgba(0, 255, 65, 0.4); + transition: all 0.3s ease; + animation: pixel-blink 3s infinite; + } + + .header-icon:hover { + transform: scale(1.1); + box-shadow: 0 0 25px rgba(0, 255, 65, 0.6); + } + + .script-manager-title { + color: #00ff41; + font-size: 14px; + text-shadow: 0 0 15px #00ff41; + margin: 0; + text-transform: uppercase; + letter-spacing: 2px; + animation: text-glow 2s ease-in-out infinite alternate; + } + + .script-manager-close { + background: #16213e; + border: 2px solid #ff073a; + border-radius: 0; + color: #ff073a; + width: 30px; + height: 30px; + cursor: pointer; + font-family: 'Press Start 2P', monospace; + font-size: 12px; + display: flex; + align-items: center; + justify-content: center; + transition: all 0.3s ease; + text-shadow: 0 0 10px #ff073a; + } + + .script-manager-close:hover { + background: #ff073a; + color: #1a1a2e; + box-shadow: 0 0 20px #ff073a; + animation: pixel-blink 0.5s infinite; + } + + .script-manager-content { + padding: 20px; + max-height: 60vh; + overflow-y: auto; + background: linear-gradient(45deg, rgba(0,255,65,0.03) 0%, rgba(22,33,62,0.05) 100%); + } + + .script-grid { + display: grid; + grid-template-columns: repeat(auto-fit, minmax(250px, 1fr)); + gap: 15px; + margin-bottom: 20px; + } + + .script-card { + background: #16213e; + border: 2px solid #00ff41; + border-radius: 0; + padding: 15px; + cursor: pointer; + transition: all 0.3s ease; + position: relative; + overflow: hidden; + } + + .script-card::before { + content: ''; + position: absolute; + top: -50%; + left: -50%; + width: 200%; + height: 200%; + background: linear-gradient(45deg, transparent, rgba(0,255,65,0.1), transparent); + transform: rotate(45deg); + transition: all 0.5s ease; + opacity: 0; + } + + .script-card:hover::before { + animation: neon-sweep 1s ease-in-out; + } + + .script-card:hover { + background: rgba(0,255,65,0.1); + box-shadow: + 0 0 25px rgb(0 255 65 / 60%), + inset 0 0 25px rgb(0 255 65 / 15%); + transform: translateY(-3px); + animation: card-glow 0.5s ease-in-out infinite alternate; + } + + .script-card-header { + display: flex; + align-items: center; + margin-bottom: 10px; + } + + .script-icon { + font-size: 24px; + margin-right: 10px; + filter: drop-shadow(0 0 10px #00ff41); + } + + .script-title { + color: #00ff41; + font-size: 11px; + text-shadow: 0 0 10px #00ff41; + text-transform: uppercase; + letter-spacing: 1px; + margin: 0; + } + + .script-description { + color: #00ff41dd; + font-size: 8px; + line-height: 1.4; + text-shadow: 0 0 5px #00ff41; + margin-bottom: 15px; + } + + .script-category { + background: rgba(255, 107, 53, 0.2); + border: 1px solid #ff6b35; + color: #ff6b35; + padding: 3px 8px; + font-size: 7px; + text-transform: uppercase; + letter-spacing: 1px; + text-shadow: 0 0 5px #ff6b35; + display: inline-block; + } + + .script-manager-footer { + background: #16213e; + border-top: 2px solid #00ff41; + padding: 15px 20px; + display: flex; + justify-content: space-between; + align-items: center; + } + + .status-text { + color: #00ff41dd; + font-size: 8px; + text-shadow: 0 0 5px #00ff41; + } + + .action-buttons { + display: flex; + gap: 10px; + } + + .neon-btn { + background: #16213e; + border: 2px solid #00ff41; + border-radius: 0; + color: #00ff41; + padding: 8px 15px; + font-family: 'Press Start 2P', monospace; + font-size: 8px; + text-transform: uppercase; + cursor: pointer; + transition: all 0.3s ease; + text-shadow: 0 0 8px #00ff41; + letter-spacing: 1px; + } + + .neon-btn:hover { + background: rgba(0,255,65,0.1); + box-shadow: 0 0 20px rgb(0 255 65 / 60%); + animation: pixel-blink 0.5s infinite; + } + + .neon-btn.secondary { + border-color: #ff6b35; + color: #ff6b35; + text-shadow: 0 0 8px #ff6b35; + } + + .neon-btn.secondary:hover { + background: rgba(255, 107, 53, 0.1); + box-shadow: 0 0 20px rgb(255 107 53 / 60%); + } + + .script-manager-backdrop { + position: fixed !important; + top: 0 !important; + left: 0 !important; + width: 100% !important; + height: 100% !important; + background: rgba(0, 0, 0, 0.8); + z-index: 10000 !important; + backdrop-filter: blur(5px); + animation: backdrop-fade-in 0.3s ease-out; + } + + /* Loading animation */ + .loading-container { + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + height: 200px; + } + + .loading-spinner { + width: 40px; + height: 40px; + border: 3px solid #16213e; + border-top: 3px solid #00ff41; + border-radius: 0; + animation: neon-spin 1s linear infinite; + margin-bottom: 15px; + box-shadow: 0 0 20px rgb(0 255 65 / 50%); + } + + .loading-text { + color: #00ff41; + font-size: 8px; + text-shadow: 0 0 10px #00ff41; + text-transform: uppercase; + letter-spacing: 2px; + animation: text-pulse 1.5s ease-in-out infinite; + } + + /* Custom scrollbar */ + .script-manager-content::-webkit-scrollbar { + width: 12px; + } + + .script-manager-content::-webkit-scrollbar-track { + background: #16213e; + border: 1px solid #00ff41; + } + + .script-manager-content::-webkit-scrollbar-thumb { + background: #00ff41; + border-radius: 0; + box-shadow: 0 0 10px #00ff41; + } + + .script-manager-content::-webkit-scrollbar-thumb:hover { + background: #39ff14; + box-shadow: 0 0 15px #39ff14; + } + + /* Animations */ + @keyframes neon-pulse { + 0% { box-shadow: 0 0 30px rgb(0 255 65 / 50%), inset 0 0 30px rgb(0 255 65 / 10%), 0 0 0 1px #00ff41; } + 100% { box-shadow: 0 0 40px rgb(0 255 65 / 70%), inset 0 0 40px rgb(0 255 65 / 15%), 0 0 0 1px #00ff41; } + } + + @keyframes text-glow { + 0% { text-shadow: 0 0 15px #00ff41; } + 100% { text-shadow: 0 0 25px #00ff41, 0 0 35px #00ff41; } + } + + @keyframes pixel-blink { + 0%, 50% { opacity: 1; } + 51%, 100% { opacity: 0.7; } + } + + @keyframes scanline { + 0% { transform: translateY(-100%); } + 100% { transform: translateY(400px); } + } + + @keyframes neon-sweep { + 0% { opacity: 0; transform: translateX(-100%) translateY(-100%) rotate(45deg); } + 50% { opacity: 1; } + 100% { opacity: 0; transform: translateX(100%) translateY(100%) rotate(45deg); } + } + + @keyframes card-glow { + 0% { box-shadow: 0 0 25px rgb(0 255 65 / 60%), inset 0 0 25px rgb(0 255 65 / 15%); } + 100% { box-shadow: 0 0 35px rgb(0 255 65 / 80%), inset 0 0 35px rgb(0 255 65 / 20%); } + } + + @keyframes neon-spin { + 0% { transform: rotate(0deg); } + 100% { transform: rotate(360deg); } + } + + @keyframes text-pulse { + 0%, 100% { opacity: 1; } + 50% { opacity: 0.6; } + } + + @keyframes backdrop-fade-in { + 0% { opacity: 0; } + 100% { opacity: 1; } + } + + /* Responsive design */ + @media (max-width: 768px) { + .script-manager-container { + min-width: 90vw; + max-width: 95vw; + } + + .script-grid { + grid-template-columns: 1fr; + } + + .script-manager-title { + font-size: 10px; + } + } + `; + + // Execute script function - Fixed to work like extension popup + async function executeScript(scriptName) { + console.group(`%c๐Ÿš€ Executing ${scriptName}`, 'color: #00ff41; font-weight: bold;'); + + try { + // Show loading in the UI + showLoading(`Launching ${scriptName}...`); + + // The Script Manager runs in MAIN world context and doesn't have direct Chrome API access + // Instead, we need to communicate back to the content script which can use Chrome APIs + console.log('%c๐Ÿ”„ Script Manager context - delegating to content script', 'color: #ff6b35;'); + + // Create a custom event to communicate with the content script + const executeEvent = new CustomEvent('autobot-execute-script', { + detail: { scriptName: scriptName } + }); + + // Dispatch the event to the content script + window.dispatchEvent(executeEvent); + + // Show success immediately since we're delegating + console.log(`%cโœ… ${scriptName} execution delegated to content script`, 'color: #39ff14; font-weight: bold;'); + showSuccess(`${scriptName} execution started!`); + + // Auto-close after success + setTimeout(() => { + closeScriptManager(); + }, 1500); + + } catch (error) { + console.error(`%cโŒ Failed to execute ${scriptName}:`, 'color: #ff073a; font-weight: bold;', error); + showError(`Failed to launch ${scriptName}: ${error.message}`); + } finally { + console.groupEnd(); + } + } + + // UI Management functions + function showLoading(message) { + const container = document.getElementById('script-manager-content'); + if (!container) return; + + container.innerHTML = ` +
+
+
${message}
+
+ `; + } + + function showSuccess(message) { + const statusText = document.querySelector('.status-text'); + if (statusText) { + statusText.textContent = `โœ… ${message}`; + statusText.style.color = '#39ff14'; + statusText.style.textShadow = '0 0 10px #39ff14'; + } + } + + function showError(message) { + const statusText = document.querySelector('.status-text'); + if (statusText) { + statusText.textContent = `โŒ ${message}`; + statusText.style.color = '#ff073a'; + statusText.style.textShadow = '0 0 10px #ff073a'; + } + + // Reset the content to show scripts again + setTimeout(() => { + renderScripts(); + }, 3000); + } + + function renderScripts() { + const container = document.getElementById('script-manager-content'); + if (!container) return; + + const scriptGrid = AVAILABLE_SCRIPTS.map(script => ` +
+
+
${script.icon}
+

${script.displayName}

+
+

${script.description}

+ ${script.category} +
+ `).join(''); + + container.innerHTML = ` +
+ ${scriptGrid} +
+ `; + } + + // Close script manager + function closeScriptManager() { + const container = document.getElementById('script-manager-container'); + const backdrop = document.getElementById('script-manager-backdrop'); + + if (container) { + container.style.animation = 'neon-fade-out 0.3s ease-in forwards'; + setTimeout(() => { + container.remove(); + }, 300); + } + + if (backdrop) { + backdrop.style.animation = 'backdrop-fade-out 0.3s ease-in forwards'; + setTimeout(() => { + backdrop.remove(); + }, 300); + } + + // Remove ESC key listener + document.removeEventListener('keydown', handleEscKey); + + console.log('%c๐Ÿ‘‹ Script Manager closed', 'color: #ff6b35;'); + } + + // ESC key handler + function handleEscKey(event) { + if (event.key === 'Escape') { + closeScriptManager(); + } + } + + // Main function to show script manager + function showScriptManager() { + // Remove any existing manager + const existing = document.getElementById('script-manager-container'); + if (existing) existing.remove(); + + const existingBackdrop = document.getElementById('script-manager-backdrop'); + if (existingBackdrop) existingBackdrop.remove(); + + console.log('%c๐ŸŽฎ Opening Script Manager with Neon Theme', 'color: #00ff41; font-weight: bold;'); + + // Get icon URL for display + let iconUrl = ''; + try { + if (chrome && chrome.runtime && chrome.runtime.getURL) { + iconUrl = chrome.runtime.getURL('icons/icon32.png'); + console.log('๐Ÿ“ท Icon URL:', iconUrl); + } + } catch (e) { + console.log('Extension context not available for icon'); + } + + // Inject styles + if (!document.getElementById('script-manager-styles')) { + const styleElement = document.createElement('style'); + styleElement.id = 'script-manager-styles'; + styleElement.textContent = NEON_STYLES; + document.head.appendChild(styleElement); + } + + // Create backdrop + const backdrop = document.createElement('div'); + backdrop.id = 'script-manager-backdrop'; + backdrop.className = 'script-manager-backdrop'; + backdrop.addEventListener('click', closeScriptManager); + + // Create container + const container = document.createElement('div'); + container.id = 'script-manager-container'; + container.className = 'script-manager-container'; + + container.innerHTML = ` +
+
+ ${iconUrl ? `AutoBOT` : ''} +

โšก WPlace AutoBOT Script Manager โšก

+
+ +
+
+ +
+ + `; + + // Add to page + document.body.appendChild(backdrop); + document.body.appendChild(container); + + // Debug: Check positioning + console.log('%c๐Ÿ” Script Manager Positioning Debug:', 'color: #ff6b35; font-weight: bold;'); + console.log(` - Container position: ${getComputedStyle(container).position}`); + console.log(` - Container top: ${getComputedStyle(container).top}`); + console.log(` - Container left: ${getComputedStyle(container).left}`); + console.log(` - Container transform: ${getComputedStyle(container).transform}`); + console.log(` - Container z-index: ${getComputedStyle(container).zIndex}`); + console.log(` - Backdrop z-index: ${getComputedStyle(backdrop).zIndex}`); + + // Render scripts + renderScripts(); + + // Add ESC key listener + document.addEventListener('keydown', handleEscKey); + + // Focus container for accessibility + container.focus(); + + console.log('%cโœ… Script Manager opened successfully', 'color: #39ff14; font-weight: bold;'); + } + + // Make functions globally available + window.executeScript = executeScript; + window.closeScriptManager = closeScriptManager; + window.showScriptManager = showScriptManager; + + // Auto-start the script manager + console.log('%c๐ŸŽฏ Auto-launching Script Manager...', 'color: #00ff41; font-weight: bold;'); + showScriptManager(); + + console.log('%c๐Ÿš€ WPlace AutoBOT Script Manager Ready!', 'color: #39ff14; font-weight: bold; font-size: 16px;'); +})(); +======= +// ==UserScript== +// @name WPlace AutoBOT Script Manager +// @namespace http://tampermonkey.net/ +// @version 2025-09-08.1 +// @description Script manager and launcher for WPlace AutoBOT +// @author TH3C0D3R +// @match https://wplace.live/* +// @grant none +// @icon +// ==/UserScript==l + +; (async () => { + console.log('%c๐Ÿš€ WPlace AutoBOT Script Manager Loading...', 'color: #00ff41; font-weight: bold; font-size: 16px;'); + + // Available scripts configuration + const AVAILABLE_SCRIPTS = [ + { + name: 'Auto-Farm.js', + displayName: '๐ŸŒพ Auto Farm', + description: 'Automated farming and pixel painting', + icon: '๐ŸŒพ', + category: 'automation' + }, + { + name: 'Auto-Image.js', + displayName: '๐Ÿ–ผ๏ธ Auto Image', + description: 'Automated image processing and placement', + icon: '๐Ÿ–ผ๏ธ', + category: 'automation' + }, + { + name: 'Auto-Repair.js', + displayName: '๐Ÿ”ง Auto Repair', + description: 'Automated repair and maintenance tasks', + icon: '๐Ÿ”ง', + category: 'utility' + }, + { + name: 'Art-Extractor.js', + displayName: '๐ŸŽจ Art Extractor', + description: 'Extract artwork areas to JSON for auto-repair', + icon: '๐ŸŽจ', + category: 'utility' + } + ]; // Neon theme styling + const NEON_STYLES = ` + @import url('https://fonts.googleapis.com/css2?family=Press+Start+2P&display=swap'); + + .script-manager-container { + position: fixed !important; + top: 50% !important; + left: 50% !important; + transform: translate(-50%, -50%) !important; + background: #1a1a2e; + border: 3px solid #00ff41; + border-radius: 0; + box-shadow: + 0 0 30px rgb(0 255 65 / 50%), + inset 0 0 30px rgb(0 255 65 / 10%), + 0 0 0 1px #00ff41; + font-family: 'Press Start 2P', monospace, 'Courier New'; + z-index: 10001 !important; + min-width: 600px; + max-width: 800px; + max-height: 80vh; + overflow: hidden; + color: #00ff41; + animation: neon-pulse 2s ease-in-out infinite alternate; + } + + .script-manager-container::before { + content: ''; + position: absolute; + top: 0; + left: 0; + right: 0; + height: 2px; + background: linear-gradient(90deg, transparent, #00ff41, transparent); + z-index: 1; + pointer-events: none; + animation: scanline 3s linear infinite; + opacity: 0.7; + } + + .script-manager-header { + background: #16213e; + border-bottom: 2px solid #00ff41; + padding: 15px 20px; + position: relative; + display: flex; + justify-content: space-between; + align-items: center; + } + + .header-content { + display: flex; + align-items: center; + gap: 15px; + } + + .header-icon { + width: 32px; + height: 32px; + border-radius: 6px; + box-shadow: 0 0 15px rgba(0, 255, 65, 0.4); + transition: all 0.3s ease; + animation: pixel-blink 3s infinite; + } + + .header-icon:hover { + transform: scale(1.1); + box-shadow: 0 0 25px rgba(0, 255, 65, 0.6); + } + + .script-manager-title { + color: #00ff41; + font-size: 14px; + text-shadow: 0 0 15px #00ff41; + margin: 0; + text-transform: uppercase; + letter-spacing: 2px; + animation: text-glow 2s ease-in-out infinite alternate; + } + + .script-manager-close { + background: #16213e; + border: 2px solid #ff073a; + border-radius: 0; + color: #ff073a; + width: 30px; + height: 30px; + cursor: pointer; + font-family: 'Press Start 2P', monospace; + font-size: 12px; + display: flex; + align-items: center; + justify-content: center; + transition: all 0.3s ease; + text-shadow: 0 0 10px #ff073a; + } + + .script-manager-close:hover { + background: #ff073a; + color: #1a1a2e; + box-shadow: 0 0 20px #ff073a; + animation: pixel-blink 0.5s infinite; + } + + .script-manager-content { + padding: 20px; + max-height: 60vh; + overflow-y: auto; + background: linear-gradient(45deg, rgba(0,255,65,0.03) 0%, rgba(22,33,62,0.05) 100%); + } + + .script-grid { + display: grid; + grid-template-columns: repeat(auto-fit, minmax(250px, 1fr)); + gap: 15px; + margin-bottom: 20px; + } + + .script-card { + background: #16213e; + border: 2px solid #00ff41; + border-radius: 0; + padding: 15px; + cursor: pointer; + transition: all 0.3s ease; + position: relative; + overflow: hidden; + } + + .script-card::before { + content: ''; + position: absolute; + top: -50%; + left: -50%; + width: 200%; + height: 200%; + background: linear-gradient(45deg, transparent, rgba(0,255,65,0.1), transparent); + transform: rotate(45deg); + transition: all 0.5s ease; + opacity: 0; + } + + .script-card:hover::before { + animation: neon-sweep 1s ease-in-out; + } + + .script-card:hover { + background: rgba(0,255,65,0.1); + box-shadow: + 0 0 25px rgb(0 255 65 / 60%), + inset 0 0 25px rgb(0 255 65 / 15%); + transform: translateY(-3px); + animation: card-glow 0.5s ease-in-out infinite alternate; + } + + .script-card-header { + display: flex; + align-items: center; + margin-bottom: 10px; + } + + .script-icon { + font-size: 24px; + margin-right: 10px; + filter: drop-shadow(0 0 10px #00ff41); + } + + .script-title { + color: #00ff41; + font-size: 11px; + text-shadow: 0 0 10px #00ff41; + text-transform: uppercase; + letter-spacing: 1px; + margin: 0; + } + + .script-description { + color: #00ff41dd; + font-size: 8px; + line-height: 1.4; + text-shadow: 0 0 5px #00ff41; + margin-bottom: 15px; + } + + .script-category { + background: rgba(255, 107, 53, 0.2); + border: 1px solid #ff6b35; + color: #ff6b35; + padding: 3px 8px; + font-size: 7px; + text-transform: uppercase; + letter-spacing: 1px; + text-shadow: 0 0 5px #ff6b35; + display: inline-block; + } + + .script-manager-footer { + background: #16213e; + border-top: 2px solid #00ff41; + padding: 15px 20px; + display: flex; + justify-content: space-between; + align-items: center; + } + + .status-text { + color: #00ff41dd; + font-size: 8px; + text-shadow: 0 0 5px #00ff41; + } + + .action-buttons { + display: flex; + gap: 10px; + } + + .neon-btn { + background: #16213e; + border: 2px solid #00ff41; + border-radius: 0; + color: #00ff41; + padding: 8px 15px; + font-family: 'Press Start 2P', monospace; + font-size: 8px; + text-transform: uppercase; + cursor: pointer; + transition: all 0.3s ease; + text-shadow: 0 0 8px #00ff41; + letter-spacing: 1px; + } + + .neon-btn:hover { + background: rgba(0,255,65,0.1); + box-shadow: 0 0 20px rgb(0 255 65 / 60%); + animation: pixel-blink 0.5s infinite; + } + + .neon-btn.secondary { + border-color: #ff6b35; + color: #ff6b35; + text-shadow: 0 0 8px #ff6b35; + } + + .neon-btn.secondary:hover { + background: rgba(255, 107, 53, 0.1); + box-shadow: 0 0 20px rgb(255 107 53 / 60%); + } + + .script-manager-backdrop { + position: fixed !important; + top: 0 !important; + left: 0 !important; + width: 100% !important; + height: 100% !important; + background: rgba(0, 0, 0, 0.8); + z-index: 10000 !important; + backdrop-filter: blur(5px); + animation: backdrop-fade-in 0.3s ease-out; + } + + /* Loading animation */ + .loading-container { + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + height: 200px; + } + + .loading-spinner { + width: 40px; + height: 40px; + border: 3px solid #16213e; + border-top: 3px solid #00ff41; + border-radius: 0; + animation: neon-spin 1s linear infinite; + margin-bottom: 15px; + box-shadow: 0 0 20px rgb(0 255 65 / 50%); + } + + .loading-text { + color: #00ff41; + font-size: 8px; + text-shadow: 0 0 10px #00ff41; + text-transform: uppercase; + letter-spacing: 2px; + animation: text-pulse 1.5s ease-in-out infinite; + } + + /* Custom scrollbar */ + .script-manager-content::-webkit-scrollbar { + width: 12px; + } + + .script-manager-content::-webkit-scrollbar-track { + background: #16213e; + border: 1px solid #00ff41; + } + + .script-manager-content::-webkit-scrollbar-thumb { + background: #00ff41; + border-radius: 0; + box-shadow: 0 0 10px #00ff41; + } + + .script-manager-content::-webkit-scrollbar-thumb:hover { + background: #39ff14; + box-shadow: 0 0 15px #39ff14; + } + + /* Animations */ + @keyframes neon-pulse { + 0% { box-shadow: 0 0 30px rgb(0 255 65 / 50%), inset 0 0 30px rgb(0 255 65 / 10%), 0 0 0 1px #00ff41; } + 100% { box-shadow: 0 0 40px rgb(0 255 65 / 70%), inset 0 0 40px rgb(0 255 65 / 15%), 0 0 0 1px #00ff41; } + } + + @keyframes text-glow { + 0% { text-shadow: 0 0 15px #00ff41; } + 100% { text-shadow: 0 0 25px #00ff41, 0 0 35px #00ff41; } + } + + @keyframes pixel-blink { + 0%, 50% { opacity: 1; } + 51%, 100% { opacity: 0.7; } + } + + @keyframes scanline { + 0% { transform: translateY(-100%); } + 100% { transform: translateY(400px); } + } + + @keyframes neon-sweep { + 0% { opacity: 0; transform: translateX(-100%) translateY(-100%) rotate(45deg); } + 50% { opacity: 1; } + 100% { opacity: 0; transform: translateX(100%) translateY(100%) rotate(45deg); } + } + + @keyframes card-glow { + 0% { box-shadow: 0 0 25px rgb(0 255 65 / 60%), inset 0 0 25px rgb(0 255 65 / 15%); } + 100% { box-shadow: 0 0 35px rgb(0 255 65 / 80%), inset 0 0 35px rgb(0 255 65 / 20%); } + } + + @keyframes neon-spin { + 0% { transform: rotate(0deg); } + 100% { transform: rotate(360deg); } + } + + @keyframes text-pulse { + 0%, 100% { opacity: 1; } + 50% { opacity: 0.6; } + } + + @keyframes backdrop-fade-in { + 0% { opacity: 0; } + 100% { opacity: 1; } + } + + /* Responsive design */ + @media (max-width: 768px) { + .script-manager-container { + min-width: 90vw; + max-width: 95vw; + } + + .script-grid { + grid-template-columns: 1fr; + } + + .script-manager-title { + font-size: 10px; + } + } + `; + + // Execute script function - Fixed to work like extension popup + async function executeScript(scriptName) { + console.group(`%c๐Ÿš€ Executing ${scriptName}`, 'color: #00ff41; font-weight: bold;'); + + try { + // Show loading in the UI + showLoading(`Launching ${scriptName}...`); + + // The Script Manager runs in MAIN world context and doesn't have direct Chrome API access + // Instead, we need to communicate back to the content script which can use Chrome APIs + console.log('%c๐Ÿ”„ Script Manager context - delegating to content script', 'color: #ff6b35;'); + + // Create a custom event to communicate with the content script + const executeEvent = new CustomEvent('autobot-execute-script', { + detail: { scriptName: scriptName } + }); + + // Dispatch the event to the content script + window.dispatchEvent(executeEvent); + + // Show success immediately since we're delegating + console.log(`%cโœ… ${scriptName} execution delegated to content script`, 'color: #39ff14; font-weight: bold;'); + showSuccess(`${scriptName} execution started!`); + + // Auto-close after success + setTimeout(() => { + closeScriptManager(); + }, 1500); + + } catch (error) { + console.error(`%cโŒ Failed to execute ${scriptName}:`, 'color: #ff073a; font-weight: bold;', error); + showError(`Failed to launch ${scriptName}: ${error.message}`); + } finally { + console.groupEnd(); + } + } + + // UI Management functions + function showLoading(message) { + const container = document.getElementById('script-manager-content'); + if (!container) return; + + container.innerHTML = ` +
+
+
${message}
+
+ `; + } + + function showSuccess(message) { + const statusText = document.querySelector('.status-text'); + if (statusText) { + statusText.textContent = `โœ… ${message}`; + statusText.style.color = '#39ff14'; + statusText.style.textShadow = '0 0 10px #39ff14'; + } + } + + function showError(message) { + const statusText = document.querySelector('.status-text'); + if (statusText) { + statusText.textContent = `โŒ ${message}`; + statusText.style.color = '#ff073a'; + statusText.style.textShadow = '0 0 10px #ff073a'; + } + + // Reset the content to show scripts again + setTimeout(() => { + renderScripts(); + }, 3000); + } + + function renderScripts() { + const container = document.getElementById('script-manager-content'); + if (!container) return; + + const scriptGrid = AVAILABLE_SCRIPTS.map(script => ` +
+
+
${script.icon}
+

${script.displayName}

+
+

${script.description}

+ ${script.category} +
+ `).join(''); + + container.innerHTML = ` +
+ ${scriptGrid} +
+ `; + } + + // Close script manager + function closeScriptManager() { + const container = document.getElementById('script-manager-container'); + const backdrop = document.getElementById('script-manager-backdrop'); + + if (container) { + container.style.animation = 'neon-fade-out 0.3s ease-in forwards'; + setTimeout(() => { + container.remove(); + }, 300); + } + + if (backdrop) { + backdrop.style.animation = 'backdrop-fade-out 0.3s ease-in forwards'; + setTimeout(() => { + backdrop.remove(); + }, 300); + } + + // Remove ESC key listener + document.removeEventListener('keydown', handleEscKey); + + console.log('%c๐Ÿ‘‹ Script Manager closed', 'color: #ff6b35;'); + } + + // ESC key handler + function handleEscKey(event) { + if (event.key === 'Escape') { + closeScriptManager(); + } + } + + // Main function to show script manager + function showScriptManager() { + // Remove any existing manager + const existing = document.getElementById('script-manager-container'); + if (existing) existing.remove(); + + const existingBackdrop = document.getElementById('script-manager-backdrop'); + if (existingBackdrop) existingBackdrop.remove(); + + console.log('%c๐ŸŽฎ Opening Script Manager with Neon Theme', 'color: #00ff41; font-weight: bold;'); + + // Get icon URL for display + let iconUrl = ''; + try { + if (chrome && chrome.runtime && chrome.runtime.getURL) { + iconUrl = chrome.runtime.getURL('icons/icon32.png'); + console.log('๐Ÿ“ท Icon URL:', iconUrl); + } + } catch (e) { + console.log('Extension context not available for icon'); + } + + // Inject styles + if (!document.getElementById('script-manager-styles')) { + const styleElement = document.createElement('style'); + styleElement.id = 'script-manager-styles'; + styleElement.textContent = NEON_STYLES; + document.head.appendChild(styleElement); + } + + // Create backdrop + const backdrop = document.createElement('div'); + backdrop.id = 'script-manager-backdrop'; + backdrop.className = 'script-manager-backdrop'; + backdrop.addEventListener('click', closeScriptManager); + + // Create container + const container = document.createElement('div'); + container.id = 'script-manager-container'; + container.className = 'script-manager-container'; + + container.innerHTML = ` +
+
+ ${iconUrl ? `AutoBOT` : ''} +

โšก WPlace AutoBOT Script Manager โšก

+
+ +
+
+ +
+ + `; + + // Add to page + document.body.appendChild(backdrop); + document.body.appendChild(container); + + // Debug: Check positioning + console.log('%c๐Ÿ” Script Manager Positioning Debug:', 'color: #ff6b35; font-weight: bold;'); + console.log(` - Container position: ${getComputedStyle(container).position}`); + console.log(` - Container top: ${getComputedStyle(container).top}`); + console.log(` - Container left: ${getComputedStyle(container).left}`); + console.log(` - Container transform: ${getComputedStyle(container).transform}`); + console.log(` - Container z-index: ${getComputedStyle(container).zIndex}`); + console.log(` - Backdrop z-index: ${getComputedStyle(backdrop).zIndex}`); + + // Render scripts + renderScripts(); + + // Add ESC key listener + document.addEventListener('keydown', handleEscKey); + + // Focus container for accessibility + container.focus(); + + console.log('%cโœ… Script Manager opened successfully', 'color: #39ff14; font-weight: bold;'); + } + + // Make functions globally available + window.executeScript = executeScript; + window.closeScriptManager = closeScriptManager; + window.showScriptManager = showScriptManager; + + // Auto-start the script manager + console.log('%c๐ŸŽฏ Auto-launching Script Manager...', 'color: #00ff41; font-weight: bold;'); + showScriptManager(); + + console.log('%c๐Ÿš€ WPlace AutoBOT Script Manager Ready!', 'color: #39ff14; font-weight: bold; font-size: 16px;'); +})(); \ No newline at end of file diff --git a/Extension/auto-image-styles.css b/Extension/auto-image-styles.css index a4b057f5..1c6d1c6d 100644 --- a/Extension/auto-image-styles.css +++ b/Extension/auto-image-styles.css @@ -13,11 +13,14 @@ Previous CDN imports (now removed): - https://wplace-autobot.github.io/WPlace-AutoBOT/main/themes/classic.css - - https://wplace-autobot.github.io/WPlace-AutoBOT/main/themes/classic-light.css + - https://wplace-autobot.github.io/WPlace-AutoBOT/main/themes/classic-light.css + - https://wplace-autobot.github.io/WPlace-AutoBOT/main/themes/classic-teal.css - https://wplace-autobot.github.io/WPlace-AutoBOT/main/themes/acrylic.css - https://wplace-autobot.github.io/WPlace-AutoBOT/main/themes/neon.css - https://wplace-autobot.github.io/WPlace-AutoBOT/main/themes/neon-cyan.css - https://wplace-autobot.github.io/WPlace-AutoBOT/main/themes/neon-light.css + - https://wplace-autobot.github.io/WPlace-AutoBOT/main/themes/neon-purple.css + - https://wplace-autobot.github.io/WPlace-AutoBOT/main/themes/neon-teal.css These are now loaded from local extension files for better performance and offline functionality. */ @@ -2236,4 +2239,4 @@ input[type="number"] { .pill-btn.active { color: white; -} \ No newline at end of file +} diff --git a/Extension/background.js b/Extension/background.js index 435a8166..2f35dafe 100644 --- a/Extension/background.js +++ b/Extension/background.js @@ -417,11 +417,13 @@ async function loadExtensionResources() { const themeFiles = [ 'auto-image-styles.css', 'themes/acrylic.css', - 'themes/classic-light.css', + 'themes/classic-light.css', + 'themes/classic-teal.css', 'themes/classic.css', 'themes/neon.css', 'themes/neon-cyan.css', - 'themes/neon-light.css' + 'themes/neon-light.css', + 'themes/neon-purple.css' ]; for (const themeFile of themeFiles) { @@ -963,3 +965,4 @@ chrome.runtime.onMessage.addListener((msg, sender, sendResponse) => { } }); + diff --git a/Extension/icons/icon128.png b/Extension/icons/icon128.png index 003ea1e5..39f31066 100644 Binary files a/Extension/icons/icon128.png and b/Extension/icons/icon128.png differ diff --git a/Extension/icons/icon16.png b/Extension/icons/icon16.png index 003ea1e5..39f31066 100644 Binary files a/Extension/icons/icon16.png and b/Extension/icons/icon16.png differ diff --git a/Extension/icons/icon32.png b/Extension/icons/icon32.png index 003ea1e5..39f31066 100644 Binary files a/Extension/icons/icon32.png and b/Extension/icons/icon32.png differ diff --git a/Extension/icons/icon48.png b/Extension/icons/icon48.png deleted file mode 100644 index 003ea1e5..00000000 Binary files a/Extension/icons/icon48.png and /dev/null differ diff --git a/Extension/icons/textures/meloncat.png b/Extension/icons/textures/meloncat.png new file mode 100644 index 00000000..023d40de Binary files /dev/null and b/Extension/icons/textures/meloncat.png differ diff --git a/Extension/manifest.json b/Extension/manifest.json index ffc94430..f6782cfa 100644 --- a/Extension/manifest.json +++ b/Extension/manifest.json @@ -6,7 +6,6 @@ "icons": { "16": "icons/icon16.png", "32": "icons/icon32.png", - "48": "icons/icon48.png", "64": "icons/icon64.png", "128": "icons/icon128.png", "256": "icons/icon256.png" @@ -31,7 +30,6 @@ "default_icon": { "16": "icons/icon16.png", "32": "icons/icon32.png", - "48": "icons/icon48.png", "64": "icons/icon64.png", "128": "icons/icon128.png", "256": "icons/icon256.png" diff --git a/Extension/scripts/Art-Extractor.js b/Extension/scripts/Art-Extractor.js index 66f182e5..c9c3adc4 100644 --- a/Extension/scripts/Art-Extractor.js +++ b/Extension/scripts/Art-Extractor.js @@ -1,1671 +1,3343 @@ -// ==UserScript== -// @name WPlace Art Extractor -// @namespace http://tampermonkey.net/ -// @version 2025-09-20.1 -// @description Extract artwork areas to JSON for Auto-Repair.js -// @author Wbot -// @match https://wplace.live/* -// @grant none -// @icon -// ==/UserScript== - -localStorage.removeItem("lp"); - -// Fallback translation function for when utils manager isn't loaded -function getText(key, params) { - // Try to get translation from loadedTranslations - try { - if (window.loadedTranslations && window.loadedTranslations[window.state?.language || 'en']) { - const translation = window.loadedTranslations[window.state?.language || 'en'][key]; - if (translation && typeof translation === 'string') { - if (params && typeof params === 'object') { - return translation.replace(/\{(\w+)\}/g, (match, paramKey) => params[paramKey] || match); - } - return translation; - } - } - } catch (error) { - console.warn('Translation lookup error:', error); - } - - return key; // Fallback to key if no translation found -} - -;(async () => { - // Prevent multiple instances of this script from running - if (window.WPLACE_ART_EXTRACTOR_LOADED) { - console.log('Art Extractor already loaded, skipping...'); - return; - } - window.WPLACE_ART_EXTRACTOR_LOADED = true; - - console.log('%c๐ŸŽจ WPlace Art Extractor Starting...', 'color: #ff6b35; font-weight: bold; font-size: 16px;'); - console.log('%cโœจ Interactive pixel capture for area extraction!', 'color: #26de81; font-weight: bold;'); - - // Immediate visual confirmation that script is running - try { - const testDiv = document.createElement('div'); - testDiv.innerHTML = ` -
- ๐ŸŽจ Art Extractor Loading... -
- `; - document.body.appendChild(testDiv); - - // Remove after 3 seconds - setTimeout(() => { - if (testDiv.parentNode) { - testDiv.parentNode.removeChild(testDiv); - } - }, 3000); - } catch (e) { - console.log('Could not create test notification:', e); - } - - // CONFIGURATION CONSTANTS - const CONFIG = { - OVERLAY_OPACITY: 0.7, - SELECTION_COLOR: { r: 50, g: 200, b: 50, a: 180 }, // Green extraction overlay - CORNER_MARKER_COLOR: { r: 255, g: 255, b: 0, a: 255 }, // Yellow corner markers - CORNER_MARKER_SIZE: 12, // Corner marker size - MIN_AREA_SIZE: 1, - MAX_AREA_SIZE: 2500, - COLOR_MAP: { - 0: { id: 1, name: 'Black', rgb: { r: 0, g: 0, b: 0 } }, - 1: { id: 2, name: 'Dark Gray', rgb: { r: 60, g: 60, b: 60 } }, - 2: { id: 3, name: 'Gray', rgb: { r: 120, g: 120, b: 120 } }, - 3: { id: 4, name: 'Light Gray', rgb: { r: 210, g: 210, b: 210 } }, - 4: { id: 5, name: 'White', rgb: { r: 255, g: 255, b: 255 } }, - 5: { id: 6, name: 'Deep Red', rgb: { r: 96, g: 0, b: 24 } }, - 6: { id: 7, name: 'Red', rgb: { r: 237, g: 28, b: 36 } }, - 7: { id: 8, name: 'Orange', rgb: { r: 255, g: 127, b: 39 } }, - 8: { id: 9, name: 'Gold', rgb: { r: 246, g: 170, b: 9 } }, - 9: { id: 10, name: 'Yellow', rgb: { r: 249, g: 221, b: 59 } }, - 10: { id: 11, name: 'Light Yellow', rgb: { r: 255, g: 250, b: 188 } }, - 11: { id: 12, name: 'Dark Green', rgb: { r: 14, g: 185, b: 104 } }, - 12: { id: 13, name: 'Green', rgb: { r: 19, g: 230, b: 123 } }, - 13: { id: 14, name: 'Light Green', rgb: { r: 135, g: 255, b: 94 } }, - 14: { id: 15, name: 'Dark Teal', rgb: { r: 12, g: 129, b: 110 } }, - 15: { id: 16, name: 'Teal', rgb: { r: 16, g: 174, b: 166 } }, - 16: { id: 17, name: 'Light Teal', rgb: { r: 19, g: 225, b: 190 } }, - 17: { id: 20, name: 'Cyan', rgb: { r: 96, g: 247, b: 242 } }, - 18: { id: 44, name: 'Light Cyan', rgb: { r: 187, g: 250, b: 242 } }, - 19: { id: 18, name: 'Dark Blue', rgb: { r: 40, g: 80, b: 158 } }, - 20: { id: 19, name: 'Blue', rgb: { r: 64, g: 147, b: 228 } }, - 21: { id: 21, name: 'Indigo', rgb: { r: 107, g: 80, b: 246 } }, - 22: { id: 22, name: 'Light Indigo', rgb: { r: 153, g: 177, b: 251 } }, - 23: { id: 23, name: 'Dark Purple', rgb: { r: 120, g: 12, b: 153 } }, - 24: { id: 24, name: 'Purple', rgb: { r: 170, g: 56, b: 185 } }, - 25: { id: 25, name: 'Light Purple', rgb: { r: 224, g: 159, b: 249 } }, - 26: { id: 26, name: 'Dark Pink', rgb: { r: 203, g: 0, b: 122 } }, - 27: { id: 27, name: 'Pink', rgb: { r: 236, g: 31, b: 128 } }, - 28: { id: 28, name: 'Light Pink', rgb: { r: 243, g: 141, b: 169 } }, - 29: { id: 29, name: 'Dark Brown', rgb: { r: 104, g: 70, b: 52 } }, - 30: { id: 30, name: 'Brown', rgb: { r: 149, g: 104, b: 42 } }, - 31: { id: 31, name: 'Beige', rgb: { r: 248, g: 178, b: 119 } }, - 32: { id: 52, name: 'Light Beige', rgb: { r: 255, g: 197, b: 165 } }, - 33: { id: 32, name: 'Medium Gray', rgb: { r: 170, g: 170, b: 170 } }, - 34: { id: 33, name: 'Dark Red', rgb: { r: 165, g: 14, b: 30 } }, - 35: { id: 34, name: 'Light Red', rgb: { r: 250, g: 128, b: 114 } }, - 36: { id: 35, name: 'Dark Orange', rgb: { r: 228, g: 92, b: 26 } }, - 37: { id: 37, name: 'Dark Goldenrod', rgb: { r: 156, g: 132, b: 49 } }, - 38: { id: 38, name: 'Goldenrod', rgb: { r: 197, g: 173, b: 49 } }, - 39: { id: 39, name: 'Light Goldenrod', rgb: { r: 232, g: 212, b: 95 } }, - 40: { id: 40, name: 'Dark Olive', rgb: { r: 74, g: 107, b: 58 } }, - 41: { id: 41, name: 'Olive', rgb: { r: 90, g: 148, b: 74 } }, - 42: { id: 42, name: 'Light Olive', rgb: { r: 132, g: 197, b: 115 } }, - 43: { id: 43, name: 'Dark Cyan', rgb: { r: 15, g: 121, b: 159 } }, - 44: { id: 45, name: 'Light Blue', rgb: { r: 125, g: 199, b: 255 } }, - 45: { id: 46, name: 'Dark Indigo', rgb: { r: 77, g: 49, b: 184 } }, - 46: { id: 47, name: 'Dark Slate Blue', rgb: { r: 74, g: 66, b: 132 } }, - 47: { id: 48, name: 'Slate Blue', rgb: { r: 122, g: 113, b: 196 } }, - 48: { id: 49, name: 'Light Slate Blue', rgb: { r: 181, g: 174, b: 241 } }, - 49: { id: 53, name: 'Dark Peach', rgb: { r: 155, g: 82, b: 73 } }, - 50: { id: 54, name: 'Peach', rgb: { r: 209, g: 128, b: 120 } }, - 51: { id: 55, name: 'Light Peach', rgb: { r: 250, g: 182, b: 164 } }, - 52: { id: 50, name: 'Light Brown', rgb: { r: 219, g: 164, b: 99 } }, - 53: { id: 56, name: 'Dark Tan', rgb: { r: 123, g: 99, b: 82 } }, - 54: { id: 57, name: 'Tan', rgb: { r: 156, g: 132, b: 107 } }, - 55: { id: 36, name: 'Light Tan', rgb: { r: 214, g: 181, b: 148 } }, - 56: { id: 51, name: 'Dark Beige', rgb: { r: 209, g: 128, b: 81 } }, - 57: { id: 61, name: 'Dark Stone', rgb: { r: 109, g: 100, b: 63 } }, - 58: { id: 62, name: 'Stone', rgb: { r: 148, g: 140, b: 107 } }, - 59: { id: 63, name: 'Light Stone', rgb: { r: 205, g: 197, b: 158 } }, - 60: { id: 58, name: 'Dark Slate', rgb: { r: 51, g: 57, b: 65 } }, - 61: { id: 59, name: 'Slate', rgb: { r: 109, g: 117, b: 141 } }, - 62: { id: 60, name: 'Light Slate', rgb: { r: 179, g: 185, b: 209 } }, - 63: { id: 0, name: 'Transparent', rgb: null }, - } - }; - - // Expose CONFIG globally for the utils manager and other modules - window.ART_EXTRACTOR_CONFIG = CONFIG; - - // GLOBAL STATE - const state = { - // Pixel capture state - isCapturing: false, - capturedPixels: [], // Array of painted pixels with their world coordinates - requiredPixels: 2, // Need 2 corner pixels - - // Selection area - selectionArea: null, // { x, y, width, height, regionX, regionY } - - // UI state - ui: null, - overlayManager: null, // Will use global overlay manager like Auto-Image - - // Scanning state - isScanning: false, - scannedPixels: [], - totalPixels: 0, - - // Auto-Image integration state - imageData: null, - startPosition: null, - region: null, - - // Canvas monitoring - paintEventListener: null, - lastPaintEvent: null, - - language: 'en', - }; - - // Expose state globally - window.ART_EXTRACTOR_STATE = state; - - // Use Auto-Image's overlay system - no custom overlay manager needed - // We'll integrate with the global overlay manager that Auto-Image uses - - // Monitor WPlace paint events to capture pixel coordinates using fetch interception - let originalFetch = null; - let fetchInterceptionActive = false; - - function setupPixelCapture() { - console.log('๐ŸŽฏ Setting up pixel paint monitoring...'); - - try { - // Store original fetch if not already stored - if (!originalFetch) { - originalFetch = window.fetch; - } - - // Install fetch interceptor for pixel capture - if (!fetchInterceptionActive) { - window.fetch = async (url, options) => { - // Check if this is a pixel painting request - if (state.isCapturing && - typeof url === 'string' && - url.includes('/s0/pixel/') && - options && - options.method === 'POST') { - - try { - console.log('๐ŸŽฏ Intercepting pixel paint request:', url); - - // Call original fetch first - const response = await originalFetch(url, options); - - // If request was successful and we have body data, extract coordinates - if (response.ok && options.body) { - let requestBody; - try { - requestBody = JSON.parse(options.body); - } catch (parseError) { - console.warn('Could not parse request body:', parseError); - return response; - } - - // Extract coordinates from request - if (requestBody.coords && Array.isArray(requestBody.coords) && requestBody.coords.length >= 2) { - const localX = requestBody.coords[0]; // 0-999 within tile - const localY = requestBody.coords[1]; // 0-999 within tile - - // Extract tile coordinates from URL: /s0/pixel/{tileX}/{tileY} - const tileMatch = url.match(/\/s0\/pixel\/(\-?\d+)\/(\-?\d+)/); - if (tileMatch) { - const tileX = parseInt(tileMatch[1]); - const tileY = parseInt(tileMatch[2]); - - // Convert to global canvas coordinates - const globalX = tileX * 1000 + localX; - const globalY = tileY * 1000 + localY; - - console.log(`๐ŸŽฏ Captured painted pixel: (${globalX}, ${globalY}) from tile (${tileX}, ${tileY}) local (${localX}, ${localY})`); - - // Add the captured pixel - setTimeout(() => { - addCapturedPixel(globalX, globalY); - }, 100); - } - } - } - - return response; - } catch (error) { - console.error('โŒ Error intercepting pixel paint:', error); - // Fallback to original fetch - return originalFetch(url, options); - } - } - - // For all other requests, use original fetch - return originalFetch(url, options); - }; - - fetchInterceptionActive = true; - console.log('โœ… Fetch interception enabled'); - } - - return true; - - } catch (error) { - console.warn('โš ๏ธ Could not set up paint monitoring:', error); - return false; - } - } - - // Restore original fetch function - function restoreFetch() { - if (originalFetch && fetchInterceptionActive) { - window.fetch = originalFetch; - fetchInterceptionActive = false; - console.log('๐Ÿ”„ Original fetch restored'); - } - } - - // Handle canvas clicks during capture mode (fallback method) - function handleCanvasClick(event) { - if (!state.isCapturing) return; - - // Only handle clicks on canvas elements - const canvas = event.target.closest('canvas'); - if (!canvas) return; - - console.log('๏ฟฝ Canvas click detected during capture mode'); - - // For now, we'll use manual coordinate input as the primary method - // since intercepting the actual paint events is complex - setTimeout(() => { - if (state.isCapturing && state.capturedPixels.length < state.requiredPixels) { - promptForCoordinates(); - } - }, 100); - } - - // Handle detected paint events (for future implementation) - function handlePaintEvent(event) { - if (!state.isCapturing) return; - - console.log('๐ŸŽฏ Processing paint event:', event); - - // Extract coordinates from paint event - let worldX, worldY; - - if (event.x !== undefined && event.y !== undefined) { - worldX = event.x; - worldY = event.y; - } else if (event.position) { - worldX = event.position.x; - worldY = event.position.y; - } else if (event.coords) { - worldX = event.coords[0]; - worldY = event.coords[1]; - } else { - console.warn('โš ๏ธ Could not extract coordinates from paint event'); - return; - } - - // Add the painted pixel to our capture list - addCapturedPixel(worldX, worldY); - } - - // Manual coordinate input (fallback method when auto-capture doesn't work) - function promptForCoordinates() { - if (!state.isCapturing) { - showAlert('Not in capture mode', 'warning'); - return; - } - - const pixelNum = state.capturedPixels.length + 1; - const cornerType = pixelNum === 1 ? 'UPPER-LEFT' : 'LOWER-RIGHT'; - - const coordStr = prompt( - `๐Ÿ“ Enter coordinates for ${cornerType} corner (${pixelNum}/${state.requiredPixels})\n\n` + - `Format: x,y (e.g., 1000,500)\n\n` + - `To find coordinates:\n` + - `1. Navigate to the ${cornerType} corner of your area\n` + - `2. Hover over the exact pixel position\n` + - `3. Check coordinates in bottom-left corner of WPlace\n` + - `4. Enter them here:\n\n` + - `Tip: You can also just paint a pixel there and it will be captured automatically!` - ); - - if (coordStr && coordStr.trim()) { - const coords = coordStr.trim().split(',').map(n => parseInt(n.trim())); - if (coords.length === 2 && !isNaN(coords[0]) && !isNaN(coords[1])) { - addCapturedPixel(coords[0], coords[1]); - } else { - showAlert('Invalid coordinates format. Use: x,y (e.g., 1000,500)', 'error'); - // Try again - setTimeout(() => promptForCoordinates(), 500); - } - } else if (coordStr !== null) { - // User entered empty string, try again - showAlert('Please enter coordinates', 'warning'); - setTimeout(() => promptForCoordinates(), 500); - } - // If user clicked cancel (coordStr === null), do nothing - } - - // Add a captured pixel with enhanced validation - function addCapturedPixel(worldX, worldY) { - if (!state.isCapturing) { - console.log('Not in capture mode, ignoring pixel'); - return; - } - - if (state.capturedPixels.length >= state.requiredPixels) { - showAlert('Already have enough pixels', 'warning'); - return; - } - - // Validate coordinates - if (!Number.isFinite(worldX) || !Number.isFinite(worldY)) { - showAlert('Invalid pixel coordinates', 'error'); - return; - } - - // Calculate region and local coordinates - const regionX = Math.floor(worldX / 1000); - const regionY = Math.floor(worldY / 1000); - const pixelX = worldX % 1000; - const pixelY = worldY % 1000; - - const pixel = { - worldX, - worldY, - regionX, - regionY, - pixelX, - pixelY, - timestamp: Date.now(), - cornerType: state.capturedPixels.length === 0 ? 'upperLeft' : 'lowerRight' - }; - - state.capturedPixels.push(pixel); - console.log(`๐ŸŽฏ Captured ${pixel.cornerType} pixel ${state.capturedPixels.length}/${state.requiredPixels}:`, pixel); - - updateUI(); - - // Progress messages - if (state.capturedPixels.length === 1) { - showAlert(`โœ… Upper-left corner captured: (${worldX}, ${worldY})`, 'success'); - setTimeout(() => { - showAlert('๐ŸŽฏ Now paint a pixel at the LOWER-RIGHT corner of the area', 'info'); - }, 1500); - } else if (state.capturedPixels.length >= state.requiredPixels) { - showAlert(`โœ… Lower-right corner captured: (${worldX}, ${worldY})`, 'success'); - // Small delay before auto-scanning to show the success message - setTimeout(() => { - completeAreaCapture(); - // Auto-trigger scan after area capture is complete - setTimeout(() => { - scanSelectedArea(); - }, 500); - }, 1000); - } - } - - // Complete area capture when we have 2 pixels with validation - function completeAreaCapture() { - state.isCapturing = false; - restoreFetch(); // Restore original fetch function - - if (state.capturedPixels.length < 2) { - showAlert('Need at least 2 pixels to define area', 'error'); - return; - } - - // Calculate bounding box from captured pixels - const pixels = state.capturedPixels; - const minX = Math.min(...pixels.map(p => p.worldX)); - const minY = Math.min(...pixels.map(p => p.worldY)); - const maxX = Math.max(...pixels.map(p => p.worldX)); - const maxY = Math.max(...pixels.map(p => p.worldY)); - - // Validation: ensure upper-left is actually upper-left - if (minX >= maxX || minY >= maxY) { - showAlert('โŒ Invalid area: upper-left corner must be less than lower-right corner', 'error'); - // Reset capture to try again - state.capturedPixels = []; - startPixelCapture(); - return; - } - - const width = maxX - minX + 1; - const height = maxY - minY + 1; - - // Size validation - if (width < CONFIG.MIN_AREA_SIZE || height < CONFIG.MIN_AREA_SIZE) { - showAlert(`โŒ Area too small: ${width}ร—${height} pixels (minimum: ${CONFIG.MIN_AREA_SIZE}ร—${CONFIG.MIN_AREA_SIZE})`, 'error'); - return; - } - - if (width > CONFIG.MAX_AREA_SIZE || height > CONFIG.MAX_AREA_SIZE) { - showAlert(`โŒ Area too large: ${width}ร—${height} pixels (maximum: ${CONFIG.MAX_AREA_SIZE}ร—${CONFIG.MAX_AREA_SIZE})`, 'error'); - return; - } - - state.selectionArea = { - x1: minX, // Coordinate naming - y1: minY, - x2: maxX, - y2: maxY, - x: minX, // Keep compatibility - y: minY, - width: width, - height: height, - regionX: Math.floor(minX / 1000), - regionY: Math.floor(minY / 1000) - }; - - console.log('๐Ÿ“ Area capture complete:', state.selectionArea); - showAlert(`โœ… Area captured: ${width}ร—${height} pixels from (${minX},${minY}) to (${maxX},${maxY})`, 'success'); - - // Set up overlay using Auto-Image's system - setTimeout(() => { - setupAutoImageOverlay(); - }, 500); - - updateUI(); - } - - // Set up visual overlay for selected extraction area (simplified approach) - async function setupAutoImageOverlay() { - if (!state.selectionArea) return; - - console.log('๐ŸŽจ Setting up extraction area overlay...'); - - try { - const { x1, y1, x2, y2, width, height, regionX, regionY } = state.selectionArea; - - // Try to use Auto-Image's overlay if available, otherwise create simple visual feedback - if (window.autoImageOverlayManager) { - console.log('๐Ÿ“Š Using Auto-Image overlay manager for area visualization'); - - const overlayMgr = window.autoImageOverlayManager; - - // Create overlay canvas showing the selected area - const canvas = new OffscreenCanvas(width, height); - const ctx = canvas.getContext('2d'); - - // Fill with extraction area overlay - ctx.fillStyle = `rgba(${CONFIG.SELECTION_COLOR.r}, ${CONFIG.SELECTION_COLOR.g}, ${CONFIG.SELECTION_COLOR.b}, 0.3)`; - ctx.fillRect(0, 0, width, height); - - // Add extraction border - ctx.strokeStyle = `rgba(${CONFIG.SELECTION_COLOR.r}, ${CONFIG.SELECTION_COLOR.g}, ${CONFIG.SELECTION_COLOR.b}, 0.8)`; - ctx.lineWidth = 2; - ctx.strokeRect(1, 1, width - 2, height - 2); - - // Add corner markers - const cornerSize = CONFIG.CORNER_MARKER_SIZE; - ctx.fillStyle = `rgba(${CONFIG.CORNER_MARKER_COLOR.r}, ${CONFIG.CORNER_MARKER_COLOR.g}, ${CONFIG.CORNER_MARKER_COLOR.b}, 1)`; - - // Top-left corner - ctx.fillRect(0, 0, cornerSize, cornerSize); - // Top-right corner - ctx.fillRect(width - cornerSize, 0, cornerSize, cornerSize); - // Bottom-left corner - ctx.fillRect(0, height - cornerSize, cornerSize, cornerSize); - // Bottom-right corner - ctx.fillRect(width - cornerSize, height - cornerSize, cornerSize, cornerSize); - - try { - // Create image bitmap - const overlayBitmap = await canvas.transferToImageBitmap(); - - // Set the overlay using Auto-Image's API - await overlayMgr.setImage(overlayBitmap); - await overlayMgr.setPosition( - { x: x1 % 1000, y: y1 % 1000 }, - { x: regionX, y: regionY } - ); - - // Enable the overlay - overlayMgr.enable(); - - // Store references for cleanup - state.overlayManager = overlayMgr; - state.imageData = overlayBitmap; - state.startPosition = { x: x1 % 1000, y: y1 % 1000 }; - state.region = { x: regionX, y: regionY }; - - console.log('โœ… Extraction area overlay enabled'); - } catch (overlayError) { - console.warn('โš ๏ธ Could not set overlay, continuing without visual feedback:', overlayError); - } - - } else { - console.log('๐Ÿ“‹ Auto-Image overlay not available, using console feedback only'); - } - - } catch (error) { - console.error('โŒ Failed to setup extraction area overlay:', error); - // Continue without overlay - not critical for extraction functionality - } - } - // Tile-based pixel scanning for area extraction - async function scanSelectedArea() { - if (!state.selectionArea) { - showAlert('No area selected', 'error'); - return; - } - - console.log('๐Ÿ” Starting area scan...'); - state.isScanning = true; - state.scannedPixels = []; - updateUI(); - - try { - const { x1, y1, x2, y2 } = state.selectionArea; - const areaWidth = x2 - x1 + 1; - const areaHeight = y2 - y1 + 1; - - console.log(`๐Ÿ” Analyzing area ${areaWidth}x${areaHeight} from (${x1},${y1}) to (${x2},${y2})`); - - // Tile calculation - const startTileX = Math.floor(x1 / 1000); - const startTileY = Math.floor(y1 / 1000); - const endTileX = Math.floor(x2 / 1000); - const endTileY = Math.floor(y2 / 1000); - - state.totalPixels = areaWidth * areaHeight; - let processedPixels = 0; - - // Process each tile that intersects with our area - for (let tileY = startTileY; tileY <= endTileY; tileY++) { - for (let tileX = startTileX; tileX <= endTileX; tileX++) { - try { - console.log(`๐Ÿ“„ Processing tile (${tileX}, ${tileY})...`); - - // Download tile data - const tileBlob = await getTileImage(tileX, tileY); - if (!tileBlob) { - console.warn(`โš ๏ธ Could not download tile ${tileX},${tileY}, skipping...`); - continue; - } - - // Process tile data - const tileImageData = await processTileBlob(tileBlob); - if (!tileImageData) { - console.warn(`โš ๏ธ Could not process tile ${tileX},${tileY}, skipping...`); - continue; - } - - // Calculate intersection between area and current tile - const tileStartX = tileX * 1000; - const tileStartY = tileY * 1000; - const tileEndX = tileStartX + 1000; - const tileEndY = tileStartY + 1000; - - const intersectStartX = Math.max(x1, tileStartX); - const intersectStartY = Math.max(y1, tileStartY); - const intersectEndX = Math.min(x2 + 1, tileEndX); - const intersectEndY = Math.min(y2 + 1, tileEndY); - - // Extract pixels from intersection area - for (let globalY = intersectStartY; globalY < intersectEndY; globalY++) { - for (let globalX = intersectStartX; globalX < intersectEndX; globalX++) { - // Convert to tile-local coordinates - const localX = globalX - tileStartX; - const localY = globalY - tileStartY; - - // Normalize coordinates - const normalizedX = (localX % 1000 + 1000) % 1000; - const normalizedY = (localY % 1000 + 1000) % 1000; - - if (normalizedX >= 0 && normalizedX < 1000 && - normalizedY >= 0 && normalizedY < 1000 && - normalizedX < tileImageData.width && - normalizedY < tileImageData.height) { - - // Extract RGBA values - const pixelIndex = (normalizedY * tileImageData.width + normalizedX) * 4; - const r = tileImageData.data[pixelIndex]; - const g = tileImageData.data[pixelIndex + 1]; - const b = tileImageData.data[pixelIndex + 2]; - const a = tileImageData.data[pixelIndex + 3]; - - if (a > 0) { // Skip transparent pixels - // Find closest color in WPlace palette - const closestColor = findClosestColor(r, g, b); - - if (closestColor) { - // Convert to area-relative coordinates - const areaX = globalX - x1; - const areaY = globalY - y1; - - state.scannedPixels.push({ - x: areaX, - y: areaY, - worldX: globalX, - worldY: globalY, - color: closestColor, - rgb: { r, g, b, a } - }); - } - } - } - - processedPixels++; - } - } - - // Update progress every tile - updateUI(); - await new Promise(resolve => setTimeout(resolve, 1)); - - } catch (error) { - console.error(`โŒ Error processing tile ${tileX},${tileY}:`, error); - } - } - } - - console.log(`โœ… Scan complete: ${state.scannedPixels.length} pixels extracted`); - showAlert(`โœ… Extracted ${state.scannedPixels.length} pixels from area`, 'success'); - - } catch (error) { - console.error('โŒ Area extraction failed:', error); - showAlert(`โŒ Extraction failed: ${error.message}`, 'error'); - } finally { - state.isScanning = false; - updateUI(); - } - } - - // Download tile images - async function getTileImage(tileX, tileY) { - try { - const tileUrl = `https://backend.wplace.live/files/s0/tiles/${tileX}/${tileY}.png`; - const response = await fetch(tileUrl); - if (!response.ok) { - throw new Error(`HTTP ${response.status}`); - } - return await response.blob(); - } catch (error) { - console.warn(`Error downloading tile ${tileX},${tileY}:`, error); - return null; - } - } - - // Process tile blob into ImageData - async function processTileBlob(blob) { - try { - const img = new Image(); - const canvas = document.createElement('canvas'); - const ctx = canvas.getContext('2d'); - - return new Promise((resolve, reject) => { - img.onload = () => { - canvas.width = img.width; - canvas.height = img.height; - ctx.drawImage(img, 0, 0); - const imageData = ctx.getImageData(0, 0, canvas.width, canvas.height); - resolve(imageData); - }; - img.onerror = reject; - img.src = URL.createObjectURL(blob); - }); - } catch (error) { - console.error('Error processing tile blob:', error); - return null; - } - } - - // Color matching using LAB color space for better accuracy - function findClosestColor(r, g, b) { - if (!CONFIG.COLOR_MAP) return null; - - let closestColor = null; - let minDistance = Infinity; - - // Convert input RGB to LAB color space - const inputLab = rgbToLab(r, g, b); - - for (const [index, colorInfo] of Object.entries(CONFIG.COLOR_MAP)) { - if (!colorInfo.rgb) continue; // Skip transparent - - const { r: cr, g: cg, b: cb } = colorInfo.rgb; - - // Convert palette color to LAB - const paletteLab = rgbToLab(cr, cg, cb); - - // Calculate Delta-E distance - const deltaE = calculateDeltaE(inputLab, paletteLab); - - if (deltaE < minDistance) { - minDistance = deltaE; - closestColor = colorInfo; - } - } - - return closestColor; - } - - // RGB to LAB color space conversion - function rgbToLab(r, g, b) { - // Normalize RGB values - r = r / 255; - g = g / 255; - b = b / 255; - - // Apply gamma correction - r = r > 0.04045 ? Math.pow((r + 0.055) / 1.055, 2.4) : r / 12.92; - g = g > 0.04045 ? Math.pow((g + 0.055) / 1.055, 2.4) : g / 12.92; - b = b > 0.04045 ? Math.pow((b + 0.055) / 1.055, 2.4) : b / 12.92; - - // Convert to XYZ - const x = r * 0.4124564 + g * 0.3575761 + b * 0.1804375; - const y = r * 0.2126729 + g * 0.7151522 + b * 0.072175; - const z = r * 0.0193339 + g * 0.119192 + b * 0.9503041; - - // Convert XYZ to LAB - const xn = x / 0.95047; - const yn = y / 1.00000; - const zn = z / 1.08883; - - const fx = xn > 0.008856 ? Math.pow(xn, 1/3) : (7.787 * xn + 16/116); - const fy = yn > 0.008856 ? Math.pow(yn, 1/3) : (7.787 * yn + 16/116); - const fz = zn > 0.008856 ? Math.pow(zn, 1/3) : (7.787 * zn + 16/116); - - const l = 116 * fy - 16; - const a = 500 * (fx - fy); - const bLab = 200 * (fy - fz); - - return { l, a, b: bLab }; - } - - // Delta-E color difference calculation - function calculateDeltaE(lab1, lab2) { - const deltaL = lab1.l - lab2.l; - const deltaA = lab1.a - lab2.a; - const deltaB = lab1.b - lab2.b; - - return Math.sqrt(deltaL * deltaL + deltaA * deltaA + deltaB * deltaB); - } - - // Export to JSON (Auto-Image compatible format for Auto-Repair) - function exportToJSON(exportType = 'autofix') { - if (!state.selectionArea || state.scannedPixels.length === 0) { - showAlert('No data to export. Please select an area and scan it first.', 'error'); - return; - } - - console.log(`๐Ÿ“ค Exporting ${state.scannedPixels.length} pixels in ${exportType} format...`); - console.log('๐Ÿ” [DEBUG] Selection area:', state.selectionArea); - console.log('๐Ÿ” [DEBUG] Scanned pixels count:', state.scannedPixels.length); - - try { - // Get filename from input - const filenameInput = state.ui?.querySelector('#filename-input'); - const userFilename = filenameInput?.value?.trim() || '.json'; - let filename; - - if (userFilename === '.json' || userFilename === '') { - // Use default filename with timestamp - const timestamp = new Date().toISOString().slice(0, 19).replace(/:/g, '-'); - filename = `wplace-extracted-${exportType}-${timestamp}.json`; - } else { - // Ensure filename ends with .json - filename = userFilename.endsWith('.json') ? userFilename : userFilename + '.json'; - } - - // Build Auto-Image compatible data structure - const { x1, y1, x2, y2 } = state.selectionArea; - const width = x2 - x1 + 1; - const height = y2 - y1 + 1; - - // Convert scanned pixels to Auto-Image pixel format (Uint8ClampedArray) - const imagePixels = new Uint8ClampedArray(width * height * 4); - imagePixels.fill(0); // Initialize with transparent pixels - - // Fill pixel data from scanned pixels - for (const pixel of state.scannedPixels) { - const pixelIndex = (pixel.y * width + pixel.x) * 4; - if (pixelIndex >= 0 && pixelIndex < imagePixels.length - 3) { - const rgb = pixel.color?.rgb || pixel.rgb; - imagePixels[pixelIndex] = rgb.r; - imagePixels[pixelIndex + 1] = rgb.g; - imagePixels[pixelIndex + 2] = rgb.b; - imagePixels[pixelIndex + 3] = rgb.a || 255; - } - } - - // Create different formats based on export type - let saveData; - - if (exportType === 'autofix') { - // Auto-Fix format: Include exact position and region for restoration - saveData = { - timestamp: Date.now(), - version: '2.2', - state: { - totalPixels: width * height, - paintedPixels: 0, - lastPosition: { x: 0, y: 0 }, - startPosition: { x: x1 % 1000, y: y1 % 1000 }, - region: { x: Math.floor(x1 / 1000), y: Math.floor(y1 / 1000) }, - imageLoaded: true, - colorsChecked: true, - coordinateMode: 'rows', - coordinateDirection: 'top-left', - coordinateSnake: false, - blockWidth: 1, - blockHeight: 1, - availableColors: Object.values(CONFIG.COLOR_MAP).filter(c => c.rgb).map(color => ({ - ...color, - rgb: [color.rgb.r, color.rgb.g, color.rgb.b] // Convert RGB object to array for Auto-Image compatibility - })), - // Additional Auto-Image required fields for compatibility - paintWhitePixels: true, - paintTransparentPixels: false, - displayCharges: 0, - preciseCurrentCharges: 0, - maxCharges: 1, - cooldown: 30000, - stopFlag: false, - selectingPosition: false, - minimized: false, - estimatedTime: 0, - language: 'en', - paintingSpeed: 5, - batchMode: 'normal', - randomBatchMin: 1, - randomBatchMax: 5, - cooldownChargeThreshold: 10, - overlayOpacity: 0.7, - blueMarbleEnabled: false, - ditheringEnabled: true, - colorMatchingAlgorithm: 'lab', - enableChromaPenalty: true, - chromaPenaltyWeight: 0.15, - customTransparencyThreshold: 128, - customWhiteThreshold: 230, - paintUnavailablePixels: true, - // Critical missing fields that Auto-Image expects - fullChargeData: null, - fullChargeInterval: null, - tokenSource: 'generator', - initialSetupComplete: true, - resizeSettings: null, - originalImage: null, - resizeIgnoreMask: null - }, - imageData: { - width: width, - height: height, - pixels: Array.from(imagePixels), - totalPixels: width * height - }, - paintedMapPacked: null - }; - } else { - // Auto-Image format: Empty position and region for manual placement - saveData = { - timestamp: Date.now(), - version: '2.2', - state: { - totalPixels: width * height, - paintedPixels: 0, - lastPosition: { x: 0, y: 0 }, - startPosition: null, // Empty for manual placement - region: null, // Empty for manual placement - imageLoaded: true, - colorsChecked: true, - coordinateMode: 'rows', - coordinateDirection: 'top-left', - coordinateSnake: false, - blockWidth: 1, - blockHeight: 1, - availableColors: Object.values(CONFIG.COLOR_MAP).filter(c => c.rgb).map(color => ({ - ...color, - rgb: [color.rgb.r, color.rgb.g, color.rgb.b] // Convert RGB object to array for Auto-Image compatibility - })), - // Additional Auto-Image required fields for compatibility - paintWhitePixels: true, - paintTransparentPixels: false, - displayCharges: 0, - preciseCurrentCharges: 0, - maxCharges: 1, - cooldown: 30000, - stopFlag: false, - selectingPosition: false, - minimized: false, - estimatedTime: 0, - language: 'en', - paintingSpeed: 5, - batchMode: 'normal', - randomBatchMin: 1, - randomBatchMax: 5, - cooldownChargeThreshold: 10, - overlayOpacity: 0.7, - blueMarbleEnabled: false, - ditheringEnabled: true, - colorMatchingAlgorithm: 'lab', - enableChromaPenalty: true, - chromaPenaltyWeight: 0.15, - customTransparencyThreshold: 128, - customWhiteThreshold: 230, - paintUnavailablePixels: true, - // Critical missing fields that Auto-Image expects - fullChargeData: null, - fullChargeInterval: null, - tokenSource: 'generator', - initialSetupComplete: true, - resizeSettings: null, - originalImage: null, - resizeIgnoreMask: null - }, - imageData: { - width: width, - height: height, - pixels: Array.from(imagePixels), - totalPixels: width * height - }, - paintedMapPacked: null - }; - } - - // Debug: Log the export data structure before saving - console.log('๐Ÿ” [DEBUG] Export data structure:', { - hasState: !!saveData.state, - hasImageData: !!saveData.imageData, - topLevelKeys: Object.keys(saveData), - stateKeys: saveData.state ? Object.keys(saveData.state) : 'N/A', - imageDataKeys: saveData.imageData ? Object.keys(saveData.imageData) : 'N/A', - version: saveData.version, - exportType: exportType, - imageDataPixelsLength: saveData.imageData?.pixels?.length || 0, - imageWidth: saveData.imageData?.width || 0, - imageHeight: saveData.imageData?.height || 0 - }); - - // Validate export data before saving - if (!saveData.state || !saveData.imageData) { - throw new Error('Export validation failed: Missing state or imageData'); - } - - if (!saveData.imageData.pixels || saveData.imageData.pixels.length === 0) { - throw new Error('Export validation failed: No pixel data to export'); - } - - // Download the file using Auto-Image's method - const dataStr = JSON.stringify(saveData, null, 2); - const blob = new Blob([dataStr], { type: 'application/json' }); - const url = URL.createObjectURL(blob); - - const a = document.createElement('a'); - a.href = url; - a.download = filename; - document.body.appendChild(a); - a.click(); - document.body.removeChild(a); - URL.revokeObjectURL(url); - - console.log(`โœ… ${exportType} export complete: ${filename}`); - console.log(`๐Ÿ“ Area: ${width}x${height} from (${x1},${y1}) to (${x2},${y2})`); - - if (exportType === 'autofix') { - console.log(`๐Ÿ”— Compatible with Auto-Repair for restoration`); - showAlert(`โœ… Exported ${state.scannedPixels.length} pixels for Auto-Fix to ${filename}`, 'success'); - } else { - console.log(`๐ŸŽจ Ready for manual placement in Auto-Image`); - showAlert(`โœ… Exported ${state.scannedPixels.length} pixels for Auto-Image to ${filename}`, 'success'); - } - - } catch (error) { - console.error('โŒ Export failed:', error); - showAlert(`โŒ Export failed: ${error.message}`, 'error'); - } - } - - // Filename input handlers - function handleFilenameInput(event) { - const input = event.target; - let value = input.value; - - // Always ensure it ends with .json - if (!value.endsWith('.json')) { - if (value.length > 0 && !value.includes('.json')) { - input.value = value + '.json'; - } else if (value === '') { - input.value = '.json'; - } - } - - // Position cursor before .json - if (input.value.endsWith('.json') && event.inputType !== 'deleteContentBackward') { - const nameLength = input.value.length - 5; // -5 for '.json' - input.setSelectionRange(nameLength, nameLength); - } - } - - function preventJsonDeletion(event) { - const input = event.target; - const cursorPos = input.selectionStart; - const value = input.value; - - // Prevent deletion of .json extension - if ((event.key === 'Backspace' || event.key === 'Delete') && - cursorPos > value.length - 5) { - event.preventDefault(); - return false; - } - - // Position cursor before .json on certain keys - if (event.key === 'End' || event.key === 'ArrowRight') { - const nameLength = value.length - 5; - if (cursorPos >= nameLength) { - event.preventDefault(); - input.setSelectionRange(nameLength, nameLength); - return false; - } - } - } - - // Utility functions - function showAlert(message, type = 'info') { - console.log(`${type === 'error' ? 'โŒ' : type === 'warning' ? 'โš ๏ธ' : type === 'success' ? 'โœ…' : 'โ„น๏ธ'} ${message}`); - - // Create visual alert - const alert = document.createElement('div'); - alert.style.cssText = ` - position: fixed; - top: 20px; - right: 20px; - padding: 12px 20px; - border-radius: 8px; - color: white; - font-family: 'Segoe UI', Arial, sans-serif; - font-size: 14px; - font-weight: 500; - z-index: 10000; - box-shadow: 0 4px 12px rgba(0,0,0,0.3); - animation: slideIn 0.3s ease-out; - max-width: 300px; - ${type === 'error' ? 'background: linear-gradient(135deg, #ff4757, #ff3742);' : - type === 'warning' ? 'background: linear-gradient(135deg, #ffa502, #ff6348);' : - type === 'success' ? 'background: linear-gradient(135deg, #26de81, #20bf6b);' : - 'background: linear-gradient(135deg, #3742fa, #2f3542);'} - `; - alert.textContent = message; - - // Add animation styles - if (!document.getElementById('alert-styles')) { - const style = document.createElement('style'); - style.id = 'alert-styles'; - style.textContent = ` - @keyframes slideIn { - from { transform: translateX(100%); opacity: 0; } - to { transform: translateX(0); opacity: 1; } - } - @keyframes slideOut { - from { transform: translateX(0); opacity: 1; } - to { transform: translateX(100%); opacity: 0; } - } - `; - document.head.appendChild(style); - } - - document.body.appendChild(alert); - - // Auto remove after 3 seconds - setTimeout(() => { - alert.style.animation = 'slideOut 0.3s ease-in'; - setTimeout(() => { - if (alert.parentNode) { - alert.parentNode.removeChild(alert); - } - }, 300); - }, 3000); - } - - // UI Creation and Management - async function createUI() { - console.log('๐ŸŽจ Creating Art Extractor UI...'); - - try { - // Remove existing UI - const existing = document.getElementById('art-extractor-ui'); - if (existing) { - console.log('๐Ÿ—‘๏ธ Removing existing UI'); - existing.remove(); - } - - // Check if document body exists - if (!document.body) { - throw new Error('Document body not available'); - } - - console.log('๐Ÿ”จ Building UI container...'); - - // Create main container - const container = document.createElement('div'); - container.id = 'art-extractor-ui'; - container.className = 'wplace-theme-neon-cyan'; - container.style.cssText = ` - position: fixed; - top: 20px; - left: 20px; - width: 320px; - background: #1959A1; - border: 2px solid #81DCF7; - border-radius: 0; - padding: 20px; - font-family: 'Press Start 2P', monospace; - color: #81DCF7; - z-index: 9999; - box-shadow: 0 0 30px rgba(129, 220, 247, 0.6), inset 0 0 30px rgba(234, 156, 0, 0.1); - user-select: none; - `; - - container.innerHTML = ` - -
- - -
-

๐ŸŽจ ART EXTRACTOR

- -
- - -
-
Status: CAPTURING
-
Area: NONE
-
Pixels: 0
-
- - -
- - - -
- - -
- -
- - -
- - - - - -
- - -
- PAINT UPPER-LEFT & LOWER-RIGHT PIXELS -
- - - - - `; - - // Add event listeners - container.querySelector('#art-extractor-close').addEventListener('click', closeArtExtractor); - // Add event listeners - container.querySelector('#art-extractor-close').addEventListener('click', closeArtExtractor); - container.querySelector('#export-autofix').addEventListener('click', () => exportToJSON('autofix')); - container.querySelector('#export-autoimage').addEventListener('click', () => exportToJSON('autoimage')); - container.querySelector('#start-selecting').addEventListener('click', startPixelCapture); - container.querySelector('#clear-selection').addEventListener('click', clearSelection); - - // Set up filename input with .json enforcement - const filenameInput = container.querySelector('#filename-input'); - if (filenameInput) { - // Initialize with default value - filenameInput.value = '.json'; - - // Add event listeners for .json enforcement - filenameInput.addEventListener('input', handleFilenameInput); - filenameInput.addEventListener('keydown', preventJsonDeletion); - filenameInput.addEventListener('focus', function() { - // Position cursor before .json on focus - const nameLength = this.value.length - 5; - if (nameLength >= 0) { - this.setSelectionRange(nameLength, nameLength); - } - }); - - // Set initial cursor position - setTimeout(() => { - filenameInput.setSelectionRange(0, 0); - }, 100); - } - - // Add to page - document.body.appendChild(container); - state.ui = container; - - console.log('โœ… Art Extractor UI created'); - - // Auto-start pixel capture immediately - console.log('๐ŸŽฏ Auto-starting pixel capture...'); - setTimeout(() => { - startPixelCapture(); - }, 500); - updateUI(); - - } catch (error) { - console.error('โŒ Failed to create UI:', error); - throw error; - } - } - - // UI update function - function updateUI() { - if (!state.ui) return; - - const statusElement = state.ui.querySelector('#current-status'); - const areaElement = state.ui.querySelector('#area-info'); - const pixelElement = state.ui.querySelector('#pixel-count'); - const exportAutofixBtn = state.ui.querySelector('#export-autofix'); - const exportAutoimageBtn = state.ui.querySelector('#export-autoimage'); - - // Update status with Neon Cyan styling - if (state.isCapturing) { - statusElement.textContent = `CAPTURING (${state.capturedPixels.length}/${state.requiredPixels})`; - statusElement.style.color = '#EA9C00'; - statusElement.style.textShadow = '0 0 8px #EA9C00'; - } else if (state.isScanning) { - statusElement.textContent = 'SCANNING...'; - statusElement.style.color = '#39ff14'; - statusElement.style.textShadow = '0 0 8px #39ff14'; - } else if (state.selectionArea) { - statusElement.textContent = 'READY TO EXPORT'; - statusElement.style.color = '#39ff14'; - statusElement.style.textShadow = '0 0 8px #39ff14'; - } else { - statusElement.textContent = 'READY'; - statusElement.style.color = '#81DCF7'; - statusElement.style.textShadow = '0 0 5px #81DCF7'; - } - - // Update area info with uppercase styling - if (state.selectionArea) { - areaElement.textContent = `${state.selectionArea.width}ร—${state.selectionArea.height} PX`; - } else if (state.capturedPixels.length > 0) { - areaElement.textContent = `${state.capturedPixels.length}/${state.requiredPixels} CAPTURED`; - } else { - areaElement.textContent = 'NONE'; - } - - // Update pixel count with progress info - if (state.isScanning && state.totalPixels > 0) { - const progress = Math.round((state.scannedPixels.length / state.totalPixels) * 100); - pixelElement.textContent = `${state.scannedPixels.length}/${state.totalPixels} (${progress}%)`; - } else { - pixelElement.textContent = state.scannedPixels.length.toString(); - } - - // Update export button states - const hasScannedData = state.scannedPixels && state.scannedPixels.length > 0; - - if (exportAutofixBtn) { - exportAutofixBtn.disabled = !hasScannedData; - if (!exportAutofixBtn.disabled) { - exportAutofixBtn.style.opacity = '1'; - exportAutofixBtn.style.cursor = 'pointer'; - } else { - exportAutofixBtn.style.opacity = '0.5'; - exportAutofixBtn.style.cursor = 'not-allowed'; - } - } - - if (exportAutoimageBtn) { - exportAutoimageBtn.disabled = !hasScannedData; - if (!exportAutoimageBtn.disabled) { - exportAutoimageBtn.style.opacity = '1'; - exportAutoimageBtn.style.cursor = 'pointer'; - } else { - exportAutoimageBtn.style.opacity = '0.5'; - exportAutoimageBtn.style.cursor = 'not-allowed'; - } - } - } - - // Control functions - function startPixelCapture() { - console.log('๐ŸŽฏ Starting pixel capture mode...'); - - state.isCapturing = true; - state.capturedPixels = []; - state.selectionArea = null; - state.scannedPixels = []; - - // Clear any existing overlay - if (state.overlayManager) { - state.overlayManager.disable(); - } - - // Set up pixel paint monitoring with fetch interception - const setupSuccess = setupPixelCapture(); - - if (setupSuccess) { - updateUI(); - showAlert('๐ŸŽฏ Paint a pixel at the UPPER-LEFT corner of the area you want to extract', 'info'); - - // Set timeout for capture process - setTimeout(() => { - if (state.isCapturing && state.capturedPixels.length === 0) { - showAlert('โฐ No pixels captured yet. Try painting a pixel or use "Enter Coordinates Manually"', 'warning'); - } - }, 15000); - } else { - showAlert('โš ๏ธ Could not set up automatic capture. Use "Enter Coordinates Manually"', 'warning'); - updateUI(); - } - } - - function clearSelection() { - console.log('๐Ÿงน Clearing pixel capture...'); - - state.isCapturing = false; - state.capturedPixels = []; - state.selectionArea = null; - state.scannedPixels = []; - - // Restore original fetch function - restoreFetch(); - - // Disable overlay - if (state.overlayManager) { - state.overlayManager.disable(); - } - - updateUI(); - showAlert('Pixel capture cleared', 'info'); - } - - function closeArtExtractor() { - console.log('๐Ÿ‘‹ Closing Art Extractor...'); - - // Stop capturing - state.isCapturing = false; - - // Restore original fetch function - restoreFetch(); - - // Clean up overlay - if (state.overlayManager) { - state.overlayManager.disable(); - } - - // Remove UI - if (state.ui) { - state.ui.remove(); - } - - // Reset state - Object.assign(state, { - isCapturing: false, - capturedPixels: [], - selectionArea: null, - ui: null, - overlayManager: null, - isScanning: false, - scannedPixels: [], - paintEventListener: null, - lastPaintEvent: null - }); - - console.log('โœ… Art Extractor closed'); - } - - // Main initialization - async function initialize() { - console.log('๐Ÿš€ Initializing Art Extractor...'); - - try { - // Create UI immediately - don't wait for dependencies - console.log('๐ŸŽจ Creating UI first...'); - await createUI(); - - // Then try to set up dependencies in background - setTimeout(async () => { - try { - // Wait for Auto-Image overlay manager with timeout - if (!window.autoImageOverlayManager) { - console.log('โณ Waiting for Auto-Image overlay manager...'); - await new Promise((resolve, reject) => { - let attempts = 0; - const maxAttempts = 50; // 5 seconds timeout - const check = () => { - attempts++; - if (window.autoImageOverlayManager) { - console.log('โœ… Auto-Image overlay manager found'); - resolve(); - } else if (attempts >= maxAttempts) { - console.warn('โš ๏ธ Auto-Image overlay manager not found, proceeding without it'); - resolve(); // Continue anyway - } else { - setTimeout(check, 100); - } - }; - check(); - }); - } - - // Set up pixel capture monitoring - try { - const captureReady = setupPixelCapture(); - if (captureReady) { - console.log('โœ… Pixel capture monitoring ready'); - } else { - console.warn('โš ๏ธ Pixel capture monitoring setup failed'); - } - } catch (error) { - console.warn('โš ๏ธ Pixel capture error:', error); - } - - } catch (error) { - console.warn('โš ๏ธ Background initialization error:', error); - } - }, 1000); - - console.log('โœ… Art Extractor initialized successfully'); - showAlert('Art Extractor ready! UI loaded - dependencies loading in background.', 'success'); - - } catch (error) { - console.error('โŒ Failed to initialize Art Extractor:', error); - - // Try to create UI anyway as fallback - try { - await createUI(); - showAlert(`Art Extractor loaded with limited functionality: ${error.message}`, 'warning'); - } catch (uiError) { - console.error('โŒ Failed to create UI:', uiError); - showAlert(`Critical initialization failure: ${error.message}`, 'error'); - } - } - } - - // Start initialization with additional error handling - try { - await initialize(); - } catch (error) { - console.error('โŒ Critical error during Art Extractor initialization:', error); - - // Final fallback - try to show minimal UI - try { - const container = document.createElement('div'); - container.innerHTML = ` -
- โŒ Art Extractor failed to load: ${error.message} - -
- `; - document.body.appendChild(container); - } catch (e) { - console.error('โŒ Even fallback UI failed:', e); - } - } - - // Expose globally for extension integration - window.WPlaceArtExtractor = { - state, - initialize, - closeArtExtractor, - exportToJSON, - scanSelectedArea - }; - - console.log('๐ŸŽจ WPlace Art Extractor loaded successfully!'); +// ==UserScript== +// @name WPlace Art Extractor +// @namespace http://tampermonkey.net/ +// @version 2025-09-20.1 +// @description Extract artwork areas to JSON for Auto-Repair.js +// @author Wbot +// @match https://wplace.live/* +// @grant none +// @icon +// ==/UserScript== + +localStorage.removeItem("lp"); + +// Fallback translation function for when utils manager isn't loaded +function getText(key, params) { + // Try to get translation from loadedTranslations + try { + if (window.loadedTranslations && window.loadedTranslations[window.state?.language || 'en']) { + const translation = window.loadedTranslations[window.state?.language || 'en'][key]; + if (translation && typeof translation === 'string') { + if (params && typeof params === 'object') { + return translation.replace(/\{(\w+)\}/g, (match, paramKey) => params[paramKey] || match); + } + return translation; + } + } + } catch (error) { + console.warn('Translation lookup error:', error); + } + + return key; // Fallback to key if no translation found +} + +;(async () => { + // Prevent multiple instances of this script from running + if (window.WPLACE_ART_EXTRACTOR_LOADED) { + console.log('Art Extractor already loaded, skipping...'); + return; + } + window.WPLACE_ART_EXTRACTOR_LOADED = true; + + console.log('%c๐ŸŽจ WPlace Art Extractor Starting...', 'color: #ff6b35; font-weight: bold; font-size: 16px;'); + console.log('%cโœจ Interactive pixel capture for area extraction!', 'color: #26de81; font-weight: bold;'); + + // Immediate visual confirmation that script is running + try { + const testDiv = document.createElement('div'); + testDiv.innerHTML = ` +
+ ๐ŸŽจ Art Extractor Loading... +
+ `; + document.body.appendChild(testDiv); + + // Remove after 3 seconds + setTimeout(() => { + if (testDiv.parentNode) { + testDiv.parentNode.removeChild(testDiv); + } + }, 3000); + } catch (e) { + console.log('Could not create test notification:', e); + } + + // CONFIGURATION CONSTANTS + const CONFIG = { + OVERLAY_OPACITY: 0.7, + SELECTION_COLOR: { r: 50, g: 200, b: 50, a: 180 }, // Green extraction overlay + CORNER_MARKER_COLOR: { r: 255, g: 255, b: 0, a: 255 }, // Yellow corner markers + CORNER_MARKER_SIZE: 12, // Corner marker size + MIN_AREA_SIZE: 1, + MAX_AREA_SIZE: 2500, + COLOR_MAP: { + 0: { id: 1, name: 'Black', rgb: { r: 0, g: 0, b: 0 } }, + 1: { id: 2, name: 'Dark Gray', rgb: { r: 60, g: 60, b: 60 } }, + 2: { id: 3, name: 'Gray', rgb: { r: 120, g: 120, b: 120 } }, + 3: { id: 4, name: 'Light Gray', rgb: { r: 210, g: 210, b: 210 } }, + 4: { id: 5, name: 'White', rgb: { r: 255, g: 255, b: 255 } }, + 5: { id: 6, name: 'Deep Red', rgb: { r: 96, g: 0, b: 24 } }, + 6: { id: 7, name: 'Red', rgb: { r: 237, g: 28, b: 36 } }, + 7: { id: 8, name: 'Orange', rgb: { r: 255, g: 127, b: 39 } }, + 8: { id: 9, name: 'Gold', rgb: { r: 246, g: 170, b: 9 } }, + 9: { id: 10, name: 'Yellow', rgb: { r: 249, g: 221, b: 59 } }, + 10: { id: 11, name: 'Light Yellow', rgb: { r: 255, g: 250, b: 188 } }, + 11: { id: 12, name: 'Dark Green', rgb: { r: 14, g: 185, b: 104 } }, + 12: { id: 13, name: 'Green', rgb: { r: 19, g: 230, b: 123 } }, + 13: { id: 14, name: 'Light Green', rgb: { r: 135, g: 255, b: 94 } }, + 14: { id: 15, name: 'Dark Teal', rgb: { r: 12, g: 129, b: 110 } }, + 15: { id: 16, name: 'Teal', rgb: { r: 16, g: 174, b: 166 } }, + 16: { id: 17, name: 'Light Teal', rgb: { r: 19, g: 225, b: 190 } }, + 17: { id: 20, name: 'Cyan', rgb: { r: 96, g: 247, b: 242 } }, + 18: { id: 44, name: 'Light Cyan', rgb: { r: 187, g: 250, b: 242 } }, + 19: { id: 18, name: 'Dark Blue', rgb: { r: 40, g: 80, b: 158 } }, + 20: { id: 19, name: 'Blue', rgb: { r: 64, g: 147, b: 228 } }, + 21: { id: 21, name: 'Indigo', rgb: { r: 107, g: 80, b: 246 } }, + 22: { id: 22, name: 'Light Indigo', rgb: { r: 153, g: 177, b: 251 } }, + 23: { id: 23, name: 'Dark Purple', rgb: { r: 120, g: 12, b: 153 } }, + 24: { id: 24, name: 'Purple', rgb: { r: 170, g: 56, b: 185 } }, + 25: { id: 25, name: 'Light Purple', rgb: { r: 224, g: 159, b: 249 } }, + 26: { id: 26, name: 'Dark Pink', rgb: { r: 203, g: 0, b: 122 } }, + 27: { id: 27, name: 'Pink', rgb: { r: 236, g: 31, b: 128 } }, + 28: { id: 28, name: 'Light Pink', rgb: { r: 243, g: 141, b: 169 } }, + 29: { id: 29, name: 'Dark Brown', rgb: { r: 104, g: 70, b: 52 } }, + 30: { id: 30, name: 'Brown', rgb: { r: 149, g: 104, b: 42 } }, + 31: { id: 31, name: 'Beige', rgb: { r: 248, g: 178, b: 119 } }, + 32: { id: 52, name: 'Light Beige', rgb: { r: 255, g: 197, b: 165 } }, + 33: { id: 32, name: 'Medium Gray', rgb: { r: 170, g: 170, b: 170 } }, + 34: { id: 33, name: 'Dark Red', rgb: { r: 165, g: 14, b: 30 } }, + 35: { id: 34, name: 'Light Red', rgb: { r: 250, g: 128, b: 114 } }, + 36: { id: 35, name: 'Dark Orange', rgb: { r: 228, g: 92, b: 26 } }, + 37: { id: 37, name: 'Dark Goldenrod', rgb: { r: 156, g: 132, b: 49 } }, + 38: { id: 38, name: 'Goldenrod', rgb: { r: 197, g: 173, b: 49 } }, + 39: { id: 39, name: 'Light Goldenrod', rgb: { r: 232, g: 212, b: 95 } }, + 40: { id: 40, name: 'Dark Olive', rgb: { r: 74, g: 107, b: 58 } }, + 41: { id: 41, name: 'Olive', rgb: { r: 90, g: 148, b: 74 } }, + 42: { id: 42, name: 'Light Olive', rgb: { r: 132, g: 197, b: 115 } }, + 43: { id: 43, name: 'Dark Cyan', rgb: { r: 15, g: 121, b: 159 } }, + 44: { id: 45, name: 'Light Blue', rgb: { r: 125, g: 199, b: 255 } }, + 45: { id: 46, name: 'Dark Indigo', rgb: { r: 77, g: 49, b: 184 } }, + 46: { id: 47, name: 'Dark Slate Blue', rgb: { r: 74, g: 66, b: 132 } }, + 47: { id: 48, name: 'Slate Blue', rgb: { r: 122, g: 113, b: 196 } }, + 48: { id: 49, name: 'Light Slate Blue', rgb: { r: 181, g: 174, b: 241 } }, + 49: { id: 53, name: 'Dark Peach', rgb: { r: 155, g: 82, b: 73 } }, + 50: { id: 54, name: 'Peach', rgb: { r: 209, g: 128, b: 120 } }, + 51: { id: 55, name: 'Light Peach', rgb: { r: 250, g: 182, b: 164 } }, + 52: { id: 50, name: 'Light Brown', rgb: { r: 219, g: 164, b: 99 } }, + 53: { id: 56, name: 'Dark Tan', rgb: { r: 123, g: 99, b: 82 } }, + 54: { id: 57, name: 'Tan', rgb: { r: 156, g: 132, b: 107 } }, + 55: { id: 36, name: 'Light Tan', rgb: { r: 214, g: 181, b: 148 } }, + 56: { id: 51, name: 'Dark Beige', rgb: { r: 209, g: 128, b: 81 } }, + 57: { id: 61, name: 'Dark Stone', rgb: { r: 109, g: 100, b: 63 } }, + 58: { id: 62, name: 'Stone', rgb: { r: 148, g: 140, b: 107 } }, + 59: { id: 63, name: 'Light Stone', rgb: { r: 205, g: 197, b: 158 } }, + 60: { id: 58, name: 'Dark Slate', rgb: { r: 51, g: 57, b: 65 } }, + 61: { id: 59, name: 'Slate', rgb: { r: 109, g: 117, b: 141 } }, + 62: { id: 60, name: 'Light Slate', rgb: { r: 179, g: 185, b: 209 } }, + 63: { id: 0, name: 'Transparent', rgb: null }, + } + }; + + // Expose CONFIG globally for the utils manager and other modules + window.ART_EXTRACTOR_CONFIG = CONFIG; + + // GLOBAL STATE + const state = { + // Pixel capture state + isCapturing: false, + capturedPixels: [], // Array of painted pixels with their world coordinates + requiredPixels: 2, // Need 2 corner pixels + + // Selection area + selectionArea: null, // { x, y, width, height, regionX, regionY } + + // UI state + ui: null, + overlayManager: null, // Will use global overlay manager like Auto-Image + + // Scanning state + isScanning: false, + scannedPixels: [], + totalPixels: 0, + + // Auto-Image integration state + imageData: null, + startPosition: null, + region: null, + + // Canvas monitoring + paintEventListener: null, + lastPaintEvent: null, + + language: 'en', + }; + + // Expose state globally + window.ART_EXTRACTOR_STATE = state; + + // Use Auto-Image's overlay system - no custom overlay manager needed + // We'll integrate with the global overlay manager that Auto-Image uses + + // Monitor WPlace paint events to capture pixel coordinates using fetch interception + let originalFetch = null; + let fetchInterceptionActive = false; + + function setupPixelCapture() { + console.log('๐ŸŽฏ Setting up pixel paint monitoring...'); + + try { + // Store original fetch if not already stored + if (!originalFetch) { + originalFetch = window.fetch; + } + + // Install fetch interceptor for pixel capture + if (!fetchInterceptionActive) { + window.fetch = async (url, options) => { + // Check if this is a pixel painting request + if (state.isCapturing && + typeof url === 'string' && + url.includes('/s0/pixel/') && + options && + options.method === 'POST') { + + try { + console.log('๐ŸŽฏ Intercepting pixel paint request:', url); + + // Call original fetch first + const response = await originalFetch(url, options); + + // If request was successful and we have body data, extract coordinates + if (response.ok && options.body) { + let requestBody; + try { + requestBody = JSON.parse(options.body); + } catch (parseError) { + console.warn('Could not parse request body:', parseError); + return response; + } + + // Extract coordinates from request + if (requestBody.coords && Array.isArray(requestBody.coords) && requestBody.coords.length >= 2) { + const localX = requestBody.coords[0]; // 0-999 within tile + const localY = requestBody.coords[1]; // 0-999 within tile + + // Extract tile coordinates from URL: /s0/pixel/{tileX}/{tileY} + const tileMatch = url.match(/\/s0\/pixel\/(\-?\d+)\/(\-?\d+)/); + if (tileMatch) { + const tileX = parseInt(tileMatch[1]); + const tileY = parseInt(tileMatch[2]); + + // Convert to global canvas coordinates + const globalX = tileX * 1000 + localX; + const globalY = tileY * 1000 + localY; + + console.log(`๐ŸŽฏ Captured painted pixel: (${globalX}, ${globalY}) from tile (${tileX}, ${tileY}) local (${localX}, ${localY})`); + + // Add the captured pixel + setTimeout(() => { + addCapturedPixel(globalX, globalY); + }, 100); + } + } + } + + return response; + } catch (error) { + console.error('โŒ Error intercepting pixel paint:', error); + // Fallback to original fetch + return originalFetch(url, options); + } + } + + // For all other requests, use original fetch + return originalFetch(url, options); + }; + + fetchInterceptionActive = true; + console.log('โœ… Fetch interception enabled'); + } + + return true; + + } catch (error) { + console.warn('โš ๏ธ Could not set up paint monitoring:', error); + return false; + } + } + + // Restore original fetch function + function restoreFetch() { + if (originalFetch && fetchInterceptionActive) { + window.fetch = originalFetch; + fetchInterceptionActive = false; + console.log('๐Ÿ”„ Original fetch restored'); + } + } + + // Handle canvas clicks during capture mode (fallback method) + function handleCanvasClick(event) { + if (!state.isCapturing) return; + + // Only handle clicks on canvas elements + const canvas = event.target.closest('canvas'); + if (!canvas) return; + + console.log('๏ฟฝ Canvas click detected during capture mode'); + + // For now, we'll use manual coordinate input as the primary method + // since intercepting the actual paint events is complex + setTimeout(() => { + if (state.isCapturing && state.capturedPixels.length < state.requiredPixels) { + promptForCoordinates(); + } + }, 100); + } + + // Handle detected paint events (for future implementation) + function handlePaintEvent(event) { + if (!state.isCapturing) return; + + console.log('๐ŸŽฏ Processing paint event:', event); + + // Extract coordinates from paint event + let worldX, worldY; + + if (event.x !== undefined && event.y !== undefined) { + worldX = event.x; + worldY = event.y; + } else if (event.position) { + worldX = event.position.x; + worldY = event.position.y; + } else if (event.coords) { + worldX = event.coords[0]; + worldY = event.coords[1]; + } else { + console.warn('โš ๏ธ Could not extract coordinates from paint event'); + return; + } + + // Add the painted pixel to our capture list + addCapturedPixel(worldX, worldY); + } + + // Manual coordinate input (fallback method when auto-capture doesn't work) + function promptForCoordinates() { + if (!state.isCapturing) { + showAlert('Not in capture mode', 'warning'); + return; + } + + const pixelNum = state.capturedPixels.length + 1; + const cornerType = pixelNum === 1 ? 'UPPER-LEFT' : 'LOWER-RIGHT'; + + const coordStr = prompt( + `๐Ÿ“ Enter coordinates for ${cornerType} corner (${pixelNum}/${state.requiredPixels})\n\n` + + `Format: x,y (e.g., 1000,500)\n\n` + + `To find coordinates:\n` + + `1. Navigate to the ${cornerType} corner of your area\n` + + `2. Hover over the exact pixel position\n` + + `3. Check coordinates in bottom-left corner of WPlace\n` + + `4. Enter them here:\n\n` + + `Tip: You can also just paint a pixel there and it will be captured automatically!` + ); + + if (coordStr && coordStr.trim()) { + const coords = coordStr.trim().split(',').map(n => parseInt(n.trim())); + if (coords.length === 2 && !isNaN(coords[0]) && !isNaN(coords[1])) { + addCapturedPixel(coords[0], coords[1]); + } else { + showAlert('Invalid coordinates format. Use: x,y (e.g., 1000,500)', 'error'); + // Try again + setTimeout(() => promptForCoordinates(), 500); + } + } else if (coordStr !== null) { + // User entered empty string, try again + showAlert('Please enter coordinates', 'warning'); + setTimeout(() => promptForCoordinates(), 500); + } + // If user clicked cancel (coordStr === null), do nothing + } + + // Add a captured pixel with enhanced validation + function addCapturedPixel(worldX, worldY) { + if (!state.isCapturing) { + console.log('Not in capture mode, ignoring pixel'); + return; + } + + if (state.capturedPixels.length >= state.requiredPixels) { + showAlert('Already have enough pixels', 'warning'); + return; + } + + // Validate coordinates + if (!Number.isFinite(worldX) || !Number.isFinite(worldY)) { + showAlert('Invalid pixel coordinates', 'error'); + return; + } + + // Calculate region and local coordinates + const regionX = Math.floor(worldX / 1000); + const regionY = Math.floor(worldY / 1000); + const pixelX = worldX % 1000; + const pixelY = worldY % 1000; + + const pixel = { + worldX, + worldY, + regionX, + regionY, + pixelX, + pixelY, + timestamp: Date.now(), + cornerType: state.capturedPixels.length === 0 ? 'upperLeft' : 'lowerRight' + }; + + state.capturedPixels.push(pixel); + console.log(`๐ŸŽฏ Captured ${pixel.cornerType} pixel ${state.capturedPixels.length}/${state.requiredPixels}:`, pixel); + + updateUI(); + + // Progress messages + if (state.capturedPixels.length === 1) { + showAlert(`โœ… Upper-left corner captured: (${worldX}, ${worldY})`, 'success'); + setTimeout(() => { + showAlert('๐ŸŽฏ Now paint a pixel at the LOWER-RIGHT corner of the area', 'info'); + }, 1500); + } else if (state.capturedPixels.length >= state.requiredPixels) { + showAlert(`โœ… Lower-right corner captured: (${worldX}, ${worldY})`, 'success'); + // Small delay before auto-scanning to show the success message + setTimeout(() => { + completeAreaCapture(); + // Auto-trigger scan after area capture is complete + setTimeout(() => { + scanSelectedArea(); + }, 500); + }, 1000); + } + } + + // Complete area capture when we have 2 pixels with validation + function completeAreaCapture() { + state.isCapturing = false; + restoreFetch(); // Restore original fetch function + + if (state.capturedPixels.length < 2) { + showAlert('Need at least 2 pixels to define area', 'error'); + return; + } + + // Calculate bounding box from captured pixels + const pixels = state.capturedPixels; + const minX = Math.min(...pixels.map(p => p.worldX)); + const minY = Math.min(...pixels.map(p => p.worldY)); + const maxX = Math.max(...pixels.map(p => p.worldX)); + const maxY = Math.max(...pixels.map(p => p.worldY)); + + // Validation: ensure upper-left is actually upper-left + if (minX >= maxX || minY >= maxY) { + showAlert('โŒ Invalid area: upper-left corner must be less than lower-right corner', 'error'); + // Reset capture to try again + state.capturedPixels = []; + startPixelCapture(); + return; + } + + const width = maxX - minX + 1; + const height = maxY - minY + 1; + + // Size validation + if (width < CONFIG.MIN_AREA_SIZE || height < CONFIG.MIN_AREA_SIZE) { + showAlert(`โŒ Area too small: ${width}ร—${height} pixels (minimum: ${CONFIG.MIN_AREA_SIZE}ร—${CONFIG.MIN_AREA_SIZE})`, 'error'); + return; + } + + if (width > CONFIG.MAX_AREA_SIZE || height > CONFIG.MAX_AREA_SIZE) { + showAlert(`โŒ Area too large: ${width}ร—${height} pixels (maximum: ${CONFIG.MAX_AREA_SIZE}ร—${CONFIG.MAX_AREA_SIZE})`, 'error'); + return; + } + + state.selectionArea = { + x1: minX, // Coordinate naming + y1: minY, + x2: maxX, + y2: maxY, + x: minX, // Keep compatibility + y: minY, + width: width, + height: height, + regionX: Math.floor(minX / 1000), + regionY: Math.floor(minY / 1000) + }; + + console.log('๐Ÿ“ Area capture complete:', state.selectionArea); + showAlert(`โœ… Area captured: ${width}ร—${height} pixels from (${minX},${minY}) to (${maxX},${maxY})`, 'success'); + + // Set up overlay using Auto-Image's system + setTimeout(() => { + setupAutoImageOverlay(); + }, 500); + + updateUI(); + } + + // Set up visual overlay for selected extraction area (simplified approach) + async function setupAutoImageOverlay() { + if (!state.selectionArea) return; + + console.log('๐ŸŽจ Setting up extraction area overlay...'); + + try { + const { x1, y1, x2, y2, width, height, regionX, regionY } = state.selectionArea; + + // Try to use Auto-Image's overlay if available, otherwise create simple visual feedback + if (window.autoImageOverlayManager) { + console.log('๐Ÿ“Š Using Auto-Image overlay manager for area visualization'); + + const overlayMgr = window.autoImageOverlayManager; + + // Create overlay canvas showing the selected area + const canvas = new OffscreenCanvas(width, height); + const ctx = canvas.getContext('2d'); + + // Fill with extraction area overlay + ctx.fillStyle = `rgba(${CONFIG.SELECTION_COLOR.r}, ${CONFIG.SELECTION_COLOR.g}, ${CONFIG.SELECTION_COLOR.b}, 0.3)`; + ctx.fillRect(0, 0, width, height); + + // Add extraction border + ctx.strokeStyle = `rgba(${CONFIG.SELECTION_COLOR.r}, ${CONFIG.SELECTION_COLOR.g}, ${CONFIG.SELECTION_COLOR.b}, 0.8)`; + ctx.lineWidth = 2; + ctx.strokeRect(1, 1, width - 2, height - 2); + + // Add corner markers + const cornerSize = CONFIG.CORNER_MARKER_SIZE; + ctx.fillStyle = `rgba(${CONFIG.CORNER_MARKER_COLOR.r}, ${CONFIG.CORNER_MARKER_COLOR.g}, ${CONFIG.CORNER_MARKER_COLOR.b}, 1)`; + + // Top-left corner + ctx.fillRect(0, 0, cornerSize, cornerSize); + // Top-right corner + ctx.fillRect(width - cornerSize, 0, cornerSize, cornerSize); + // Bottom-left corner + ctx.fillRect(0, height - cornerSize, cornerSize, cornerSize); + // Bottom-right corner + ctx.fillRect(width - cornerSize, height - cornerSize, cornerSize, cornerSize); + + try { + // Create image bitmap + const overlayBitmap = await canvas.transferToImageBitmap(); + + // Set the overlay using Auto-Image's API + await overlayMgr.setImage(overlayBitmap); + await overlayMgr.setPosition( + { x: x1 % 1000, y: y1 % 1000 }, + { x: regionX, y: regionY } + ); + + // Enable the overlay + overlayMgr.enable(); + + // Store references for cleanup + state.overlayManager = overlayMgr; + state.imageData = overlayBitmap; + state.startPosition = { x: x1 % 1000, y: y1 % 1000 }; + state.region = { x: regionX, y: regionY }; + + console.log('โœ… Extraction area overlay enabled'); + } catch (overlayError) { + console.warn('โš ๏ธ Could not set overlay, continuing without visual feedback:', overlayError); + } + + } else { + console.log('๐Ÿ“‹ Auto-Image overlay not available, using console feedback only'); + } + + } catch (error) { + console.error('โŒ Failed to setup extraction area overlay:', error); + // Continue without overlay - not critical for extraction functionality + } + } + // Tile-based pixel scanning for area extraction + async function scanSelectedArea() { + if (!state.selectionArea) { + showAlert('No area selected', 'error'); + return; + } + + console.log('๐Ÿ” Starting area scan...'); + state.isScanning = true; + state.scannedPixels = []; + updateUI(); + + try { + const { x1, y1, x2, y2 } = state.selectionArea; + const areaWidth = x2 - x1 + 1; + const areaHeight = y2 - y1 + 1; + + console.log(`๐Ÿ” Analyzing area ${areaWidth}x${areaHeight} from (${x1},${y1}) to (${x2},${y2})`); + + // Tile calculation + const startTileX = Math.floor(x1 / 1000); + const startTileY = Math.floor(y1 / 1000); + const endTileX = Math.floor(x2 / 1000); + const endTileY = Math.floor(y2 / 1000); + + state.totalPixels = areaWidth * areaHeight; + let processedPixels = 0; + + // Process each tile that intersects with our area + for (let tileY = startTileY; tileY <= endTileY; tileY++) { + for (let tileX = startTileX; tileX <= endTileX; tileX++) { + try { + console.log(`๐Ÿ“„ Processing tile (${tileX}, ${tileY})...`); + + // Download tile data + const tileBlob = await getTileImage(tileX, tileY); + if (!tileBlob) { + console.warn(`โš ๏ธ Could not download tile ${tileX},${tileY}, skipping...`); + continue; + } + + // Process tile data + const tileImageData = await processTileBlob(tileBlob); + if (!tileImageData) { + console.warn(`โš ๏ธ Could not process tile ${tileX},${tileY}, skipping...`); + continue; + } + + // Calculate intersection between area and current tile + const tileStartX = tileX * 1000; + const tileStartY = tileY * 1000; + const tileEndX = tileStartX + 1000; + const tileEndY = tileStartY + 1000; + + const intersectStartX = Math.max(x1, tileStartX); + const intersectStartY = Math.max(y1, tileStartY); + const intersectEndX = Math.min(x2 + 1, tileEndX); + const intersectEndY = Math.min(y2 + 1, tileEndY); + + // Extract pixels from intersection area + for (let globalY = intersectStartY; globalY < intersectEndY; globalY++) { + for (let globalX = intersectStartX; globalX < intersectEndX; globalX++) { + // Convert to tile-local coordinates + const localX = globalX - tileStartX; + const localY = globalY - tileStartY; + + // Normalize coordinates + const normalizedX = (localX % 1000 + 1000) % 1000; + const normalizedY = (localY % 1000 + 1000) % 1000; + + if (normalizedX >= 0 && normalizedX < 1000 && + normalizedY >= 0 && normalizedY < 1000 && + normalizedX < tileImageData.width && + normalizedY < tileImageData.height) { + + // Extract RGBA values + const pixelIndex = (normalizedY * tileImageData.width + normalizedX) * 4; + const r = tileImageData.data[pixelIndex]; + const g = tileImageData.data[pixelIndex + 1]; + const b = tileImageData.data[pixelIndex + 2]; + const a = tileImageData.data[pixelIndex + 3]; + + if (a > 0) { // Skip transparent pixels + // Find closest color in WPlace palette + const closestColor = findClosestColor(r, g, b); + + if (closestColor) { + // Convert to area-relative coordinates + const areaX = globalX - x1; + const areaY = globalY - y1; + + state.scannedPixels.push({ + x: areaX, + y: areaY, + worldX: globalX, + worldY: globalY, + color: closestColor, + rgb: { r, g, b, a } + }); + } + } + } + + processedPixels++; + } + } + + // Update progress every tile + updateUI(); + await new Promise(resolve => setTimeout(resolve, 1)); + + } catch (error) { + console.error(`โŒ Error processing tile ${tileX},${tileY}:`, error); + } + } + } + + console.log(`โœ… Scan complete: ${state.scannedPixels.length} pixels extracted`); + showAlert(`โœ… Extracted ${state.scannedPixels.length} pixels from area`, 'success'); + + } catch (error) { + console.error('โŒ Area extraction failed:', error); + showAlert(`โŒ Extraction failed: ${error.message}`, 'error'); + } finally { + state.isScanning = false; + updateUI(); + } + } + + // Download tile images + async function getTileImage(tileX, tileY) { + try { + const tileUrl = `https://backend.wplace.live/files/s0/tiles/${tileX}/${tileY}.png`; + const response = await fetch(tileUrl); + if (!response.ok) { + throw new Error(`HTTP ${response.status}`); + } + return await response.blob(); + } catch (error) { + console.warn(`Error downloading tile ${tileX},${tileY}:`, error); + return null; + } + } + + // Process tile blob into ImageData + async function processTileBlob(blob) { + try { + const img = new Image(); + const canvas = document.createElement('canvas'); + const ctx = canvas.getContext('2d'); + + return new Promise((resolve, reject) => { + img.onload = () => { + canvas.width = img.width; + canvas.height = img.height; + ctx.drawImage(img, 0, 0); + const imageData = ctx.getImageData(0, 0, canvas.width, canvas.height); + resolve(imageData); + }; + img.onerror = reject; + img.src = URL.createObjectURL(blob); + }); + } catch (error) { + console.error('Error processing tile blob:', error); + return null; + } + } + + // Color matching using LAB color space for better accuracy + function findClosestColor(r, g, b) { + if (!CONFIG.COLOR_MAP) return null; + + let closestColor = null; + let minDistance = Infinity; + + // Convert input RGB to LAB color space + const inputLab = rgbToLab(r, g, b); + + for (const [index, colorInfo] of Object.entries(CONFIG.COLOR_MAP)) { + if (!colorInfo.rgb) continue; // Skip transparent + + const { r: cr, g: cg, b: cb } = colorInfo.rgb; + + // Convert palette color to LAB + const paletteLab = rgbToLab(cr, cg, cb); + + // Calculate Delta-E distance + const deltaE = calculateDeltaE(inputLab, paletteLab); + + if (deltaE < minDistance) { + minDistance = deltaE; + closestColor = colorInfo; + } + } + + return closestColor; + } + + // RGB to LAB color space conversion + function rgbToLab(r, g, b) { + // Normalize RGB values + r = r / 255; + g = g / 255; + b = b / 255; + + // Apply gamma correction + r = r > 0.04045 ? Math.pow((r + 0.055) / 1.055, 2.4) : r / 12.92; + g = g > 0.04045 ? Math.pow((g + 0.055) / 1.055, 2.4) : g / 12.92; + b = b > 0.04045 ? Math.pow((b + 0.055) / 1.055, 2.4) : b / 12.92; + + // Convert to XYZ + const x = r * 0.4124564 + g * 0.3575761 + b * 0.1804375; + const y = r * 0.2126729 + g * 0.7151522 + b * 0.072175; + const z = r * 0.0193339 + g * 0.119192 + b * 0.9503041; + + // Convert XYZ to LAB + const xn = x / 0.95047; + const yn = y / 1.00000; + const zn = z / 1.08883; + + const fx = xn > 0.008856 ? Math.pow(xn, 1/3) : (7.787 * xn + 16/116); + const fy = yn > 0.008856 ? Math.pow(yn, 1/3) : (7.787 * yn + 16/116); + const fz = zn > 0.008856 ? Math.pow(zn, 1/3) : (7.787 * zn + 16/116); + + const l = 116 * fy - 16; + const a = 500 * (fx - fy); + const bLab = 200 * (fy - fz); + + return { l, a, b: bLab }; + } + + // Delta-E color difference calculation + function calculateDeltaE(lab1, lab2) { + const deltaL = lab1.l - lab2.l; + const deltaA = lab1.a - lab2.a; + const deltaB = lab1.b - lab2.b; + + return Math.sqrt(deltaL * deltaL + deltaA * deltaA + deltaB * deltaB); + } + + // Export to JSON (Auto-Image compatible format for Auto-Repair) + function exportToJSON(exportType = 'autofix') { + if (!state.selectionArea || state.scannedPixels.length === 0) { + showAlert('No data to export. Please select an area and scan it first.', 'error'); + return; + } + + console.log(`๐Ÿ“ค Exporting ${state.scannedPixels.length} pixels in ${exportType} format...`); + console.log('๐Ÿ” [DEBUG] Selection area:', state.selectionArea); + console.log('๐Ÿ” [DEBUG] Scanned pixels count:', state.scannedPixels.length); + + try { + // Get filename from input + const filenameInput = state.ui?.querySelector('#filename-input'); + const userFilename = filenameInput?.value?.trim() || '.json'; + let filename; + + if (userFilename === '.json' || userFilename === '') { + // Use default filename with timestamp + const timestamp = new Date().toISOString().slice(0, 19).replace(/:/g, '-'); + filename = `wplace-extracted-${exportType}-${timestamp}.json`; + } else { + // Ensure filename ends with .json + filename = userFilename.endsWith('.json') ? userFilename : userFilename + '.json'; + } + + // Build Auto-Image compatible data structure + const { x1, y1, x2, y2 } = state.selectionArea; + const width = x2 - x1 + 1; + const height = y2 - y1 + 1; + + // Convert scanned pixels to Auto-Image pixel format (Uint8ClampedArray) + const imagePixels = new Uint8ClampedArray(width * height * 4); + imagePixels.fill(0); // Initialize with transparent pixels + + // Fill pixel data from scanned pixels + for (const pixel of state.scannedPixels) { + const pixelIndex = (pixel.y * width + pixel.x) * 4; + if (pixelIndex >= 0 && pixelIndex < imagePixels.length - 3) { + const rgb = pixel.color?.rgb || pixel.rgb; + imagePixels[pixelIndex] = rgb.r; + imagePixels[pixelIndex + 1] = rgb.g; + imagePixels[pixelIndex + 2] = rgb.b; + imagePixels[pixelIndex + 3] = rgb.a || 255; + } + } + + // Create different formats based on export type + let saveData; + + if (exportType === 'autofix') { + // Auto-Fix format: Include exact position and region for restoration + saveData = { + timestamp: Date.now(), + version: '2.2', + state: { + totalPixels: width * height, + paintedPixels: 0, + lastPosition: { x: 0, y: 0 }, + startPosition: { x: x1 % 1000, y: y1 % 1000 }, + region: { x: Math.floor(x1 / 1000), y: Math.floor(y1 / 1000) }, + imageLoaded: true, + colorsChecked: true, + coordinateMode: 'rows', + coordinateDirection: 'top-left', + coordinateSnake: false, + blockWidth: 1, + blockHeight: 1, + availableColors: Object.values(CONFIG.COLOR_MAP).filter(c => c.rgb).map(color => ({ + ...color, + rgb: [color.rgb.r, color.rgb.g, color.rgb.b] // Convert RGB object to array for Auto-Image compatibility + })), + // Additional Auto-Image required fields for compatibility + paintWhitePixels: true, + paintTransparentPixels: false, + displayCharges: 0, + preciseCurrentCharges: 0, + maxCharges: 1, + cooldown: 30000, + stopFlag: false, + selectingPosition: false, + minimized: false, + estimatedTime: 0, + language: 'en', + paintingSpeed: 5, + batchMode: 'normal', + randomBatchMin: 1, + randomBatchMax: 5, + cooldownChargeThreshold: 10, + overlayOpacity: 0.7, + blueMarbleEnabled: false, + ditheringEnabled: true, + colorMatchingAlgorithm: 'lab', + enableChromaPenalty: true, + chromaPenaltyWeight: 0.15, + customTransparencyThreshold: 128, + customWhiteThreshold: 230, + paintUnavailablePixels: true, + // Critical missing fields that Auto-Image expects + fullChargeData: null, + fullChargeInterval: null, + tokenSource: 'generator', + initialSetupComplete: true, + resizeSettings: null, + originalImage: null, + resizeIgnoreMask: null + }, + imageData: { + width: width, + height: height, + pixels: Array.from(imagePixels), + totalPixels: width * height + }, + paintedMapPacked: null + }; + } else { + // Auto-Image format: Empty position and region for manual placement + saveData = { + timestamp: Date.now(), + version: '2.2', + state: { + totalPixels: width * height, + paintedPixels: 0, + lastPosition: { x: 0, y: 0 }, + startPosition: null, // Empty for manual placement + region: null, // Empty for manual placement + imageLoaded: true, + colorsChecked: true, + coordinateMode: 'rows', + coordinateDirection: 'top-left', + coordinateSnake: false, + blockWidth: 1, + blockHeight: 1, + availableColors: Object.values(CONFIG.COLOR_MAP).filter(c => c.rgb).map(color => ({ + ...color, + rgb: [color.rgb.r, color.rgb.g, color.rgb.b] // Convert RGB object to array for Auto-Image compatibility + })), + // Additional Auto-Image required fields for compatibility + paintWhitePixels: true, + paintTransparentPixels: false, + displayCharges: 0, + preciseCurrentCharges: 0, + maxCharges: 1, + cooldown: 30000, + stopFlag: false, + selectingPosition: false, + minimized: false, + estimatedTime: 0, + language: 'en', + paintingSpeed: 5, + batchMode: 'normal', + randomBatchMin: 1, + randomBatchMax: 5, + cooldownChargeThreshold: 10, + overlayOpacity: 0.7, + blueMarbleEnabled: false, + ditheringEnabled: true, + colorMatchingAlgorithm: 'lab', + enableChromaPenalty: true, + chromaPenaltyWeight: 0.15, + customTransparencyThreshold: 128, + customWhiteThreshold: 230, + paintUnavailablePixels: true, + // Critical missing fields that Auto-Image expects + fullChargeData: null, + fullChargeInterval: null, + tokenSource: 'generator', + initialSetupComplete: true, + resizeSettings: null, + originalImage: null, + resizeIgnoreMask: null + }, + imageData: { + width: width, + height: height, + pixels: Array.from(imagePixels), + totalPixels: width * height + }, + paintedMapPacked: null + }; + } + + // Debug: Log the export data structure before saving + console.log('๐Ÿ” [DEBUG] Export data structure:', { + hasState: !!saveData.state, + hasImageData: !!saveData.imageData, + topLevelKeys: Object.keys(saveData), + stateKeys: saveData.state ? Object.keys(saveData.state) : 'N/A', + imageDataKeys: saveData.imageData ? Object.keys(saveData.imageData) : 'N/A', + version: saveData.version, + exportType: exportType, + imageDataPixelsLength: saveData.imageData?.pixels?.length || 0, + imageWidth: saveData.imageData?.width || 0, + imageHeight: saveData.imageData?.height || 0 + }); + + // Validate export data before saving + if (!saveData.state || !saveData.imageData) { + throw new Error('Export validation failed: Missing state or imageData'); + } + + if (!saveData.imageData.pixels || saveData.imageData.pixels.length === 0) { + throw new Error('Export validation failed: No pixel data to export'); + } + + // Download the file using Auto-Image's method + const dataStr = JSON.stringify(saveData, null, 2); + const blob = new Blob([dataStr], { type: 'application/json' }); + const url = URL.createObjectURL(blob); + + const a = document.createElement('a'); + a.href = url; + a.download = filename; + document.body.appendChild(a); + a.click(); + document.body.removeChild(a); + URL.revokeObjectURL(url); + + console.log(`โœ… ${exportType} export complete: ${filename}`); + console.log(`๐Ÿ“ Area: ${width}x${height} from (${x1},${y1}) to (${x2},${y2})`); + + if (exportType === 'autofix') { + console.log(`๐Ÿ”— Compatible with Auto-Repair for restoration`); + showAlert(`โœ… Exported ${state.scannedPixels.length} pixels for Auto-Fix to ${filename}`, 'success'); + } else { + console.log(`๐ŸŽจ Ready for manual placement in Auto-Image`); + showAlert(`โœ… Exported ${state.scannedPixels.length} pixels for Auto-Image to ${filename}`, 'success'); + } + + } catch (error) { + console.error('โŒ Export failed:', error); + showAlert(`โŒ Export failed: ${error.message}`, 'error'); + } + } + + // Filename input handlers + function handleFilenameInput(event) { + const input = event.target; + let value = input.value; + + // Always ensure it ends with .json + if (!value.endsWith('.json')) { + if (value.length > 0 && !value.includes('.json')) { + input.value = value + '.json'; + } else if (value === '') { + input.value = '.json'; + } + } + + // Position cursor before .json + if (input.value.endsWith('.json') && event.inputType !== 'deleteContentBackward') { + const nameLength = input.value.length - 5; // -5 for '.json' + input.setSelectionRange(nameLength, nameLength); + } + } + + function preventJsonDeletion(event) { + const input = event.target; + const cursorPos = input.selectionStart; + const value = input.value; + + // Prevent deletion of .json extension + if ((event.key === 'Backspace' || event.key === 'Delete') && + cursorPos > value.length - 5) { + event.preventDefault(); + return false; + } + + // Position cursor before .json on certain keys + if (event.key === 'End' || event.key === 'ArrowRight') { + const nameLength = value.length - 5; + if (cursorPos >= nameLength) { + event.preventDefault(); + input.setSelectionRange(nameLength, nameLength); + return false; + } + } + } + + // Utility functions + function showAlert(message, type = 'info') { + console.log(`${type === 'error' ? 'โŒ' : type === 'warning' ? 'โš ๏ธ' : type === 'success' ? 'โœ…' : 'โ„น๏ธ'} ${message}`); + + // Create visual alert + const alert = document.createElement('div'); + alert.style.cssText = ` + position: fixed; + top: 20px; + right: 20px; + padding: 12px 20px; + border-radius: 8px; + color: white; + font-family: 'Segoe UI', Arial, sans-serif; + font-size: 14px; + font-weight: 500; + z-index: 10000; + box-shadow: 0 4px 12px rgba(0,0,0,0.3); + animation: slideIn 0.3s ease-out; + max-width: 300px; + ${type === 'error' ? 'background: linear-gradient(135deg, #ff4757, #ff3742);' : + type === 'warning' ? 'background: linear-gradient(135deg, #ffa502, #ff6348);' : + type === 'success' ? 'background: linear-gradient(135deg, #26de81, #20bf6b);' : + 'background: linear-gradient(135deg, #3742fa, #2f3542);'} + `; + alert.textContent = message; + + // Add animation styles + if (!document.getElementById('alert-styles')) { + const style = document.createElement('style'); + style.id = 'alert-styles'; + style.textContent = ` + @keyframes slideIn { + from { transform: translateX(100%); opacity: 0; } + to { transform: translateX(0); opacity: 1; } + } + @keyframes slideOut { + from { transform: translateX(0); opacity: 1; } + to { transform: translateX(100%); opacity: 0; } + } + `; + document.head.appendChild(style); + } + + document.body.appendChild(alert); + + // Auto remove after 3 seconds + setTimeout(() => { + alert.style.animation = 'slideOut 0.3s ease-in'; + setTimeout(() => { + if (alert.parentNode) { + alert.parentNode.removeChild(alert); + } + }, 300); + }, 3000); + } + + // UI Creation and Management + async function createUI() { + console.log('๐ŸŽจ Creating Art Extractor UI...'); + + try { + // Remove existing UI + const existing = document.getElementById('art-extractor-ui'); + if (existing) { + console.log('๐Ÿ—‘๏ธ Removing existing UI'); + existing.remove(); + } + + // Check if document body exists + if (!document.body) { + throw new Error('Document body not available'); + } + + console.log('๐Ÿ”จ Building UI container...'); + + // Create main container + const container = document.createElement('div'); + container.id = 'art-extractor-ui'; + container.className = 'wplace-theme-neon-cyan'; + container.style.cssText = ` + position: fixed; + top: 20px; + left: 20px; + width: 320px; + background: #1959A1; + border: 2px solid #81DCF7; + border-radius: 0; + padding: 20px; + font-family: 'Press Start 2P', monospace; + color: #81DCF7; + z-index: 9999; + box-shadow: 0 0 30px rgba(129, 220, 247, 0.6), inset 0 0 30px rgba(234, 156, 0, 0.1); + user-select: none; + `; + + container.innerHTML = ` + +
+ + +
+

๐ŸŽจ ART EXTRACTOR

+ +
+ + +
+
Status: CAPTURING
+
Area: NONE
+
Pixels: 0
+
+ + +
+ + + +
+ + +
+ +
+ + +
+ + + + + +
+ + +
+ PAINT UPPER-LEFT & LOWER-RIGHT PIXELS +
+ + + + + `; + + // Add event listeners + container.querySelector('#art-extractor-close').addEventListener('click', closeArtExtractor); + // Add event listeners + container.querySelector('#art-extractor-close').addEventListener('click', closeArtExtractor); + container.querySelector('#export-autofix').addEventListener('click', () => exportToJSON('autofix')); + container.querySelector('#export-autoimage').addEventListener('click', () => exportToJSON('autoimage')); + container.querySelector('#start-selecting').addEventListener('click', startPixelCapture); + container.querySelector('#clear-selection').addEventListener('click', clearSelection); + + // Set up filename input with .json enforcement + const filenameInput = container.querySelector('#filename-input'); + if (filenameInput) { + // Initialize with default value + filenameInput.value = '.json'; + + // Add event listeners for .json enforcement + filenameInput.addEventListener('input', handleFilenameInput); + filenameInput.addEventListener('keydown', preventJsonDeletion); + filenameInput.addEventListener('focus', function() { + // Position cursor before .json on focus + const nameLength = this.value.length - 5; + if (nameLength >= 0) { + this.setSelectionRange(nameLength, nameLength); + } + }); + + // Set initial cursor position + setTimeout(() => { + filenameInput.setSelectionRange(0, 0); + }, 100); + } + + // Add to page + document.body.appendChild(container); + state.ui = container; + + console.log('โœ… Art Extractor UI created'); + + // Auto-start pixel capture immediately + console.log('๐ŸŽฏ Auto-starting pixel capture...'); + setTimeout(() => { + startPixelCapture(); + }, 500); + updateUI(); + + } catch (error) { + console.error('โŒ Failed to create UI:', error); + throw error; + } + } + + // UI update function + function updateUI() { + if (!state.ui) return; + + const statusElement = state.ui.querySelector('#current-status'); + const areaElement = state.ui.querySelector('#area-info'); + const pixelElement = state.ui.querySelector('#pixel-count'); + const exportAutofixBtn = state.ui.querySelector('#export-autofix'); + const exportAutoimageBtn = state.ui.querySelector('#export-autoimage'); + + // Update status with Neon Cyan styling + if (state.isCapturing) { + statusElement.textContent = `CAPTURING (${state.capturedPixels.length}/${state.requiredPixels})`; + statusElement.style.color = '#EA9C00'; + statusElement.style.textShadow = '0 0 8px #EA9C00'; + } else if (state.isScanning) { + statusElement.textContent = 'SCANNING...'; + statusElement.style.color = '#39ff14'; + statusElement.style.textShadow = '0 0 8px #39ff14'; + } else if (state.selectionArea) { + statusElement.textContent = 'READY TO EXPORT'; + statusElement.style.color = '#39ff14'; + statusElement.style.textShadow = '0 0 8px #39ff14'; + } else { + statusElement.textContent = 'READY'; + statusElement.style.color = '#81DCF7'; + statusElement.style.textShadow = '0 0 5px #81DCF7'; + } + + // Update area info with uppercase styling + if (state.selectionArea) { + areaElement.textContent = `${state.selectionArea.width}ร—${state.selectionArea.height} PX`; + } else if (state.capturedPixels.length > 0) { + areaElement.textContent = `${state.capturedPixels.length}/${state.requiredPixels} CAPTURED`; + } else { + areaElement.textContent = 'NONE'; + } + + // Update pixel count with progress info + if (state.isScanning && state.totalPixels > 0) { + const progress = Math.round((state.scannedPixels.length / state.totalPixels) * 100); + pixelElement.textContent = `${state.scannedPixels.length}/${state.totalPixels} (${progress}%)`; + } else { + pixelElement.textContent = state.scannedPixels.length.toString(); + } + + // Update export button states + const hasScannedData = state.scannedPixels && state.scannedPixels.length > 0; + + if (exportAutofixBtn) { + exportAutofixBtn.disabled = !hasScannedData; + if (!exportAutofixBtn.disabled) { + exportAutofixBtn.style.opacity = '1'; + exportAutofixBtn.style.cursor = 'pointer'; + } else { + exportAutofixBtn.style.opacity = '0.5'; + exportAutofixBtn.style.cursor = 'not-allowed'; + } + } + + if (exportAutoimageBtn) { + exportAutoimageBtn.disabled = !hasScannedData; + if (!exportAutoimageBtn.disabled) { + exportAutoimageBtn.style.opacity = '1'; + exportAutoimageBtn.style.cursor = 'pointer'; + } else { + exportAutoimageBtn.style.opacity = '0.5'; + exportAutoimageBtn.style.cursor = 'not-allowed'; + } + } + } + + // Control functions + function startPixelCapture() { + console.log('๐ŸŽฏ Starting pixel capture mode...'); + + state.isCapturing = true; + state.capturedPixels = []; + state.selectionArea = null; + state.scannedPixels = []; + + // Clear any existing overlay + if (state.overlayManager) { + state.overlayManager.disable(); + } + + // Set up pixel paint monitoring with fetch interception + const setupSuccess = setupPixelCapture(); + + if (setupSuccess) { + updateUI(); + showAlert('๐ŸŽฏ Paint a pixel at the UPPER-LEFT corner of the area you want to extract', 'info'); + + // Set timeout for capture process + setTimeout(() => { + if (state.isCapturing && state.capturedPixels.length === 0) { + showAlert('โฐ No pixels captured yet. Try painting a pixel or use "Enter Coordinates Manually"', 'warning'); + } + }, 15000); + } else { + showAlert('โš ๏ธ Could not set up automatic capture. Use "Enter Coordinates Manually"', 'warning'); + updateUI(); + } + } + + function clearSelection() { + console.log('๐Ÿงน Clearing pixel capture...'); + + state.isCapturing = false; + state.capturedPixels = []; + state.selectionArea = null; + state.scannedPixels = []; + + // Restore original fetch function + restoreFetch(); + + // Disable overlay + if (state.overlayManager) { + state.overlayManager.disable(); + } + + updateUI(); + showAlert('Pixel capture cleared', 'info'); + } + + function closeArtExtractor() { + console.log('๐Ÿ‘‹ Closing Art Extractor...'); + + // Stop capturing + state.isCapturing = false; + + // Restore original fetch function + restoreFetch(); + + // Clean up overlay + if (state.overlayManager) { + state.overlayManager.disable(); + } + + // Remove UI + if (state.ui) { + state.ui.remove(); + } + + // Reset state + Object.assign(state, { + isCapturing: false, + capturedPixels: [], + selectionArea: null, + ui: null, + overlayManager: null, + isScanning: false, + scannedPixels: [], + paintEventListener: null, + lastPaintEvent: null + }); + + console.log('โœ… Art Extractor closed'); + } + + // Main initialization + async function initialize() { + console.log('๐Ÿš€ Initializing Art Extractor...'); + + try { + // Create UI immediately - don't wait for dependencies + console.log('๐ŸŽจ Creating UI first...'); + await createUI(); + + // Then try to set up dependencies in background + setTimeout(async () => { + try { + // Wait for Auto-Image overlay manager with timeout + if (!window.autoImageOverlayManager) { + console.log('โณ Waiting for Auto-Image overlay manager...'); + await new Promise((resolve, reject) => { + let attempts = 0; + const maxAttempts = 50; // 5 seconds timeout + const check = () => { + attempts++; + if (window.autoImageOverlayManager) { + console.log('โœ… Auto-Image overlay manager found'); + resolve(); + } else if (attempts >= maxAttempts) { + console.warn('โš ๏ธ Auto-Image overlay manager not found, proceeding without it'); + resolve(); // Continue anyway + } else { + setTimeout(check, 100); + } + }; + check(); + }); + } + + // Set up pixel capture monitoring + try { + const captureReady = setupPixelCapture(); + if (captureReady) { + console.log('โœ… Pixel capture monitoring ready'); + } else { + console.warn('โš ๏ธ Pixel capture monitoring setup failed'); + } + } catch (error) { + console.warn('โš ๏ธ Pixel capture error:', error); + } + + } catch (error) { + console.warn('โš ๏ธ Background initialization error:', error); + } + }, 1000); + + console.log('โœ… Art Extractor initialized successfully'); + showAlert('Art Extractor ready! UI loaded - dependencies loading in background.', 'success'); + + } catch (error) { + console.error('โŒ Failed to initialize Art Extractor:', error); + + // Try to create UI anyway as fallback + try { + await createUI(); + showAlert(`Art Extractor loaded with limited functionality: ${error.message}`, 'warning'); + } catch (uiError) { + console.error('โŒ Failed to create UI:', uiError); + showAlert(`Critical initialization failure: ${error.message}`, 'error'); + } + } + } + + // Start initialization with additional error handling + try { + await initialize(); + } catch (error) { + console.error('โŒ Critical error during Art Extractor initialization:', error); + + // Final fallback - try to show minimal UI + try { + const container = document.createElement('div'); + container.innerHTML = ` +
+ โŒ Art Extractor failed to load: ${error.message} + +
+ `; + document.body.appendChild(container); + } catch (e) { + console.error('โŒ Even fallback UI failed:', e); + } + } + + // Expose globally for extension integration + window.WPlaceArtExtractor = { + state, + initialize, + closeArtExtractor, + exportToJSON, + scanSelectedArea + }; + + console.log('๐ŸŽจ WPlace Art Extractor loaded successfully!'); +})(); +======= +// ==UserScript== +// @name WPlace Art Extractor +// @namespace http://tampermonkey.net/ +// @version 2025-09-20.1 +// @description Extract artwork areas to JSON for Auto-Repair.js +// @author Wbot +// @match https://wplace.live/* +// @grant none +// @icon +// ==/UserScript== + +localStorage.removeItem("lp"); + +// Fallback translation function for when utils manager isn't loaded +function getText(key, params) { + // Try to get translation from loadedTranslations + try { + if (window.loadedTranslations && window.loadedTranslations[window.state?.language || 'en']) { + const translation = window.loadedTranslations[window.state?.language || 'en'][key]; + if (translation && typeof translation === 'string') { + if (params && typeof params === 'object') { + return translation.replace(/\{(\w+)\}/g, (match, paramKey) => params[paramKey] || match); + } + return translation; + } + } + } catch (error) { + console.warn('Translation lookup error:', error); + } + + return key; // Fallback to key if no translation found +} + +;(async () => { + // Prevent multiple instances of this script from running + if (window.WPLACE_ART_EXTRACTOR_LOADED) { + console.log('Art Extractor already loaded, skipping...'); + return; + } + window.WPLACE_ART_EXTRACTOR_LOADED = true; + + console.log('%c๐ŸŽจ WPlace Art Extractor Starting...', 'color: #ff6b35; font-weight: bold; font-size: 16px;'); + console.log('%cโœจ Interactive pixel capture for area extraction!', 'color: #26de81; font-weight: bold;'); + + // Immediate visual confirmation that script is running + try { + const testDiv = document.createElement('div'); + testDiv.innerHTML = ` +
+ ๐ŸŽจ Art Extractor Loading... +
+ `; + document.body.appendChild(testDiv); + + // Remove after 3 seconds + setTimeout(() => { + if (testDiv.parentNode) { + testDiv.parentNode.removeChild(testDiv); + } + }, 3000); + } catch (e) { + console.log('Could not create test notification:', e); + } + + // CONFIGURATION CONSTANTS + const CONFIG = { + OVERLAY_OPACITY: 0.7, + SELECTION_COLOR: { r: 50, g: 200, b: 50, a: 180 }, // Green extraction overlay + CORNER_MARKER_COLOR: { r: 255, g: 255, b: 0, a: 255 }, // Yellow corner markers + CORNER_MARKER_SIZE: 12, // Corner marker size + MIN_AREA_SIZE: 1, + MAX_AREA_SIZE: 2500, + COLOR_MAP: { + 0: { id: 1, name: 'Black', rgb: { r: 0, g: 0, b: 0 } }, + 1: { id: 2, name: 'Dark Gray', rgb: { r: 60, g: 60, b: 60 } }, + 2: { id: 3, name: 'Gray', rgb: { r: 120, g: 120, b: 120 } }, + 3: { id: 4, name: 'Light Gray', rgb: { r: 210, g: 210, b: 210 } }, + 4: { id: 5, name: 'White', rgb: { r: 255, g: 255, b: 255 } }, + 5: { id: 6, name: 'Deep Red', rgb: { r: 96, g: 0, b: 24 } }, + 6: { id: 7, name: 'Red', rgb: { r: 237, g: 28, b: 36 } }, + 7: { id: 8, name: 'Orange', rgb: { r: 255, g: 127, b: 39 } }, + 8: { id: 9, name: 'Gold', rgb: { r: 246, g: 170, b: 9 } }, + 9: { id: 10, name: 'Yellow', rgb: { r: 249, g: 221, b: 59 } }, + 10: { id: 11, name: 'Light Yellow', rgb: { r: 255, g: 250, b: 188 } }, + 11: { id: 12, name: 'Dark Green', rgb: { r: 14, g: 185, b: 104 } }, + 12: { id: 13, name: 'Green', rgb: { r: 19, g: 230, b: 123 } }, + 13: { id: 14, name: 'Light Green', rgb: { r: 135, g: 255, b: 94 } }, + 14: { id: 15, name: 'Dark Teal', rgb: { r: 12, g: 129, b: 110 } }, + 15: { id: 16, name: 'Teal', rgb: { r: 16, g: 174, b: 166 } }, + 16: { id: 17, name: 'Light Teal', rgb: { r: 19, g: 225, b: 190 } }, + 17: { id: 20, name: 'Cyan', rgb: { r: 96, g: 247, b: 242 } }, + 18: { id: 44, name: 'Light Cyan', rgb: { r: 187, g: 250, b: 242 } }, + 19: { id: 18, name: 'Dark Blue', rgb: { r: 40, g: 80, b: 158 } }, + 20: { id: 19, name: 'Blue', rgb: { r: 64, g: 147, b: 228 } }, + 21: { id: 21, name: 'Indigo', rgb: { r: 107, g: 80, b: 246 } }, + 22: { id: 22, name: 'Light Indigo', rgb: { r: 153, g: 177, b: 251 } }, + 23: { id: 23, name: 'Dark Purple', rgb: { r: 120, g: 12, b: 153 } }, + 24: { id: 24, name: 'Purple', rgb: { r: 170, g: 56, b: 185 } }, + 25: { id: 25, name: 'Light Purple', rgb: { r: 224, g: 159, b: 249 } }, + 26: { id: 26, name: 'Dark Pink', rgb: { r: 203, g: 0, b: 122 } }, + 27: { id: 27, name: 'Pink', rgb: { r: 236, g: 31, b: 128 } }, + 28: { id: 28, name: 'Light Pink', rgb: { r: 243, g: 141, b: 169 } }, + 29: { id: 29, name: 'Dark Brown', rgb: { r: 104, g: 70, b: 52 } }, + 30: { id: 30, name: 'Brown', rgb: { r: 149, g: 104, b: 42 } }, + 31: { id: 31, name: 'Beige', rgb: { r: 248, g: 178, b: 119 } }, + 32: { id: 52, name: 'Light Beige', rgb: { r: 255, g: 197, b: 165 } }, + 33: { id: 32, name: 'Medium Gray', rgb: { r: 170, g: 170, b: 170 } }, + 34: { id: 33, name: 'Dark Red', rgb: { r: 165, g: 14, b: 30 } }, + 35: { id: 34, name: 'Light Red', rgb: { r: 250, g: 128, b: 114 } }, + 36: { id: 35, name: 'Dark Orange', rgb: { r: 228, g: 92, b: 26 } }, + 37: { id: 37, name: 'Dark Goldenrod', rgb: { r: 156, g: 132, b: 49 } }, + 38: { id: 38, name: 'Goldenrod', rgb: { r: 197, g: 173, b: 49 } }, + 39: { id: 39, name: 'Light Goldenrod', rgb: { r: 232, g: 212, b: 95 } }, + 40: { id: 40, name: 'Dark Olive', rgb: { r: 74, g: 107, b: 58 } }, + 41: { id: 41, name: 'Olive', rgb: { r: 90, g: 148, b: 74 } }, + 42: { id: 42, name: 'Light Olive', rgb: { r: 132, g: 197, b: 115 } }, + 43: { id: 43, name: 'Dark Cyan', rgb: { r: 15, g: 121, b: 159 } }, + 44: { id: 45, name: 'Light Blue', rgb: { r: 125, g: 199, b: 255 } }, + 45: { id: 46, name: 'Dark Indigo', rgb: { r: 77, g: 49, b: 184 } }, + 46: { id: 47, name: 'Dark Slate Blue', rgb: { r: 74, g: 66, b: 132 } }, + 47: { id: 48, name: 'Slate Blue', rgb: { r: 122, g: 113, b: 196 } }, + 48: { id: 49, name: 'Light Slate Blue', rgb: { r: 181, g: 174, b: 241 } }, + 49: { id: 53, name: 'Dark Peach', rgb: { r: 155, g: 82, b: 73 } }, + 50: { id: 54, name: 'Peach', rgb: { r: 209, g: 128, b: 120 } }, + 51: { id: 55, name: 'Light Peach', rgb: { r: 250, g: 182, b: 164 } }, + 52: { id: 50, name: 'Light Brown', rgb: { r: 219, g: 164, b: 99 } }, + 53: { id: 56, name: 'Dark Tan', rgb: { r: 123, g: 99, b: 82 } }, + 54: { id: 57, name: 'Tan', rgb: { r: 156, g: 132, b: 107 } }, + 55: { id: 36, name: 'Light Tan', rgb: { r: 214, g: 181, b: 148 } }, + 56: { id: 51, name: 'Dark Beige', rgb: { r: 209, g: 128, b: 81 } }, + 57: { id: 61, name: 'Dark Stone', rgb: { r: 109, g: 100, b: 63 } }, + 58: { id: 62, name: 'Stone', rgb: { r: 148, g: 140, b: 107 } }, + 59: { id: 63, name: 'Light Stone', rgb: { r: 205, g: 197, b: 158 } }, + 60: { id: 58, name: 'Dark Slate', rgb: { r: 51, g: 57, b: 65 } }, + 61: { id: 59, name: 'Slate', rgb: { r: 109, g: 117, b: 141 } }, + 62: { id: 60, name: 'Light Slate', rgb: { r: 179, g: 185, b: 209 } }, + 63: { id: 0, name: 'Transparent', rgb: null }, + } + }; + + // Expose CONFIG globally for the utils manager and other modules + window.ART_EXTRACTOR_CONFIG = CONFIG; + + // GLOBAL STATE + const state = { + // Pixel capture state + isCapturing: false, + capturedPixels: [], // Array of painted pixels with their world coordinates + requiredPixels: 2, // Need 2 corner pixels + + // Selection area + selectionArea: null, // { x, y, width, height, regionX, regionY } + + // UI state + ui: null, + overlayManager: null, // Will use global overlay manager like Auto-Image + + // Scanning state + isScanning: false, + scannedPixels: [], + totalPixels: 0, + + // Auto-Image integration state + imageData: null, + startPosition: null, + region: null, + + // Canvas monitoring + paintEventListener: null, + lastPaintEvent: null, + + language: 'en', + }; + + // Expose state globally + window.ART_EXTRACTOR_STATE = state; + + // Use Auto-Image's overlay system - no custom overlay manager needed + // We'll integrate with the global overlay manager that Auto-Image uses + + // Monitor WPlace paint events to capture pixel coordinates using fetch interception + let originalFetch = null; + let fetchInterceptionActive = false; + + function setupPixelCapture() { + console.log('๐ŸŽฏ Setting up pixel paint monitoring...'); + + try { + // Store original fetch if not already stored + if (!originalFetch) { + originalFetch = window.fetch; + } + + // Install fetch interceptor for pixel capture + if (!fetchInterceptionActive) { + window.fetch = async (url, options) => { + // Check if this is a pixel painting request + if (state.isCapturing && + typeof url === 'string' && + url.includes('/s0/pixel/') && + options && + options.method === 'POST') { + + try { + console.log('๐ŸŽฏ Intercepting pixel paint request:', url); + + // Call original fetch first + const response = await originalFetch(url, options); + + // If request was successful and we have body data, extract coordinates + if (response.ok && options.body) { + let requestBody; + try { + requestBody = JSON.parse(options.body); + } catch (parseError) { + console.warn('Could not parse request body:', parseError); + return response; + } + + // Extract coordinates from request + if (requestBody.coords && Array.isArray(requestBody.coords) && requestBody.coords.length >= 2) { + const localX = requestBody.coords[0]; // 0-999 within tile + const localY = requestBody.coords[1]; // 0-999 within tile + + // Extract tile coordinates from URL: /s0/pixel/{tileX}/{tileY} + const tileMatch = url.match(/\/s0\/pixel\/(\-?\d+)\/(\-?\d+)/); + if (tileMatch) { + const tileX = parseInt(tileMatch[1]); + const tileY = parseInt(tileMatch[2]); + + // Convert to global canvas coordinates + const globalX = tileX * 1000 + localX; + const globalY = tileY * 1000 + localY; + + console.log(`๐ŸŽฏ Captured painted pixel: (${globalX}, ${globalY}) from tile (${tileX}, ${tileY}) local (${localX}, ${localY})`); + + // Add the captured pixel + setTimeout(() => { + addCapturedPixel(globalX, globalY); + }, 100); + } + } + } + + return response; + } catch (error) { + console.error('โŒ Error intercepting pixel paint:', error); + // Fallback to original fetch + return originalFetch(url, options); + } + } + + // For all other requests, use original fetch + return originalFetch(url, options); + }; + + fetchInterceptionActive = true; + console.log('โœ… Fetch interception enabled'); + } + + return true; + + } catch (error) { + console.warn('โš ๏ธ Could not set up paint monitoring:', error); + return false; + } + } + + // Restore original fetch function + function restoreFetch() { + if (originalFetch && fetchInterceptionActive) { + window.fetch = originalFetch; + fetchInterceptionActive = false; + console.log('๐Ÿ”„ Original fetch restored'); + } + } + + // Handle canvas clicks during capture mode (fallback method) + function handleCanvasClick(event) { + if (!state.isCapturing) return; + + // Only handle clicks on canvas elements + const canvas = event.target.closest('canvas'); + if (!canvas) return; + + console.log('๏ฟฝ Canvas click detected during capture mode'); + + // For now, we'll use manual coordinate input as the primary method + // since intercepting the actual paint events is complex + setTimeout(() => { + if (state.isCapturing && state.capturedPixels.length < state.requiredPixels) { + promptForCoordinates(); + } + }, 100); + } + + // Handle detected paint events (for future implementation) + function handlePaintEvent(event) { + if (!state.isCapturing) return; + + console.log('๐ŸŽฏ Processing paint event:', event); + + // Extract coordinates from paint event + let worldX, worldY; + + if (event.x !== undefined && event.y !== undefined) { + worldX = event.x; + worldY = event.y; + } else if (event.position) { + worldX = event.position.x; + worldY = event.position.y; + } else if (event.coords) { + worldX = event.coords[0]; + worldY = event.coords[1]; + } else { + console.warn('โš ๏ธ Could not extract coordinates from paint event'); + return; + } + + // Add the painted pixel to our capture list + addCapturedPixel(worldX, worldY); + } + + // Manual coordinate input (fallback method when auto-capture doesn't work) + function promptForCoordinates() { + if (!state.isCapturing) { + showAlert('Not in capture mode', 'warning'); + return; + } + + const pixelNum = state.capturedPixels.length + 1; + const cornerType = pixelNum === 1 ? 'UPPER-LEFT' : 'LOWER-RIGHT'; + + const coordStr = prompt( + `๐Ÿ“ Enter coordinates for ${cornerType} corner (${pixelNum}/${state.requiredPixels})\n\n` + + `Format: x,y (e.g., 1000,500)\n\n` + + `To find coordinates:\n` + + `1. Navigate to the ${cornerType} corner of your area\n` + + `2. Hover over the exact pixel position\n` + + `3. Check coordinates in bottom-left corner of WPlace\n` + + `4. Enter them here:\n\n` + + `Tip: You can also just paint a pixel there and it will be captured automatically!` + ); + + if (coordStr && coordStr.trim()) { + const coords = coordStr.trim().split(',').map(n => parseInt(n.trim())); + if (coords.length === 2 && !isNaN(coords[0]) && !isNaN(coords[1])) { + addCapturedPixel(coords[0], coords[1]); + } else { + showAlert('Invalid coordinates format. Use: x,y (e.g., 1000,500)', 'error'); + // Try again + setTimeout(() => promptForCoordinates(), 500); + } + } else if (coordStr !== null) { + // User entered empty string, try again + showAlert('Please enter coordinates', 'warning'); + setTimeout(() => promptForCoordinates(), 500); + } + // If user clicked cancel (coordStr === null), do nothing + } + + // Add a captured pixel with enhanced validation + function addCapturedPixel(worldX, worldY) { + if (!state.isCapturing) { + console.log('Not in capture mode, ignoring pixel'); + return; + } + + if (state.capturedPixels.length >= state.requiredPixels) { + showAlert('Already have enough pixels', 'warning'); + return; + } + + // Validate coordinates + if (!Number.isFinite(worldX) || !Number.isFinite(worldY)) { + showAlert('Invalid pixel coordinates', 'error'); + return; + } + + // Calculate region and local coordinates + const regionX = Math.floor(worldX / 1000); + const regionY = Math.floor(worldY / 1000); + const pixelX = worldX % 1000; + const pixelY = worldY % 1000; + + const pixel = { + worldX, + worldY, + regionX, + regionY, + pixelX, + pixelY, + timestamp: Date.now(), + cornerType: state.capturedPixels.length === 0 ? 'upperLeft' : 'lowerRight' + }; + + state.capturedPixels.push(pixel); + console.log(`๐ŸŽฏ Captured ${pixel.cornerType} pixel ${state.capturedPixels.length}/${state.requiredPixels}:`, pixel); + + updateUI(); + + // Progress messages + if (state.capturedPixels.length === 1) { + showAlert(`โœ… Upper-left corner captured: (${worldX}, ${worldY})`, 'success'); + setTimeout(() => { + showAlert('๐ŸŽฏ Now paint a pixel at the LOWER-RIGHT corner of the area', 'info'); + }, 1500); + } else if (state.capturedPixels.length >= state.requiredPixels) { + showAlert(`โœ… Lower-right corner captured: (${worldX}, ${worldY})`, 'success'); + // Small delay before auto-scanning to show the success message + setTimeout(() => { + completeAreaCapture(); + // Auto-trigger scan after area capture is complete + setTimeout(() => { + scanSelectedArea(); + }, 500); + }, 1000); + } + } + + // Complete area capture when we have 2 pixels with validation + function completeAreaCapture() { + state.isCapturing = false; + restoreFetch(); // Restore original fetch function + + if (state.capturedPixels.length < 2) { + showAlert('Need at least 2 pixels to define area', 'error'); + return; + } + + // Calculate bounding box from captured pixels + const pixels = state.capturedPixels; + const minX = Math.min(...pixels.map(p => p.worldX)); + const minY = Math.min(...pixels.map(p => p.worldY)); + const maxX = Math.max(...pixels.map(p => p.worldX)); + const maxY = Math.max(...pixels.map(p => p.worldY)); + + // Validation: ensure upper-left is actually upper-left + if (minX >= maxX || minY >= maxY) { + showAlert('โŒ Invalid area: upper-left corner must be less than lower-right corner', 'error'); + // Reset capture to try again + state.capturedPixels = []; + startPixelCapture(); + return; + } + + const width = maxX - minX + 1; + const height = maxY - minY + 1; + + // Size validation + if (width < CONFIG.MIN_AREA_SIZE || height < CONFIG.MIN_AREA_SIZE) { + showAlert(`โŒ Area too small: ${width}ร—${height} pixels (minimum: ${CONFIG.MIN_AREA_SIZE}ร—${CONFIG.MIN_AREA_SIZE})`, 'error'); + return; + } + + if (width > CONFIG.MAX_AREA_SIZE || height > CONFIG.MAX_AREA_SIZE) { + showAlert(`โŒ Area too large: ${width}ร—${height} pixels (maximum: ${CONFIG.MAX_AREA_SIZE}ร—${CONFIG.MAX_AREA_SIZE})`, 'error'); + return; + } + + state.selectionArea = { + x1: minX, // Coordinate naming + y1: minY, + x2: maxX, + y2: maxY, + x: minX, // Keep compatibility + y: minY, + width: width, + height: height, + regionX: Math.floor(minX / 1000), + regionY: Math.floor(minY / 1000) + }; + + console.log('๐Ÿ“ Area capture complete:', state.selectionArea); + showAlert(`โœ… Area captured: ${width}ร—${height} pixels from (${minX},${minY}) to (${maxX},${maxY})`, 'success'); + + // Set up overlay using Auto-Image's system + setTimeout(() => { + setupAutoImageOverlay(); + }, 500); + + updateUI(); + } + + // Set up visual overlay for selected extraction area (simplified approach) + async function setupAutoImageOverlay() { + if (!state.selectionArea) return; + + console.log('๐ŸŽจ Setting up extraction area overlay...'); + + try { + const { x1, y1, x2, y2, width, height, regionX, regionY } = state.selectionArea; + + // Try to use Auto-Image's overlay if available, otherwise create simple visual feedback + if (window.autoImageOverlayManager) { + console.log('๐Ÿ“Š Using Auto-Image overlay manager for area visualization'); + + const overlayMgr = window.autoImageOverlayManager; + + // Create overlay canvas showing the selected area + const canvas = new OffscreenCanvas(width, height); + const ctx = canvas.getContext('2d'); + + // Fill with extraction area overlay + ctx.fillStyle = `rgba(${CONFIG.SELECTION_COLOR.r}, ${CONFIG.SELECTION_COLOR.g}, ${CONFIG.SELECTION_COLOR.b}, 0.3)`; + ctx.fillRect(0, 0, width, height); + + // Add extraction border + ctx.strokeStyle = `rgba(${CONFIG.SELECTION_COLOR.r}, ${CONFIG.SELECTION_COLOR.g}, ${CONFIG.SELECTION_COLOR.b}, 0.8)`; + ctx.lineWidth = 2; + ctx.strokeRect(1, 1, width - 2, height - 2); + + // Add corner markers + const cornerSize = CONFIG.CORNER_MARKER_SIZE; + ctx.fillStyle = `rgba(${CONFIG.CORNER_MARKER_COLOR.r}, ${CONFIG.CORNER_MARKER_COLOR.g}, ${CONFIG.CORNER_MARKER_COLOR.b}, 1)`; + + // Top-left corner + ctx.fillRect(0, 0, cornerSize, cornerSize); + // Top-right corner + ctx.fillRect(width - cornerSize, 0, cornerSize, cornerSize); + // Bottom-left corner + ctx.fillRect(0, height - cornerSize, cornerSize, cornerSize); + // Bottom-right corner + ctx.fillRect(width - cornerSize, height - cornerSize, cornerSize, cornerSize); + + try { + // Create image bitmap + const overlayBitmap = await canvas.transferToImageBitmap(); + + // Set the overlay using Auto-Image's API + await overlayMgr.setImage(overlayBitmap); + await overlayMgr.setPosition( + { x: x1 % 1000, y: y1 % 1000 }, + { x: regionX, y: regionY } + ); + + // Enable the overlay + overlayMgr.enable(); + + // Store references for cleanup + state.overlayManager = overlayMgr; + state.imageData = overlayBitmap; + state.startPosition = { x: x1 % 1000, y: y1 % 1000 }; + state.region = { x: regionX, y: regionY }; + + console.log('โœ… Extraction area overlay enabled'); + } catch (overlayError) { + console.warn('โš ๏ธ Could not set overlay, continuing without visual feedback:', overlayError); + } + + } else { + console.log('๐Ÿ“‹ Auto-Image overlay not available, using console feedback only'); + } + + } catch (error) { + console.error('โŒ Failed to setup extraction area overlay:', error); + // Continue without overlay - not critical for extraction functionality + } + } + // Tile-based pixel scanning for area extraction + async function scanSelectedArea() { + if (!state.selectionArea) { + showAlert('No area selected', 'error'); + return; + } + + console.log('๐Ÿ” Starting area scan...'); + state.isScanning = true; + state.scannedPixels = []; + updateUI(); + + try { + const { x1, y1, x2, y2 } = state.selectionArea; + const areaWidth = x2 - x1 + 1; + const areaHeight = y2 - y1 + 1; + + console.log(`๐Ÿ” Analyzing area ${areaWidth}x${areaHeight} from (${x1},${y1}) to (${x2},${y2})`); + + // Tile calculation + const startTileX = Math.floor(x1 / 1000); + const startTileY = Math.floor(y1 / 1000); + const endTileX = Math.floor(x2 / 1000); + const endTileY = Math.floor(y2 / 1000); + + state.totalPixels = areaWidth * areaHeight; + let processedPixels = 0; + + // Process each tile that intersects with our area + for (let tileY = startTileY; tileY <= endTileY; tileY++) { + for (let tileX = startTileX; tileX <= endTileX; tileX++) { + try { + console.log(`๐Ÿ“„ Processing tile (${tileX}, ${tileY})...`); + + // Download tile data + const tileBlob = await getTileImage(tileX, tileY); + if (!tileBlob) { + console.warn(`โš ๏ธ Could not download tile ${tileX},${tileY}, skipping...`); + continue; + } + + // Process tile data + const tileImageData = await processTileBlob(tileBlob); + if (!tileImageData) { + console.warn(`โš ๏ธ Could not process tile ${tileX},${tileY}, skipping...`); + continue; + } + + // Calculate intersection between area and current tile + const tileStartX = tileX * 1000; + const tileStartY = tileY * 1000; + const tileEndX = tileStartX + 1000; + const tileEndY = tileStartY + 1000; + + const intersectStartX = Math.max(x1, tileStartX); + const intersectStartY = Math.max(y1, tileStartY); + const intersectEndX = Math.min(x2 + 1, tileEndX); + const intersectEndY = Math.min(y2 + 1, tileEndY); + + // Extract pixels from intersection area + for (let globalY = intersectStartY; globalY < intersectEndY; globalY++) { + for (let globalX = intersectStartX; globalX < intersectEndX; globalX++) { + // Convert to tile-local coordinates + const localX = globalX - tileStartX; + const localY = globalY - tileStartY; + + // Normalize coordinates + const normalizedX = (localX % 1000 + 1000) % 1000; + const normalizedY = (localY % 1000 + 1000) % 1000; + + if (normalizedX >= 0 && normalizedX < 1000 && + normalizedY >= 0 && normalizedY < 1000 && + normalizedX < tileImageData.width && + normalizedY < tileImageData.height) { + + // Extract RGBA values + const pixelIndex = (normalizedY * tileImageData.width + normalizedX) * 4; + const r = tileImageData.data[pixelIndex]; + const g = tileImageData.data[pixelIndex + 1]; + const b = tileImageData.data[pixelIndex + 2]; + const a = tileImageData.data[pixelIndex + 3]; + + if (a > 0) { // Skip transparent pixels + // Find closest color in WPlace palette + const closestColor = findClosestColor(r, g, b); + + if (closestColor) { + // Convert to area-relative coordinates + const areaX = globalX - x1; + const areaY = globalY - y1; + + state.scannedPixels.push({ + x: areaX, + y: areaY, + worldX: globalX, + worldY: globalY, + color: closestColor, + rgb: { r, g, b, a } + }); + } + } + } + + processedPixels++; + } + } + + // Update progress every tile + updateUI(); + await new Promise(resolve => setTimeout(resolve, 1)); + + } catch (error) { + console.error(`โŒ Error processing tile ${tileX},${tileY}:`, error); + } + } + } + + console.log(`โœ… Scan complete: ${state.scannedPixels.length} pixels extracted`); + showAlert(`โœ… Extracted ${state.scannedPixels.length} pixels from area`, 'success'); + + } catch (error) { + console.error('โŒ Area extraction failed:', error); + showAlert(`โŒ Extraction failed: ${error.message}`, 'error'); + } finally { + state.isScanning = false; + updateUI(); + } + } + + // Download tile images + async function getTileImage(tileX, tileY) { + try { + const tileUrl = `https://backend.wplace.live/files/s0/tiles/${tileX}/${tileY}.png`; + const response = await fetch(tileUrl); + if (!response.ok) { + throw new Error(`HTTP ${response.status}`); + } + return await response.blob(); + } catch (error) { + console.warn(`Error downloading tile ${tileX},${tileY}:`, error); + return null; + } + } + + // Process tile blob into ImageData + async function processTileBlob(blob) { + try { + const img = new Image(); + const canvas = document.createElement('canvas'); + const ctx = canvas.getContext('2d'); + + return new Promise((resolve, reject) => { + img.onload = () => { + canvas.width = img.width; + canvas.height = img.height; + ctx.drawImage(img, 0, 0); + const imageData = ctx.getImageData(0, 0, canvas.width, canvas.height); + resolve(imageData); + }; + img.onerror = reject; + img.src = URL.createObjectURL(blob); + }); + } catch (error) { + console.error('Error processing tile blob:', error); + return null; + } + } + + // Color matching using LAB color space for better accuracy + function findClosestColor(r, g, b) { + if (!CONFIG.COLOR_MAP) return null; + + let closestColor = null; + let minDistance = Infinity; + + // Convert input RGB to LAB color space + const inputLab = rgbToLab(r, g, b); + + for (const [index, colorInfo] of Object.entries(CONFIG.COLOR_MAP)) { + if (!colorInfo.rgb) continue; // Skip transparent + + const { r: cr, g: cg, b: cb } = colorInfo.rgb; + + // Convert palette color to LAB + const paletteLab = rgbToLab(cr, cg, cb); + + // Calculate Delta-E distance + const deltaE = calculateDeltaE(inputLab, paletteLab); + + if (deltaE < minDistance) { + minDistance = deltaE; + closestColor = colorInfo; + } + } + + return closestColor; + } + + // RGB to LAB color space conversion + function rgbToLab(r, g, b) { + // Normalize RGB values + r = r / 255; + g = g / 255; + b = b / 255; + + // Apply gamma correction + r = r > 0.04045 ? Math.pow((r + 0.055) / 1.055, 2.4) : r / 12.92; + g = g > 0.04045 ? Math.pow((g + 0.055) / 1.055, 2.4) : g / 12.92; + b = b > 0.04045 ? Math.pow((b + 0.055) / 1.055, 2.4) : b / 12.92; + + // Convert to XYZ + const x = r * 0.4124564 + g * 0.3575761 + b * 0.1804375; + const y = r * 0.2126729 + g * 0.7151522 + b * 0.072175; + const z = r * 0.0193339 + g * 0.119192 + b * 0.9503041; + + // Convert XYZ to LAB + const xn = x / 0.95047; + const yn = y / 1.00000; + const zn = z / 1.08883; + + const fx = xn > 0.008856 ? Math.pow(xn, 1/3) : (7.787 * xn + 16/116); + const fy = yn > 0.008856 ? Math.pow(yn, 1/3) : (7.787 * yn + 16/116); + const fz = zn > 0.008856 ? Math.pow(zn, 1/3) : (7.787 * zn + 16/116); + + const l = 116 * fy - 16; + const a = 500 * (fx - fy); + const bLab = 200 * (fy - fz); + + return { l, a, b: bLab }; + } + + // Delta-E color difference calculation + function calculateDeltaE(lab1, lab2) { + const deltaL = lab1.l - lab2.l; + const deltaA = lab1.a - lab2.a; + const deltaB = lab1.b - lab2.b; + + return Math.sqrt(deltaL * deltaL + deltaA * deltaA + deltaB * deltaB); + } + + // Export to JSON (Auto-Image compatible format for Auto-Repair) + function exportToJSON(exportType = 'autofix') { + if (!state.selectionArea || state.scannedPixels.length === 0) { + showAlert('No data to export. Please select an area and scan it first.', 'error'); + return; + } + + console.log(`๐Ÿ“ค Exporting ${state.scannedPixels.length} pixels in ${exportType} format...`); + console.log('๐Ÿ” [DEBUG] Selection area:', state.selectionArea); + console.log('๐Ÿ” [DEBUG] Scanned pixels count:', state.scannedPixels.length); + + try { + // Get filename from input + const filenameInput = state.ui?.querySelector('#filename-input'); + const userFilename = filenameInput?.value?.trim() || '.json'; + let filename; + + if (userFilename === '.json' || userFilename === '') { + // Use default filename with timestamp + const timestamp = new Date().toISOString().slice(0, 19).replace(/:/g, '-'); + filename = `wplace-extracted-${exportType}-${timestamp}.json`; + } else { + // Ensure filename ends with .json + filename = userFilename.endsWith('.json') ? userFilename : userFilename + '.json'; + } + + // Build Auto-Image compatible data structure + const { x1, y1, x2, y2 } = state.selectionArea; + const width = x2 - x1 + 1; + const height = y2 - y1 + 1; + + // Convert scanned pixels to Auto-Image pixel format (Uint8ClampedArray) + const imagePixels = new Uint8ClampedArray(width * height * 4); + imagePixels.fill(0); // Initialize with transparent pixels + + // Fill pixel data from scanned pixels + for (const pixel of state.scannedPixels) { + const pixelIndex = (pixel.y * width + pixel.x) * 4; + if (pixelIndex >= 0 && pixelIndex < imagePixels.length - 3) { + const rgb = pixel.color?.rgb || pixel.rgb; + imagePixels[pixelIndex] = rgb.r; + imagePixels[pixelIndex + 1] = rgb.g; + imagePixels[pixelIndex + 2] = rgb.b; + imagePixels[pixelIndex + 3] = rgb.a || 255; + } + } + + // Create different formats based on export type + let saveData; + + if (exportType === 'autofix') { + // Auto-Fix format: Include exact position and region for restoration + saveData = { + timestamp: Date.now(), + version: '2.2', + state: { + totalPixels: width * height, + paintedPixels: 0, + lastPosition: { x: 0, y: 0 }, + startPosition: { x: x1 % 1000, y: y1 % 1000 }, + region: { x: Math.floor(x1 / 1000), y: Math.floor(y1 / 1000) }, + imageLoaded: true, + colorsChecked: true, + coordinateMode: 'rows', + coordinateDirection: 'top-left', + coordinateSnake: false, + blockWidth: 1, + blockHeight: 1, + availableColors: Object.values(CONFIG.COLOR_MAP).filter(c => c.rgb).map(color => ({ + ...color, + rgb: [color.rgb.r, color.rgb.g, color.rgb.b] // Convert RGB object to array for Auto-Image compatibility + })), + // Additional Auto-Image required fields for compatibility + paintWhitePixels: true, + paintTransparentPixels: false, + displayCharges: 0, + preciseCurrentCharges: 0, + maxCharges: 1, + cooldown: 30000, + stopFlag: false, + selectingPosition: false, + minimized: false, + estimatedTime: 0, + language: 'en', + paintingSpeed: 5, + batchMode: 'normal', + randomBatchMin: 1, + randomBatchMax: 5, + cooldownChargeThreshold: 10, + overlayOpacity: 0.7, + blueMarbleEnabled: false, + ditheringEnabled: true, + colorMatchingAlgorithm: 'lab', + enableChromaPenalty: true, + chromaPenaltyWeight: 0.15, + customTransparencyThreshold: 128, + customWhiteThreshold: 230, + paintUnavailablePixels: true, + // Critical missing fields that Auto-Image expects + fullChargeData: null, + fullChargeInterval: null, + tokenSource: 'generator', + initialSetupComplete: true, + resizeSettings: null, + originalImage: null, + resizeIgnoreMask: null + }, + imageData: { + width: width, + height: height, + pixels: Array.from(imagePixels), + totalPixels: width * height + }, + paintedMapPacked: null + }; + } else { + // Auto-Image format: Empty position and region for manual placement + saveData = { + timestamp: Date.now(), + version: '2.2', + state: { + totalPixels: width * height, + paintedPixels: 0, + lastPosition: { x: 0, y: 0 }, + startPosition: null, // Empty for manual placement + region: null, // Empty for manual placement + imageLoaded: true, + colorsChecked: true, + coordinateMode: 'rows', + coordinateDirection: 'top-left', + coordinateSnake: false, + blockWidth: 1, + blockHeight: 1, + availableColors: Object.values(CONFIG.COLOR_MAP).filter(c => c.rgb).map(color => ({ + ...color, + rgb: [color.rgb.r, color.rgb.g, color.rgb.b] // Convert RGB object to array for Auto-Image compatibility + })), + // Additional Auto-Image required fields for compatibility + paintWhitePixels: true, + paintTransparentPixels: false, + displayCharges: 0, + preciseCurrentCharges: 0, + maxCharges: 1, + cooldown: 30000, + stopFlag: false, + selectingPosition: false, + minimized: false, + estimatedTime: 0, + language: 'en', + paintingSpeed: 5, + batchMode: 'normal', + randomBatchMin: 1, + randomBatchMax: 5, + cooldownChargeThreshold: 10, + overlayOpacity: 0.7, + blueMarbleEnabled: false, + ditheringEnabled: true, + colorMatchingAlgorithm: 'lab', + enableChromaPenalty: true, + chromaPenaltyWeight: 0.15, + customTransparencyThreshold: 128, + customWhiteThreshold: 230, + paintUnavailablePixels: true, + // Critical missing fields that Auto-Image expects + fullChargeData: null, + fullChargeInterval: null, + tokenSource: 'generator', + initialSetupComplete: true, + resizeSettings: null, + originalImage: null, + resizeIgnoreMask: null + }, + imageData: { + width: width, + height: height, + pixels: Array.from(imagePixels), + totalPixels: width * height + }, + paintedMapPacked: null + }; + } + + // Debug: Log the export data structure before saving + console.log('๐Ÿ” [DEBUG] Export data structure:', { + hasState: !!saveData.state, + hasImageData: !!saveData.imageData, + topLevelKeys: Object.keys(saveData), + stateKeys: saveData.state ? Object.keys(saveData.state) : 'N/A', + imageDataKeys: saveData.imageData ? Object.keys(saveData.imageData) : 'N/A', + version: saveData.version, + exportType: exportType, + imageDataPixelsLength: saveData.imageData?.pixels?.length || 0, + imageWidth: saveData.imageData?.width || 0, + imageHeight: saveData.imageData?.height || 0 + }); + + // Validate export data before saving + if (!saveData.state || !saveData.imageData) { + throw new Error('Export validation failed: Missing state or imageData'); + } + + if (!saveData.imageData.pixels || saveData.imageData.pixels.length === 0) { + throw new Error('Export validation failed: No pixel data to export'); + } + + // Download the file using Auto-Image's method + const dataStr = JSON.stringify(saveData, null, 2); + const blob = new Blob([dataStr], { type: 'application/json' }); + const url = URL.createObjectURL(blob); + + const a = document.createElement('a'); + a.href = url; + a.download = filename; + document.body.appendChild(a); + a.click(); + document.body.removeChild(a); + URL.revokeObjectURL(url); + + console.log(`โœ… ${exportType} export complete: ${filename}`); + console.log(`๐Ÿ“ Area: ${width}x${height} from (${x1},${y1}) to (${x2},${y2})`); + + if (exportType === 'autofix') { + console.log(`๐Ÿ”— Compatible with Auto-Repair for restoration`); + showAlert(`โœ… Exported ${state.scannedPixels.length} pixels for Auto-Fix to ${filename}`, 'success'); + } else { + console.log(`๐ŸŽจ Ready for manual placement in Auto-Image`); + showAlert(`โœ… Exported ${state.scannedPixels.length} pixels for Auto-Image to ${filename}`, 'success'); + } + + } catch (error) { + console.error('โŒ Export failed:', error); + showAlert(`โŒ Export failed: ${error.message}`, 'error'); + } + } + + // Filename input handlers + function handleFilenameInput(event) { + const input = event.target; + let value = input.value; + + // Always ensure it ends with .json + if (!value.endsWith('.json')) { + if (value.length > 0 && !value.includes('.json')) { + input.value = value + '.json'; + } else if (value === '') { + input.value = '.json'; + } + } + + // Position cursor before .json + if (input.value.endsWith('.json') && event.inputType !== 'deleteContentBackward') { + const nameLength = input.value.length - 5; // -5 for '.json' + input.setSelectionRange(nameLength, nameLength); + } + } + + function preventJsonDeletion(event) { + const input = event.target; + const cursorPos = input.selectionStart; + const value = input.value; + + // Prevent deletion of .json extension + if ((event.key === 'Backspace' || event.key === 'Delete') && + cursorPos > value.length - 5) { + event.preventDefault(); + return false; + } + + // Position cursor before .json on certain keys + if (event.key === 'End' || event.key === 'ArrowRight') { + const nameLength = value.length - 5; + if (cursorPos >= nameLength) { + event.preventDefault(); + input.setSelectionRange(nameLength, nameLength); + return false; + } + } + } + + // Utility functions + function showAlert(message, type = 'info') { + console.log(`${type === 'error' ? 'โŒ' : type === 'warning' ? 'โš ๏ธ' : type === 'success' ? 'โœ…' : 'โ„น๏ธ'} ${message}`); + + // Create visual alert + const alert = document.createElement('div'); + alert.style.cssText = ` + position: fixed; + top: 20px; + right: 20px; + padding: 12px 20px; + border-radius: 8px; + color: white; + font-family: 'Segoe UI', Arial, sans-serif; + font-size: 14px; + font-weight: 500; + z-index: 10000; + box-shadow: 0 4px 12px rgba(0,0,0,0.3); + animation: slideIn 0.3s ease-out; + max-width: 300px; + ${type === 'error' ? 'background: linear-gradient(135deg, #ff4757, #ff3742);' : + type === 'warning' ? 'background: linear-gradient(135deg, #ffa502, #ff6348);' : + type === 'success' ? 'background: linear-gradient(135deg, #26de81, #20bf6b);' : + 'background: linear-gradient(135deg, #3742fa, #2f3542);'} + `; + alert.textContent = message; + + // Add animation styles + if (!document.getElementById('alert-styles')) { + const style = document.createElement('style'); + style.id = 'alert-styles'; + style.textContent = ` + @keyframes slideIn { + from { transform: translateX(100%); opacity: 0; } + to { transform: translateX(0); opacity: 1; } + } + @keyframes slideOut { + from { transform: translateX(0); opacity: 1; } + to { transform: translateX(100%); opacity: 0; } + } + `; + document.head.appendChild(style); + } + + document.body.appendChild(alert); + + // Auto remove after 3 seconds + setTimeout(() => { + alert.style.animation = 'slideOut 0.3s ease-in'; + setTimeout(() => { + if (alert.parentNode) { + alert.parentNode.removeChild(alert); + } + }, 300); + }, 3000); + } + + // UI Creation and Management + async function createUI() { + console.log('๐ŸŽจ Creating Art Extractor UI...'); + + try { + // Remove existing UI + const existing = document.getElementById('art-extractor-ui'); + if (existing) { + console.log('๐Ÿ—‘๏ธ Removing existing UI'); + existing.remove(); + } + + // Check if document body exists + if (!document.body) { + throw new Error('Document body not available'); + } + + console.log('๐Ÿ”จ Building UI container...'); + + // Create main container + const container = document.createElement('div'); + container.id = 'art-extractor-ui'; + container.className = 'wplace-theme-neon-cyan'; + container.style.cssText = ` + position: fixed; + top: 20px; + left: 20px; + width: 320px; + background: #1959A1; + border: 2px solid #81DCF7; + border-radius: 0; + padding: 20px; + font-family: 'Press Start 2P', monospace; + color: #81DCF7; + z-index: 9999; + box-shadow: 0 0 30px rgba(129, 220, 247, 0.6), inset 0 0 30px rgba(234, 156, 0, 0.1); + user-select: none; + `; + + container.innerHTML = ` + +
+ + +
+

๐ŸŽจ ART EXTRACTOR

+ +
+ + +
+
Status: CAPTURING
+
Area: NONE
+
Pixels: 0
+
+ + +
+ + + +
+ + +
+ +
+ + +
+ + + + + +
+ + +
+ PAINT UPPER-LEFT & LOWER-RIGHT PIXELS +
+ + + + + `; + + // Add event listeners + container.querySelector('#art-extractor-close').addEventListener('click', closeArtExtractor); + // Add event listeners + container.querySelector('#art-extractor-close').addEventListener('click', closeArtExtractor); + container.querySelector('#export-autofix').addEventListener('click', () => exportToJSON('autofix')); + container.querySelector('#export-autoimage').addEventListener('click', () => exportToJSON('autoimage')); + container.querySelector('#start-selecting').addEventListener('click', startPixelCapture); + container.querySelector('#clear-selection').addEventListener('click', clearSelection); + + // Set up filename input with .json enforcement + const filenameInput = container.querySelector('#filename-input'); + if (filenameInput) { + // Initialize with default value + filenameInput.value = '.json'; + + // Add event listeners for .json enforcement + filenameInput.addEventListener('input', handleFilenameInput); + filenameInput.addEventListener('keydown', preventJsonDeletion); + filenameInput.addEventListener('focus', function() { + // Position cursor before .json on focus + const nameLength = this.value.length - 5; + if (nameLength >= 0) { + this.setSelectionRange(nameLength, nameLength); + } + }); + + // Set initial cursor position + setTimeout(() => { + filenameInput.setSelectionRange(0, 0); + }, 100); + } + + // Add to page + document.body.appendChild(container); + state.ui = container; + + console.log('โœ… Art Extractor UI created'); + + // Auto-start pixel capture immediately + console.log('๐ŸŽฏ Auto-starting pixel capture...'); + setTimeout(() => { + startPixelCapture(); + }, 500); + updateUI(); + + } catch (error) { + console.error('โŒ Failed to create UI:', error); + throw error; + } + } + + // UI update function + function updateUI() { + if (!state.ui) return; + + const statusElement = state.ui.querySelector('#current-status'); + const areaElement = state.ui.querySelector('#area-info'); + const pixelElement = state.ui.querySelector('#pixel-count'); + const exportAutofixBtn = state.ui.querySelector('#export-autofix'); + const exportAutoimageBtn = state.ui.querySelector('#export-autoimage'); + + // Update status with Neon Cyan styling + if (state.isCapturing) { + statusElement.textContent = `CAPTURING (${state.capturedPixels.length}/${state.requiredPixels})`; + statusElement.style.color = '#EA9C00'; + statusElement.style.textShadow = '0 0 8px #EA9C00'; + } else if (state.isScanning) { + statusElement.textContent = 'SCANNING...'; + statusElement.style.color = '#39ff14'; + statusElement.style.textShadow = '0 0 8px #39ff14'; + } else if (state.selectionArea) { + statusElement.textContent = 'READY TO EXPORT'; + statusElement.style.color = '#39ff14'; + statusElement.style.textShadow = '0 0 8px #39ff14'; + } else { + statusElement.textContent = 'READY'; + statusElement.style.color = '#81DCF7'; + statusElement.style.textShadow = '0 0 5px #81DCF7'; + } + + // Update area info with uppercase styling + if (state.selectionArea) { + areaElement.textContent = `${state.selectionArea.width}ร—${state.selectionArea.height} PX`; + } else if (state.capturedPixels.length > 0) { + areaElement.textContent = `${state.capturedPixels.length}/${state.requiredPixels} CAPTURED`; + } else { + areaElement.textContent = 'NONE'; + } + + // Update pixel count with progress info + if (state.isScanning && state.totalPixels > 0) { + const progress = Math.round((state.scannedPixels.length / state.totalPixels) * 100); + pixelElement.textContent = `${state.scannedPixels.length}/${state.totalPixels} (${progress}%)`; + } else { + pixelElement.textContent = state.scannedPixels.length.toString(); + } + + // Update export button states + const hasScannedData = state.scannedPixels && state.scannedPixels.length > 0; + + if (exportAutofixBtn) { + exportAutofixBtn.disabled = !hasScannedData; + if (!exportAutofixBtn.disabled) { + exportAutofixBtn.style.opacity = '1'; + exportAutofixBtn.style.cursor = 'pointer'; + } else { + exportAutofixBtn.style.opacity = '0.5'; + exportAutofixBtn.style.cursor = 'not-allowed'; + } + } + + if (exportAutoimageBtn) { + exportAutoimageBtn.disabled = !hasScannedData; + if (!exportAutoimageBtn.disabled) { + exportAutoimageBtn.style.opacity = '1'; + exportAutoimageBtn.style.cursor = 'pointer'; + } else { + exportAutoimageBtn.style.opacity = '0.5'; + exportAutoimageBtn.style.cursor = 'not-allowed'; + } + } + } + + // Control functions + function startPixelCapture() { + console.log('๐ŸŽฏ Starting pixel capture mode...'); + + state.isCapturing = true; + state.capturedPixels = []; + state.selectionArea = null; + state.scannedPixels = []; + + // Clear any existing overlay + if (state.overlayManager) { + state.overlayManager.disable(); + } + + // Set up pixel paint monitoring with fetch interception + const setupSuccess = setupPixelCapture(); + + if (setupSuccess) { + updateUI(); + showAlert('๐ŸŽฏ Paint a pixel at the UPPER-LEFT corner of the area you want to extract', 'info'); + + // Set timeout for capture process + setTimeout(() => { + if (state.isCapturing && state.capturedPixels.length === 0) { + showAlert('โฐ No pixels captured yet. Try painting a pixel or use "Enter Coordinates Manually"', 'warning'); + } + }, 15000); + } else { + showAlert('โš ๏ธ Could not set up automatic capture. Use "Enter Coordinates Manually"', 'warning'); + updateUI(); + } + } + + function clearSelection() { + console.log('๐Ÿงน Clearing pixel capture...'); + + state.isCapturing = false; + state.capturedPixels = []; + state.selectionArea = null; + state.scannedPixels = []; + + // Restore original fetch function + restoreFetch(); + + // Disable overlay + if (state.overlayManager) { + state.overlayManager.disable(); + } + + updateUI(); + showAlert('Pixel capture cleared', 'info'); + } + + function closeArtExtractor() { + console.log('๐Ÿ‘‹ Closing Art Extractor...'); + + // Stop capturing + state.isCapturing = false; + + // Restore original fetch function + restoreFetch(); + + // Clean up overlay + if (state.overlayManager) { + state.overlayManager.disable(); + } + + // Remove UI + if (state.ui) { + state.ui.remove(); + } + + // Reset state + Object.assign(state, { + isCapturing: false, + capturedPixels: [], + selectionArea: null, + ui: null, + overlayManager: null, + isScanning: false, + scannedPixels: [], + paintEventListener: null, + lastPaintEvent: null + }); + + console.log('โœ… Art Extractor closed'); + } + + // Main initialization + async function initialize() { + console.log('๐Ÿš€ Initializing Art Extractor...'); + + try { + // Create UI immediately - don't wait for dependencies + console.log('๐ŸŽจ Creating UI first...'); + await createUI(); + + // Then try to set up dependencies in background + setTimeout(async () => { + try { + // Wait for Auto-Image overlay manager with timeout + if (!window.autoImageOverlayManager) { + console.log('โณ Waiting for Auto-Image overlay manager...'); + await new Promise((resolve, reject) => { + let attempts = 0; + const maxAttempts = 50; // 5 seconds timeout + const check = () => { + attempts++; + if (window.autoImageOverlayManager) { + console.log('โœ… Auto-Image overlay manager found'); + resolve(); + } else if (attempts >= maxAttempts) { + console.warn('โš ๏ธ Auto-Image overlay manager not found, proceeding without it'); + resolve(); // Continue anyway + } else { + setTimeout(check, 100); + } + }; + check(); + }); + } + + // Set up pixel capture monitoring + try { + const captureReady = setupPixelCapture(); + if (captureReady) { + console.log('โœ… Pixel capture monitoring ready'); + } else { + console.warn('โš ๏ธ Pixel capture monitoring setup failed'); + } + } catch (error) { + console.warn('โš ๏ธ Pixel capture error:', error); + } + + } catch (error) { + console.warn('โš ๏ธ Background initialization error:', error); + } + }, 1000); + + console.log('โœ… Art Extractor initialized successfully'); + showAlert('Art Extractor ready! UI loaded - dependencies loading in background.', 'success'); + + } catch (error) { + console.error('โŒ Failed to initialize Art Extractor:', error); + + // Try to create UI anyway as fallback + try { + await createUI(); + showAlert(`Art Extractor loaded with limited functionality: ${error.message}`, 'warning'); + } catch (uiError) { + console.error('โŒ Failed to create UI:', uiError); + showAlert(`Critical initialization failure: ${error.message}`, 'error'); + } + } + } + + // Start initialization with additional error handling + try { + await initialize(); + } catch (error) { + console.error('โŒ Critical error during Art Extractor initialization:', error); + + // Final fallback - try to show minimal UI + try { + const container = document.createElement('div'); + container.innerHTML = ` +
+ โŒ Art Extractor failed to load: ${error.message} + +
+ `; + document.body.appendChild(container); + } catch (e) { + console.error('โŒ Even fallback UI failed:', e); + } + } + + // Expose globally for extension integration + window.WPlaceArtExtractor = { + state, + initialize, + closeArtExtractor, + exportToJSON, + scanSelectedArea + }; + + console.log('๐ŸŽจ WPlace Art Extractor loaded successfully!'); })(); \ No newline at end of file diff --git a/Extension/scripts/Auto-Image.js b/Extension/scripts/Auto-Image.js index b2a51aa4..acbff687 100644 --- a/Extension/scripts/Auto-Image.js +++ b/Extension/scripts/Auto-Image.js @@ -224,6 +224,27 @@ function getText(key, params) { 'pixel-blink': false, }, }, + 'Classic Teal': { + primary: '#2BCAB0', + secondary: '#41726D', + accent: '#046A79', + text: '#0B483C', + highlight: '#0B483C', + success: '#28a745', + error: '#dc3545', + warning: '#ffc107', + fontFamily: "'Segoe UI', Roboto, sans-serif", + borderRadius: '12px', + borderStyle: 'solid', + borderWidth: '1px', + boxShadow: '0 8px 32px rgba(0,0,0,0.15), 0 0 0 1px rgba(0,0,0,0.08)', + backdropFilter: 'none', + animations: { + glow: false, + scanline: false, + 'pixel-blink': false, + }, + }, 'Neon Retro': { primary: '#1a1a2e', secondary: '#16213e', @@ -296,6 +317,30 @@ function getText(key, params) { 'pixel-blink': true, }, }, + 'Neon Retro Purple': { + primary: '#1A172A', + secondary: '#34315E', + accent: '#1A172A', + text: '#F8A9FD', + highlight: '#F8A9FD', + success: '#39ff14', + error: '#ff073a', + warning: '#ffff00', + neon: '#F8A9FD', + purple: '#bf00ff', + pink: '#ff1493', + fontFamily: "'Press Start 2P', monospace", + borderRadius: '0', + borderStyle: 'solid', + borderWidth: '3px', + boxShadow: '0 0 20px rgba(234 156 0, 0.3), inset 0 0 20px rgba(234 156 0, 0.1)', + backdropFilter: 'none', + animations: { + glow: true, + scanline: true, + 'pixel-blink': true, + }, + }, 'Acrylic': { primary: '#00000080', secondary: '#00000040', @@ -360,10 +405,12 @@ function getText(key, params) { document.documentElement.classList.remove( 'wplace-theme-classic', 'wplace-theme-classic-light', + 'wplace-theme-classic-teal', 'wplace-theme-acrylic', 'wplace-theme-neon', 'wplace-theme-neon-cyan', - 'wplace-theme-neon-light' + 'wplace-theme-neon-light', + 'wplace-theme-neon-purple' ); let themeClass = 'wplace-theme-classic'; // default @@ -375,12 +422,18 @@ function getText(key, params) { } else if (CONFIG.currentTheme === 'Classic Light') { themeClass = 'wplace-theme-classic-light'; themeFileName = 'classic-light'; + } else if (CONFIG.currentTheme === 'Classic Teal') { + themeClass = 'wplace-theme-classic-teal'; + themeFileName = 'classic-teal'; } else if (CONFIG.currentTheme === 'Neon Retro Cyan') { themeClass = 'wplace-theme-neon-cyan'; themeFileName = 'neon-cyan'; } else if (CONFIG.currentTheme === 'Neon Retro Light') { themeClass = 'wplace-theme-neon-light'; themeFileName = 'neon-light'; + } else if (CONFIG.currentTheme === 'Neon Retro Purple') { + themeClass = 'wplace-theme-neon-purple'; + themeFileName = 'neon-purple'; } else if (CONFIG.currentTheme === 'Acrylic') { themeClass = 'wplace-theme-acrylic'; themeFileName = 'acrylic'; @@ -997,7 +1050,9 @@ function getText(key, params) { initialSetupComplete: false, // Track if initial startup setup is complete (only happens once) overlayOpacity: CONFIG.OVERLAY.OPACITY_DEFAULT, blueMarbleEnabled: CONFIG.OVERLAY.BLUE_MARBLE_DEFAULT, - ditheringEnabled: false, + ditheringEnabled: true, + invertColorEnabled: false, + ditheringEnabled: true, // Advanced color matching settings colorMatchingAlgorithm: 'lab', enableChromaPenalty: true, @@ -2521,14 +2576,18 @@ function getText(key, params) { let defaultTheme = 'classic'; // fallback if (CONFIG.currentTheme === 'Neon Retro') { defaultTheme = 'neon'; + } else if (CONFIG.currentTheme === 'Acrylic') { + defaultTheme = 'acrylic'; + } else if (CONFIG.currentTheme === 'Classic Light') { + defaultTheme = 'classic-light'; + } else if (CONFIG.currentTheme === 'Classic Teal') { + defaultTheme = 'classic-teal'; } else if (CONFIG.currentTheme === 'Neon Retro Cyan') { defaultTheme = 'neon-cyan'; } else if (CONFIG.currentTheme === 'Neon Retro Light') { defaultTheme = 'neon-light'; - } else if (CONFIG.currentTheme === 'Classic Light') { - defaultTheme = 'classic-light'; - } else if (CONFIG.currentTheme === 'Acrylic') { - defaultTheme = 'acrylic'; + } else if (CONFIG.currentTheme === 'Neon Retro Purple') { + defaultTheme = 'neon-purple'; } console.log(`%c๐ŸŽฏ Loading theme: ${defaultTheme} (${CONFIG.currentTheme})`, 'color: #8b5cf6;'); @@ -3586,16 +3645,22 @@ function getText(key, params) { ${Utils.t('chromaWeight')} ${state.chromaPenaltyWeight} - + -