diff --git a/Extension/Script-manager.js b/Extension/Script-manager.js index 4a93851..3c3c391 100644 --- a/Extension/Script-manager.js +++ b/Extension/Script-manager.js @@ -34,6 +34,13 @@ 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 = ` diff --git a/Extension/auto-image-styles.css b/Extension/auto-image-styles.css index 74a89c6..dff366b 100644 --- a/Extension/auto-image-styles.css +++ b/Extension/auto-image-styles.css @@ -97,11 +97,9 @@ /* Slider colors - defaults for classic compatibility */ --wplace-slider-thumb-bg: white; - --wplace-slider-track-bg: linear-gradient( - to right, - var(--wplace-icon-primary) 0%, - var(--wplace-icon-secondary) 100% - ); + --wplace-slider-track-bg: linear-gradient(to right, + var(--wplace-icon-primary) 0%, + var(--wplace-icon-secondary) 100%); } /* Theme classes are now defined in separate files */ @@ -112,6 +110,7 @@ /* Core animations (shared) */ /* ========================= */ @keyframes neon-glow { + 0%, 100% { text-shadow: @@ -129,6 +128,7 @@ } @keyframes pixel-blink { + 0%, 50% { opacity: 1; @@ -247,7 +247,7 @@ position: fixed; top: 20px; left: 370px; - width: 300px; + width: 350px; max-height: calc(100vh - 40px); padding: 0; z-index: 9997; @@ -1079,7 +1079,7 @@ /* ========================= */ /* Responsive tweaks */ /* ========================= */ -@media (width <= 768px) { +@media (width <=768px) { #wplace-image-bot-container { left: 10px; width: calc(100vw - 20px); @@ -1087,7 +1087,8 @@ } #wplace-stats-container { - display: none !important; /* hide secondary panel on small screens */ + display: none !important; + /* hide secondary panel on small screens */ } .wplace-alert-base { @@ -1992,12 +1993,12 @@ transition: all 0.3s ease; } -.wplace-toggle-switch input:checked + .wplace-toggle-slider { +.wplace-toggle-switch input:checked+.wplace-toggle-slider { background: var(--wplace-success); border-color: var(--wplace-success); } -.wplace-toggle-switch input:checked + .wplace-toggle-slider:before { +.wplace-toggle-switch input:checked+.wplace-toggle-slider:before { transform: translateX(20px); background: white; } @@ -2022,7 +2023,7 @@ justify-content: space-between; padding: 10px 12px; border-bottom: 1px solid var(--wplace-border-color); - transition: all 0.2s ease; + transition: all 0.3s ease, box-shadow 0.5s ease; } .wplace-account-item:last-child { @@ -2036,6 +2037,39 @@ .wplace-account-item.current { background: var(--wplace-success)20; border-left: 3px solid var(--wplace-success); + box-shadow: 0 0 15px rgba(46, 204, 113, 0.6), + 0 0 25px rgba(46, 204, 113, 0.4), + 0 0 35px rgba(46, 204, 113, 0.2); + animation: currentAccountGlow 2s ease-in-out infinite alternate; +} + +@keyframes currentAccountGlow { + from { + box-shadow: 0 0 15px rgba(46, 204, 113, 0.6), + 0 0 25px rgba(46, 204, 113, 0.4), + 0 0 35px rgba(46, 204, 113, 0.2); + } + to { + box-shadow: 0 0 20px rgba(46, 204, 113, 0.8), + 0 0 30px rgba(46, 204, 113, 0.6), + 0 0 40px rgba(46, 204, 113, 0.4); + } +} + +@keyframes currentAccountNumberPulse { + from { + box-shadow: 0 0 10px rgba(46, 204, 113, 0.8); + transform: scale(1); + } + to { + box-shadow: 0 0 15px rgba(46, 204, 113, 1.0); + transform: scale(1.05); + } +} + +.wplace-account-item.next-in-sequence { + background: var(--wplace-warning)15; + border-left: 3px solid var(--wplace-warning); } .wplace-account-info-item { @@ -2157,4 +2191,4 @@ input[type="number"] { .wplace-notification-interval-input { -moz-appearance: textfield; appearance: textfield; -} +} \ No newline at end of file diff --git a/Extension/background.js b/Extension/background.js index 2120fe3..fa41233 100644 --- a/Extension/background.js +++ b/Extension/background.js @@ -41,9 +41,9 @@ async function executeLocalScript(scriptName, tabId) { try { console.log(`Loading script: ${scriptName}`); - // Check if we need to inject dependencies first for Auto-Image.js - if (scriptName === 'Auto-Image.js') { - console.log('πŸ”‘ Auto-Image.js detected, ensuring dependencies are loaded first...'); + // Check if we need to inject dependencies first for Auto-Image.js or Art-Extractor.js + if (scriptName === 'Auto-Image.js' || scriptName === 'Art-Extractor.js') { + console.log(`πŸ”‘ ${scriptName} detected, ensuring dependencies are loaded first...`); try { // First inject token-manager.js @@ -777,34 +777,39 @@ async function setCookie(value) { const cleaned = value.trim(); console.log("[bg] setCookie CALLED with:", cleaned); - chrome.cookies.set( - { - url: "https://backend.wplace.live/", - name: "j", - value: cleaned, - domain: cookieDomain, - path: "/", - }, - (cookie) => { - if (chrome.runtime.lastError) { - console.error( - "[bg] cookie set error:", - chrome.runtime.lastError.message - ); - } else { - console.log("[bg] cookie set result:", cookie); + return new Promise((resolve, reject) => { + chrome.cookies.set( + { + url: "https://backend.wplace.live/", + name: "j", + value: cleaned, + domain: cookieDomain, + path: "/", + }, + (cookie) => { + if (chrome.runtime.lastError) { + console.error( + "[bg] cookie set error:", + chrome.runtime.lastError.message + ); + reject(new Error(chrome.runtime.lastError.message)); + } else { + console.log("[bg] cookie set result:", cookie); - chrome.tabs.query({ active: true, currentWindow: true }, (tabs) => { - if (tabs.length > 0) { - chrome.tabs.sendMessage(tabs[0].id, { - type: "cookieSet", - value: cleaned, - }); - } - }); + chrome.tabs.query({ active: true, currentWindow: true }, (tabs) => { + if (tabs.length > 0) { + chrome.tabs.sendMessage(tabs[0].id, { + type: "cookieSet", + value: cleaned, + }); + } + }); + + resolve(cookie); + } } - } - ); + ); + }); } async function deleteAccountAtIndex(index) { diff --git a/Extension/content-script.js b/Extension/content-script.js index dc478e5..de3f4a0 100644 --- a/Extension/content-script.js +++ b/Extension/content-script.js @@ -279,10 +279,19 @@ if (window.location.hostname === 'wplace.live') { if (event.data.source !== "my-userscript") return; const message = event.data; if (message.type === "setCookie" && message.value) { + console.log("πŸͺ Content script received setCookie message:", message); chrome.runtime.sendMessage( { type: "setCookie", value: message.value }, (response) => { - if (response.status === "ok") { + console.log("πŸ“₯ Content script received response from background:", response); + // Send response back to userscript + window.postMessage({ + type: "setCookieResponse", + status: response ? response.status || 'ok' : 'error', + originalMessage: message + }, "*"); + + if (response && response.status === "ok") { console.log("βœ… Forwarded token to background."); } } diff --git a/Extension/manifest.json b/Extension/manifest.json index 2209b6c..cf2c615 100644 --- a/Extension/manifest.json +++ b/Extension/manifest.json @@ -1,7 +1,7 @@ { "manifest_version": 3, "name": "WPlace AutoBOT Script Launcher", - "version": "2.0.0", + "version": "2.0.2", "description": "Launch and manage automation scripts for WPlace", "icons": { "16": "icons/icon16.png", diff --git a/Extension/popup/popup.html b/Extension/popup/popup.html index f15d549..fd2edb5 100644 --- a/Extension/popup/popup.html +++ b/Extension/popup/popup.html @@ -105,7 +105,7 @@ @@ -114,4 +114,3 @@ - diff --git a/Extension/popup/popup.js b/Extension/popup/popup.js index 3dcb07f..7006768 100644 --- a/Extension/popup/popup.js +++ b/Extension/popup/popup.js @@ -26,6 +26,12 @@ const AVAILABLE_SCRIPTS = [ displayName: 'πŸ”§ Auto Repair', description: 'Automated repair and maintenance tasks', filename: 'Auto-Repair.js' + }, + { + name: 'Art-Extractor.js', + displayName: '🎨 Art Extractor', + description: 'Extract artwork areas from canvas and save as JSON files', + filename: 'Art-Extractor.js' } ]; diff --git a/Extension/scripts/Art-Extractor.js b/Extension/scripts/Art-Extractor.js new file mode 100644 index 0000000..fd22307 --- /dev/null +++ b/Extension/scripts/Art-Extractor.js @@ -0,0 +1,1671 @@ +// ==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✨ Now featuring Auto-Guard style interactive pixel capture!', '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 (different from Auto-Guard's red) + CORNER_MARKER_COLOR: { r: 255, g: 255, b: 0, a: 255 }, // Yellow corners like Auto-Guard + CORNER_MARKER_SIZE: 12, // Larger markers like Auto-Guard + MIN_AREA_SIZE: 1, + MAX_AREA_SIZE: 1000, + 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 Auto-Guard's fetch interception method + let originalFetch = null; + let fetchInterceptionActive = false; + + function setupPixelCapture() { + console.log('🎯 Setting up Auto-Guard style pixel paint monitoring...'); + + try { + // Store original fetch if not already stored + if (!originalFetch) { + originalFetch = window.fetch; + } + + // Install fetch interceptor (Auto-Guard method) + 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('βœ… Auto-Guard style fetch interception enabled'); + } + + return true; + + } catch (error) { + console.warn('⚠️ Could not set up Auto-Guard style 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 (Auto-Guard style 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(); + + // Auto-Guard style 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 (Auto-Guard style 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)); + + // Auto-Guard style 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, // Auto-Guard style 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 (different color from Auto-Guard's protection 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 (like Auto-Guard) + 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 + } + } + // Auto-Guard style tile-based pixel scanning for area extraction + async function scanSelectedArea() { + if (!state.selectionArea) { + showAlert('No area selected', 'error'); + return; + } + + console.log('πŸ” Starting Auto-Guard style 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})`); + + // Auto-Guard style 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 (Auto-Guard method) + for (let tileY = startTileY; tileY <= endTileY; tileY++) { + for (let tileX = startTileX; tileX <= endTileX; tileX++) { + try { + console.log(`πŸ“„ Processing tile (${tileX}, ${tileY})...`); + + // Download tile data using Auto-Guard's getTileImage method + const tileBlob = await getTileImage(tileX, tileY); + if (!tileBlob) { + console.warn(`⚠️ Could not download tile ${tileX},${tileY}, skipping...`); + continue; + } + + // Process tile data like Auto-Guard + 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 (Auto-Guard style) + 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 (Auto-Guard normalization) + 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 (Auto-Guard method) + 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 (Auto-Guard method) + 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(`βœ… Auto-Guard style 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(); + } + } + + // Auto-Guard's getTileImage function for downloading tiles + 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 like Auto-Guard + 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; + } + } + + // Auto-Guard style 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 (Auto-Guard method) + 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 (Auto-Guard method) + const deltaE = calculateDeltaE(inputLab, paletteLab); + + if (deltaE < minDistance) { + minDistance = deltaE; + closestColor = colorInfo; + } + } + + return closestColor; + } + + // Auto-Guard's 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 }; + } + + // Auto-Guard's 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 Auto-Guard style 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 Auto-Guard style 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 f586822..ed749b1 100644 --- a/Extension/scripts/Auto-Image.js +++ b/Extension/scripts/Auto-Image.js @@ -23,7 +23,7 @@ function getText(key, params) { } return text; } - + // Try with state.language if available if (window.state && window.state.language && window.loadedTranslations && window.loadedTranslations[window.state.language] && window.loadedTranslations[window.state.language][key]) { let text = window.loadedTranslations[window.state.language][key]; @@ -37,7 +37,7 @@ function getText(key, params) { } catch (error) { console.warn('Error in getText fallback:', error); } - + return key; // Fallback to key if no translation found } @@ -48,7 +48,7 @@ function getText(key, params) { return; } window.WPLACE_AUTO_IMAGE_LOADED = true; - + console.log('%cπŸš€ WPlace AutoBot Auto-Image Starting...', 'color: #00ff41; font-weight: bold; font-size: 16px;'); // CONFIGURATION CONSTANTS @@ -821,6 +821,8 @@ function getText(key, params) { _saveInProgress: false, paintedMap: null, accountIndex: 0, + originalAccountOrder: [], // Store the original order of accounts + currentActiveIndex: 0, // Track current position in the original order // Account switching state allAccountsInfo: [], isFetchingAllAccounts: false, @@ -841,7 +843,7 @@ function getText(key, params) { console.error('❌ WPlaceOverlayManager not available - please ensure overlay-manager.js is loaded first'); throw new Error('OverlayManager dependency not found'); } - + // Ensure we only have ONE overlay manager instance globally let overlayManager; if (!window.autoImageOverlayManager) { @@ -859,16 +861,16 @@ function getText(key, params) { console.error('❌ WPlaceTokenManager not available - please ensure token-manager.js is loaded first'); throw new Error('TokenManager dependency not found'); } - + const tokenManager = window.WPlaceTokenManager; const setTurnstileToken = (token) => tokenManager.setTurnstileToken(token); const isTokenValid = () => tokenManager.isTokenValid(); const invalidateToken = () => tokenManager.invalidateToken(); const ensureToken = (forceRefresh = false) => tokenManager.ensureToken(forceRefresh); - + // Getter for the current token value const getTurnstileToken = () => tokenManager.turnstileToken; - + // Token promise management const createTokenPromise = () => { tokenManager.tokenPromise = new Promise((resolve) => { @@ -881,9 +883,9 @@ function getText(key, params) { console.error('❌ WPlaceImageProcessor not available - please ensure image-processor.js is loaded first'); throw new Error('ImageProcessor dependency not found'); } - + const globalImageProcessor = new window.WPlaceImageProcessor(); - + // Keep these constants for compatibility with other parts of the script let retryCount = 0; const MAX_RETRIES = 10; @@ -912,7 +914,7 @@ function getText(key, params) { const originalFetch = window.fetch; // Setup fetch interception - + window.fetch = async function (...args) { const response = await originalFetch.apply(this, args); const url = args[0] instanceof Request ? args[0].url : args[0]; @@ -1004,13 +1006,13 @@ function getText(key, params) { sleep: (ms) => window.globalUtilsManager ? window.globalUtilsManager.sleep(ms) : new Promise(r => setTimeout(r, ms)), dynamicSleep: (tickAndGetRemainingMs) => window.globalUtilsManager ? window.globalUtilsManager.dynamicSleep(tickAndGetRemainingMs) : Promise.resolve(), waitForSelector: (selector, interval, timeout) => window.globalUtilsManager ? window.globalUtilsManager.waitForSelector(selector, interval, timeout) : Promise.resolve(null), - msToTimeText: (ms) => window.globalUtilsManager ? window.globalUtilsManager.msToTimeText(ms) : `${Math.ceil(ms/1000)}s`, - createScrollToAdjust: (element, updateCallback, min, max, step) => window.globalUtilsManager ? window.globalUtilsManager.createScrollToAdjust(element, updateCallback, min, max, step) : () => {}, - + msToTimeText: (ms) => window.globalUtilsManager ? window.globalUtilsManager.msToTimeText(ms) : `${Math.ceil(ms / 1000)}s`, + createScrollToAdjust: (element, updateCallback, min, max, step) => window.globalUtilsManager ? window.globalUtilsManager.createScrollToAdjust(element, updateCallback, min, max, step) : () => { }, + // Tile calculation - calculateTileRange: (startRegionX, startRegionY, startPixelX, startPixelY, width, height, tileSize) => + calculateTileRange: (startRegionX, startRegionY, startPixelX, startPixelY, width, height, tileSize) => window.globalUtilsManager ? window.globalUtilsManager.calculateTileRange(startRegionX, startRegionY, startPixelX, startPixelY, width, height, tileSize) : {}, - + // Token management loadTurnstile: () => window.globalUtilsManager ? window.globalUtilsManager.loadTurnstile() : Promise.resolve(), ensureTurnstileContainer: () => window.globalUtilsManager ? window.globalUtilsManager.ensureTurnstileContainer() : Promise.resolve(), @@ -1020,7 +1022,7 @@ function getText(key, params) { createTurnstileWidgetInteractive: (sitekey, action) => window.globalUtilsManager ? window.globalUtilsManager.createTurnstileWidgetInteractive(sitekey, action) : Promise.resolve(), cleanupTurnstile: () => window.globalUtilsManager ? window.globalUtilsManager.cleanupTurnstile() : Promise.resolve(), obtainSitekeyAndToken: (fallback) => window.globalUtilsManager ? window.globalUtilsManager.obtainSitekeyAndToken(fallback) : Promise.resolve(fallback), - + // DOM utilities createElement: (tag, props, children) => window.globalUtilsManager ? window.globalUtilsManager.createElement(tag, props, children) : document.createElement(tag), createButton: (id, text, icon, onClick, style) => { @@ -1033,7 +1035,7 @@ function getText(key, params) { if (onClick) btn.addEventListener('click', onClick); return btn; }, - + // Translation with fallback t: (key, params) => { if (window.globalUtilsManager) { @@ -1048,13 +1050,13 @@ function getText(key, params) { console.log(`Alert [${type}]: ${message}`); } }, - + // Color utilities colorDistance: (a, b) => window.globalUtilsManager ? window.globalUtilsManager.colorDistance(a, b) : Math.sqrt(Math.pow(a[0] - b[0], 2) + Math.pow(a[1] - b[1], 2) + Math.pow(a[2] - b[2], 2)), findClosestPaletteColor: (r, g, b, palette) => window.globalUtilsManager ? window.globalUtilsManager.findClosestPaletteColor(r, g, b, palette) : [r, g, b], isWhitePixel: (r, g, b) => window.globalUtilsManager ? window.globalUtilsManager.isWhitePixel(r, g, b) : (r > 240 && g > 240 && b > 240), resolveColor: (targetRgb, availableColors, exactMatch) => window.globalUtilsManager ? window.globalUtilsManager.resolveColor(targetRgb, availableColors, exactMatch) : targetRgb, - + // CRITICAL FUNCTIONS - Direct implementations to ensure they work createImageUploader: () => { return new Promise((resolve, reject) => { @@ -1075,7 +1077,7 @@ function getText(key, params) { input.click(); }); }, - + createFileUploader: () => { return new Promise((resolve, reject) => { const input = document.createElement('input'); @@ -1102,7 +1104,7 @@ function getText(key, params) { input.click(); }); }, - + createFileDownloader: (data, filename) => { const blob = new Blob([data], { type: 'application/json' }); const url = URL.createObjectURL(blob); @@ -1114,7 +1116,7 @@ function getText(key, params) { document.body.removeChild(a); URL.revokeObjectURL(url); }, - + loadProgress: () => { try { const saved = localStorage.getItem('wplace-bot-progress'); @@ -1125,12 +1127,12 @@ function getText(key, params) { return null; } }, - + saveProgress: () => { try { // Use utils manager to build proper save structure like old version - const data = window.globalUtilsManager ? - window.globalUtilsManager.buildProgressData() : + const data = window.globalUtilsManager ? + window.globalUtilsManager.buildProgressData() : { timestamp: Date.now(), version: '2.2', @@ -1155,10 +1157,10 @@ function getText(key, params) { pixels: Array.from(window.state.imageData.pixels), totalPixels: window.state.imageData.totalPixels, } : null, - paintedMapPacked: window.globalUtilsManager ? + paintedMapPacked: window.globalUtilsManager ? window.globalUtilsManager.buildPaintedMapPacked() : null }; - + localStorage.setItem('wplace-bot-progress', JSON.stringify(data)); return true; } catch (error) { @@ -1166,7 +1168,7 @@ function getText(key, params) { return false; } }, - + clearProgress: () => { try { localStorage.removeItem('wplace-bot-progress'); @@ -1181,41 +1183,76 @@ function getText(key, params) { return false; } }, - + restoreProgress: (savedData) => { try { - if (!savedData || !savedData.state) return false; + console.log('πŸ” [DEBUG] RestoreProgress: Starting restoration...'); + console.log('πŸ” [DEBUG] SavedData exists:', !!savedData); + console.log('πŸ” [DEBUG] SavedData.state exists:', !!savedData?.state); + + if (!savedData || !savedData.state) { + console.error('❌ [DEBUG] RestoreProgress: Invalid savedData or missing state'); + return false; + } + + console.log('πŸ” [DEBUG] Current window.state keys before restore:', Object.keys(window.state || {})); + console.log('πŸ” [DEBUG] SavedData.state keys:', Object.keys(savedData.state)); + + console.log('πŸ” [DEBUG] Performing Object.assign...'); Object.assign(window.state, savedData.state); + console.log('βœ… [DEBUG] Object.assign completed successfully'); // Restore available colors from old save files (backward compatibility) if (savedData.state.availableColors && Array.isArray(savedData.state.availableColors)) { + console.log('πŸ” [DEBUG] Restoring availableColors, count:', savedData.state.availableColors.length); window.state.availableColors = savedData.state.availableColors; window.state.colorsChecked = true; // Mark colors as checked + console.log('βœ… [DEBUG] AvailableColors restored successfully'); + } else { + console.log('⚠️ [DEBUG] No availableColors found in savedData or not an array'); } - + if (savedData.imageData) { + console.log('πŸ” [DEBUG] Restoring imageData...'); + console.log('πŸ” [DEBUG] ImageData type check - pixels is array:', Array.isArray(savedData.imageData.pixels)); + console.log('πŸ” [DEBUG] ImageData pixels length:', savedData.imageData.pixels?.length); + window.state.imageData = { ...savedData.imageData, pixels: new Uint8ClampedArray(savedData.imageData.pixels), }; + console.log('βœ… [DEBUG] ImageData restored successfully'); + console.log('πŸ” [DEBUG] Converted pixels to Uint8ClampedArray, length:', window.state.imageData.pixels.length); + } else { + console.log('⚠️ [DEBUG] No imageData found in savedData'); } - + if (savedData.paintedMap) { + console.log('πŸ” [DEBUG] Restoring paintedMap...'); window.state.paintedMap = savedData.paintedMap.map((row) => Array.from(row)); + console.log('βœ… [DEBUG] PaintedMap restored successfully'); + } else { + console.log('πŸ” [DEBUG] No paintedMap found in savedData (this is normal for extracted data)'); } + console.log('βœ… [DEBUG] RestoreProgress completed successfully'); + console.log('πŸ” [DEBUG] Final window.state.imageLoaded:', window.state.imageLoaded); + console.log('πŸ” [DEBUG] Final window.state.totalPixels:', window.state.totalPixels); + return true; } catch (error) { - console.error('Error restoring progress:', error); + console.error('❌ [DEBUG] Error in restoreProgress:', error); + console.error('❌ [DEBUG] Error stack:', error.stack); + console.error('❌ [DEBUG] SavedData structure at error:', JSON.stringify(savedData, null, 2)); return false; } }, - + saveProgressToFile: () => { try { // Use utils manager to build proper save structure like old version - const progressData = window.globalUtilsManager ? - window.globalUtilsManager.buildProgressData() : + const progressData = window.globalUtilsManager ? + window.globalUtilsManager.buildProgressData() : { timestamp: Date.now(), version: '2.2', @@ -1240,10 +1277,10 @@ function getText(key, params) { pixels: Array.from(window.state.imageData.pixels), totalPixels: window.state.imageData.totalPixels, } : null, - paintedMapPacked: window.globalUtilsManager ? + paintedMapPacked: window.globalUtilsManager ? window.globalUtilsManager.buildPaintedMapPacked() : null }; - + const filename = `wplace-bot-progress-${new Date() .toISOString() .slice(0, 19) @@ -1255,21 +1292,85 @@ function getText(key, params) { return false; } }, - + loadProgressFromFile: async () => { try { + console.log('πŸ” [DEBUG] Starting file upload process...'); const data = await Utils.createFileUploader(); - if (!data || !data.state) { - throw new Error('Invalid file format'); + + console.log('πŸ” [DEBUG] File upload completed. Data type:', typeof data); + console.log('πŸ” [DEBUG] Data exists:', !!data); + + if (!data) { + console.error('❌ [DEBUG] No data received from file uploader'); + throw new Error('No data received from file'); } + + console.log('πŸ” [DEBUG] Data keys:', Object.keys(data)); + console.log('πŸ” [DEBUG] Data.state exists:', !!data.state); + console.log('πŸ” [DEBUG] Data.imageData exists:', !!data.imageData); + console.log('πŸ” [DEBUG] Data.version:', data.version); + console.log('πŸ” [DEBUG] Data.timestamp:', data.timestamp); + + if (!data.state) { + console.error('❌ [DEBUG] Missing state object in loaded data'); + console.log('πŸ” [DEBUG] Available top-level keys:', Object.keys(data)); + throw new Error('Invalid file format - missing state object'); + } + + console.log('πŸ” [DEBUG] State object keys:', Object.keys(data.state)); + console.log('πŸ” [DEBUG] State.imageLoaded:', data.state.imageLoaded); + console.log('πŸ” [DEBUG] State.totalPixels:', data.state.totalPixels); + console.log('πŸ” [DEBUG] State.startPosition:', data.state.startPosition); + console.log('πŸ” [DEBUG] State.region:', data.state.region); + + if (data.imageData) { + console.log('πŸ” [DEBUG] ImageData width:', data.imageData.width); + console.log('πŸ” [DEBUG] ImageData height:', data.imageData.height); + console.log('πŸ” [DEBUG] ImageData pixels length:', data.imageData.pixels?.length); + console.log('πŸ” [DEBUG] ImageData totalPixels:', data.imageData.totalPixels); + } + + console.log('πŸ” [DEBUG] Calling restoreProgress...'); const success = Utils.restoreProgress(data); + console.log('πŸ” [DEBUG] RestoreProgress result:', success); + return success; } catch (error) { - console.error('Error loading from file:', error); + console.error('❌ [DEBUG] Error in loadProgressFromFile:', error); + console.error('❌ [DEBUG] Error stack:', error.stack); return false; } }, - + + loadExtractedFileData: async () => { + try { + console.log('πŸ” [DEBUG] Starting Art-Extractor file upload process...'); + const data = await Utils.createFileUploader(); + + console.log('πŸ” [DEBUG] File upload completed. Data type:', typeof data); + console.log('πŸ” [DEBUG] Data exists:', !!data); + console.log('πŸ“– Raw loaded data:', data); + + if (!data) { + console.error('❌ [DEBUG] No data received from file uploader'); + return null; + } + + console.log('πŸ” [DEBUG] Data keys:', Object.keys(data)); + console.log('πŸ” [DEBUG] Data.state exists:', !!data.state); + console.log('πŸ” [DEBUG] Data.imageData exists:', !!data.imageData); + console.log('πŸ” [DEBUG] Data.version:', data.version); + console.log('πŸ” [DEBUG] Data.timestamp:', data.timestamp); + + console.log('πŸ” [DEBUG] Returning raw data object for Load Extracted feature'); + return data; + } catch (error) { + console.error('❌ [DEBUG] Error in loadExtractedFileData:', error); + return null; + } + }, + // Other utilities - delegate to globalUtilsManager or provide simple fallbacks extractAvailableColors: () => { if (window.globalUtilsManager) { @@ -1280,7 +1381,7 @@ function getText(key, params) { return []; } }, - formatTime: (ms) => window.globalUtilsManager ? window.globalUtilsManager.formatTime(ms) : `${Math.floor(ms/1000)}s`, + formatTime: (ms) => window.globalUtilsManager ? window.globalUtilsManager.formatTime(ms) : `${Math.floor(ms / 1000)}s`, calculateEstimatedTime: (remainingPixels, charges, cooldown) => window.globalUtilsManager ? window.globalUtilsManager.calculateEstimatedTime(remainingPixels, charges, cooldown) : 0, initializePaintedMap: (width, height) => window.globalUtilsManager ? window.globalUtilsManager.initializePaintedMap(width, height) : console.log('Painted map not available'), markPixelPainted: (x, y, regionX, regionY) => window.globalUtilsManager ? window.globalUtilsManager.markPixelPainted(x, y, regionX, regionY) : false, @@ -1304,7 +1405,7 @@ function getText(key, params) { constructor(imageSrc) { // Create instance of the modular ImageProcessor this._processor = new window.WPlaceImageProcessor(imageSrc); - + // Legacy compatibility properties this.imageSrc = imageSrc; this.img = null; @@ -1314,7 +1415,7 @@ function getText(key, params) { async load() { await this._processor.load(); - + // Update legacy properties for compatibility this.img = this._processor.img; this.canvas = this._processor.canvas; @@ -1331,12 +1432,12 @@ function getText(key, params) { resize(newWidth, newHeight) { const result = this._processor.resize(newWidth, newHeight); - + // Update legacy properties this.img = this._processor.img; this.canvas = this._processor.canvas; this.ctx = this._processor.ctx; - + return result; } @@ -1539,7 +1640,7 @@ function getText(key, params) { // UI UPDATE FUNCTIONS (declared early to avoid reference errors) let updateUI = () => { }; - let updateStats = () => { }; + let updateStats = async () => { }; let updateDataButtons = () => { }; function updateActiveColorPalette() { @@ -1984,6 +2085,12 @@ function getText(key, params) { ${Utils.t('uploadImage')} + + +
@@ -2039,7 +2146,7 @@ function getText(key, params) {
- +
πŸ’Ύ Data Management
@@ -2345,7 +2452,7 @@ function getText(key, params) {
- +
@@ -3036,6 +3143,7 @@ function getText(key, params) { try { await updateStats(); + await updateCurrentAccountInList(); } catch (error) { console.error('Error refreshing charges:', error); } finally { @@ -3570,7 +3678,7 @@ function getText(key, params) { } if (loadBtn) { - loadBtn.addEventListener('click', () => { + loadBtn.addEventListener('click', async () => { const savedData = Utils.loadProgress(); if (!savedData) { updateUI('noSavedData', 'warning'); @@ -3580,10 +3688,10 @@ function getText(key, params) { // CRITICAL FIX: If save file contains complete data, bypass initial setup check // This matches old version behavior where save files could be loaded immediately - const hasCompleteData = savedData.state && savedData.imageData && - savedData.state.availableColors && - savedData.state.availableColors.length > 0; - + const hasCompleteData = savedData.state && savedData.imageData && + savedData.state.availableColors && + savedData.state.availableColors.length > 0; + if (!state.initialSetupComplete && !hasCompleteData) { Utils.showAlert(Utils.t('pleaseWaitInitialSetup'), 'warning'); return; @@ -3608,14 +3716,14 @@ function getText(key, params) { updateDataButtons(); // CRITICAL FIX: Force immediate stats update after save loading - updateStats(); - + await updateStats(); + // Force stats display refresh by updating the stats container const statsContainer = document.getElementById('wplace-stats-container'); if (statsContainer && typeof updateStats === 'function') { // Trigger a forced visual refresh of the stats - setTimeout(() => { - updateStats(); + setTimeout(async () => { + await updateStats(); }, 100); } @@ -3626,8 +3734,12 @@ function getText(key, params) { if (!state.colorsChecked) { uploadBtn.disabled = false; + const loadExtractedBtn = document.getElementById('loadExtractedBtn'); + if (loadExtractedBtn) loadExtractedBtn.disabled = false; } else { uploadBtn.disabled = false; + const loadExtractedBtn = document.getElementById('loadExtractedBtn'); + if (loadExtractedBtn) loadExtractedBtn.disabled = false; selectPosBtn.disabled = false; } @@ -3655,28 +3767,52 @@ function getText(key, params) { if (loadFromFileBtn) { loadFromFileBtn.addEventListener('click', async () => { + console.log('πŸ” [DEBUG] Load from file button clicked'); + console.log('πŸ” [DEBUG] Initial setup complete:', state.initialSetupComplete); + try { // First check if we can load the file - this will tell us if it has complete data + console.log('πŸ” [DEBUG] Starting file upload...'); const fileData = await Utils.createFileUploader(); + + console.log('πŸ” [DEBUG] File data received:', !!fileData); + console.log('πŸ” [DEBUG] File data keys:', fileData ? Object.keys(fileData) : 'N/A'); + if (!fileData || !fileData.state) { + console.error('❌ [DEBUG] Invalid file data or missing state'); + console.log('πŸ” [DEBUG] FileData:', fileData); Utils.showAlert(Utils.t('invalidFileFormat'), 'error'); return; } // CRITICAL FIX: If file contains complete data, bypass initial setup check - const hasCompleteData = fileData.state && fileData.imageData && - fileData.state.availableColors && - fileData.state.availableColors.length > 0; - + const hasCompleteData = fileData.state && fileData.imageData && + fileData.state.availableColors && + fileData.state.availableColors.length > 0; + + console.log('πŸ” [DEBUG] Has complete data check:'); + console.log(' - fileData.state:', !!fileData.state); + console.log(' - fileData.imageData:', !!fileData.imageData); + console.log(' - fileData.state.availableColors:', !!fileData.state.availableColors); + console.log(' - availableColors length:', fileData.state.availableColors?.length); + console.log(' - hasCompleteData result:', hasCompleteData); + if (!state.initialSetupComplete && !hasCompleteData) { + console.log('⚠️ [DEBUG] Setup not complete and no complete data, showing warning'); Utils.showAlert(Utils.t('pleaseWaitFileSetup'), 'warning'); return; } + console.log('πŸ” [DEBUG] Calling restoreProgress...'); const success = Utils.restoreProgress(fileData); + console.log('πŸ” [DEBUG] RestoreProgress result:', success); + if (success) { + console.log('βœ… [DEBUG] File loaded successfully'); + // CRITICAL FIX: Set initial setup complete if file had complete data if (hasCompleteData) { + console.log('πŸ” [DEBUG] Setting initialSetupComplete to true due to complete data'); state.initialSetupComplete = true; } @@ -3684,30 +3820,51 @@ function getText(key, params) { Utils.showAlert(Utils.t('fileLoaded'), 'success'); updateDataButtons(); + console.log('πŸ” [DEBUG] Updating stats...'); await updateStats(); // Restore overlay if image data was loaded from file + console.log('πŸ” [DEBUG] Attempting to restore overlay...'); await Utils.restoreOverlayFromData().catch((error) => { - console.error('Failed to restore overlay from file:', error); + console.error('❌ [DEBUG] Failed to restore overlay from file:', error); }); + console.log('πŸ” [DEBUG] Updating button states after load...'); + console.log(' - state.colorsChecked:', state.colorsChecked); + console.log(' - state.imageLoaded:', state.imageLoaded); + console.log(' - state.startPosition:', !!state.startPosition); + console.log(' - state.region:', !!state.region); + if (state.colorsChecked) { + console.log('πŸ” [DEBUG] Enabling buttons due to colorsChecked'); uploadBtn.disabled = false; selectPosBtn.disabled = false; resizeBtn.disabled = false; } else { + console.log('πŸ” [DEBUG] Only enabling upload button'); uploadBtn.disabled = false; } if (state.imageLoaded && state.startPosition && state.region && state.colorsChecked) { + console.log('πŸ” [DEBUG] All conditions met, enabling start button'); startBtn.disabled = false; + } else { + console.log('⚠️ [DEBUG] Start button requirements not met'); } + } else { + console.error('❌ [DEBUG] RestoreProgress failed'); + Utils.showAlert('File loading failed - check console for details', 'error'); } } catch (error) { + console.error('❌ [DEBUG] Error in file load process:', error); + console.error('❌ [DEBUG] Error stack:', error.stack); + if (error.message === 'Invalid JSON file') { + console.error('❌ [DEBUG] Invalid JSON file detected'); Utils.showAlert(Utils.t('invalidFileFormat'), 'error'); } else { - Utils.showAlert(Utils.t('fileError'), 'error'); + console.error('❌ [DEBUG] General file error:', error.message); + Utils.showAlert(`File error: ${error.message}`, 'error'); } } }); @@ -3793,23 +3950,21 @@ function getText(key, params) { const shouldCallApi = isFirstCheck || isTimeToUpdate; - if (shouldCallApi) { - const { charges, max, cooldown } = await WPlaceService.getCharges(); - state.displayCharges = Math.floor(charges); - state.preciseCurrentCharges = charges; - state.cooldown = cooldown; - state.maxCharges = Math.floor(max) > 1 ? Math.floor(max) : state.maxCharges; + const { charges, max, cooldown } = await WPlaceService.getCharges(); + state.displayCharges = Math.floor(charges); + state.preciseCurrentCharges = charges; + state.cooldown = cooldown; + state.maxCharges = Math.floor(max) > 1 ? Math.floor(max) : state.maxCharges; - state.fullChargeData = { - current: charges, - max: max, - cooldownMs: cooldown, - startTime: Date.now(), - spentSinceShot: 0, - }; - // Evaluate notifications every time we refresh server-side charges - NotificationManager.maybeNotifyChargesReached(); - } + state.fullChargeData = { + current: charges, + max: max, + cooldownMs: cooldown, + startTime: Date.now(), + spentSinceShot: 0, + }; + // Evaluate notifications every time we refresh server-side charges + NotificationManager.maybeNotifyChargesReached(); if (state.fullChargeInterval) { clearInterval(state.fullChargeInterval); @@ -3828,18 +3983,18 @@ function getText(key, params) { cooldownInput.max = state.maxCharges; } - // Update current account charges in the account list - updateCurrentAccountInList(); + // // Update current account charges in the account list + // await updateCurrentAccountInList(); let imageStatsHTML = ''; if (state.imageLoaded) { const progress = state.totalPixels > 0 ? Math.round((state.paintedPixels / state.totalPixels) * 100) : 0; const remainingPixels = state.totalPixels - state.paintedPixels; - + // Updated estimation calculation for new cooldown logic // Now we wait for cooldownChargeThreshold before processing pixels - + // Calculate batch size (with fallback if function not yet defined) let batchSize; try { @@ -3847,47 +4002,47 @@ function getText(key, params) { } catch (e) { batchSize = state.paintingSpeed || 5; } - + // Ensure batchSize is valid and not zero batchSize = Math.max(1, Math.min(batchSize, remainingPixels)); const batchesNeeded = Math.ceil(remainingPixels / batchSize); - + // Calculate time accounting for cooldown threshold behavior with safety checks let estimatedMs = 0; if (remainingPixels > 0 && Number.isFinite(state.cooldown) && state.cooldown > 0) { // Current charges available check const currentCharges = Math.max(0, state.displayCharges); const thresholdCharges = Math.max(1, state.cooldownChargeThreshold || 1); - + if (currentCharges < thresholdCharges) { // Need to wait for charges to reach threshold first const chargesNeeded = thresholdCharges - currentCharges; estimatedMs += chargesNeeded * state.cooldown; } - + // Calculate painting time more accurately // Each batch uses thresholdCharges, so calculate how many cycles we need const pixelsPerCycle = Math.min(batchSize, thresholdCharges); const cyclesNeeded = Math.ceil(remainingPixels / pixelsPerCycle); - + // Time between cycles (waiting for charges to regenerate) if (cyclesNeeded > 1) { const regenerationTime = (cyclesNeeded - 1) * thresholdCharges * state.cooldown; estimatedMs += regenerationTime; } - + // Add painting speed delay if enabled if (CONFIG.PAINTING_SPEED_ENABLED && state.paintingSpeed > 0) { const paintingDelay = remainingPixels * (1000 / state.paintingSpeed); estimatedMs += paintingDelay; } } - + // Safety check to prevent infinity or invalid values if (!Number.isFinite(estimatedMs) || estimatedMs < 0) { estimatedMs = 0; } - + state.estimatedTime = estimatedMs; progressBar.style.width = `${progress}%`; @@ -5020,7 +5175,7 @@ function getText(key, params) { // Keep state.imageData.processor as the original-based source; painting uses paletted pixels already stored - updateStats(); + await updateStats(); updateUI('resizeSuccess', 'success', { width: newWidth, height: newHeight, @@ -5122,7 +5277,7 @@ function getText(key, params) { state.availableColors = availableColors; state.colorsChecked = true; updateUI('colorsFound', 'success', { count: availableColors.length }); - updateStats(); + await updateStats(); selectPosBtn.disabled = false; // Only enable resize button if image is also loaded if (state.imageLoaded) { @@ -5201,7 +5356,7 @@ function getText(key, params) { startBtn.disabled = false; } - updateStats(); + await updateStats(); updateDataButtons(); updateUI('imageLoaded', 'success', { count: totalValidPixels }); } catch { @@ -5210,6 +5365,179 @@ function getText(key, params) { }); } + // Load Extracted button event listener - for loading Art-Extractor JSON files + const loadExtractedBtn = document.getElementById('loadExtractedBtn'); + if (loadExtractedBtn) { + loadExtractedBtn.addEventListener('click', async () => { + try { + updateUI('loadingImage', 'default'); + const fileData = await Utils.loadExtractedFileData(); + + if (!fileData) { + updateUI('ready', 'default'); + return; + } + + console.log('πŸ“ Loading extracted artwork from Art-Extractor...'); + console.log('πŸ” [DEBUG] Loaded file data structure:', { + hasState: !!fileData.state, + hasImageData: !!fileData.imageData, + topLevelKeys: Object.keys(fileData), + stateKeys: fileData.state ? Object.keys(fileData.state) : 'N/A', + imageDataKeys: fileData.imageData ? Object.keys(fileData.imageData) : 'N/A', + fileDataType: typeof fileData, + stateType: typeof fileData.state, + imageDataType: typeof fileData.imageData + }); + + // Validate the data structure before restoring + if (!fileData || typeof fileData !== 'object') { + throw new Error('Invalid file format: File data is not a valid object'); + } + + if (!fileData.state || typeof fileData.state !== 'object') { + console.error('❌ State validation failed. FileData:', fileData); + throw new Error('Invalid file format: Missing or invalid state object. Please ensure you exported from Art-Extractor correctly.'); + } + + if (!fileData.imageData || typeof fileData.imageData !== 'object') { + console.error('❌ ImageData validation failed. FileData:', fileData); + throw new Error('Invalid file format: Missing or invalid imageData object. Please ensure you completed the area scan in Art-Extractor.'); + } + + if (!fileData.imageData.pixels || !Array.isArray(fileData.imageData.pixels) || fileData.imageData.pixels.length === 0) { + console.error('❌ Pixel data validation failed. ImageData:', fileData.imageData); + throw new Error('Invalid file format: No pixel data found. Please scan an area in Art-Extractor before exporting.'); + } + + // Ensure critical fields are present for Art-Extractor compatibility + if (!fileData.state.availableColors) { + console.warn('⚠️ No availableColors in file, using defaults'); + fileData.state.availableColors = []; + } + + // Use the existing restoreProgress function but with special handling + const restoreSuccess = await Utils.restoreProgress(fileData); + + if (!restoreSuccess) { + throw new Error('Failed to restore progress data'); + } + + // After loading, we need to enter position selection mode like when uploading images + if (state.imageLoaded) { + console.log('🎯 Extracted artwork loaded, entering position selection mode...'); + + // Clear position data since Art-Extractor exports may have null positions for manual placement + state.startPosition = null; + state.region = null; + state.selectingPosition = true; + + // For extracted images, ensure overlay is enabled but don't set position yet + try { + if (overlayManager && state.imageData) { + // Create image bitmap from the restored image data + const canvas = new OffscreenCanvas(state.imageData.width, state.imageData.height); + const ctx = canvas.getContext('2d'); + const imageData = new ImageData( + new Uint8ClampedArray(state.imageData.pixels), + state.imageData.width, + state.imageData.height + ); + ctx.putImageData(imageData, 0, 0); + const imageBitmap = await canvas.transferToImageBitmap(); + + await overlayManager.setImage(imageBitmap); + overlayManager.enable(); + console.log('βœ… Overlay enabled for extracted artwork'); + } + } catch (overlayError) { + console.warn('⚠️ Could not set overlay for extracted artwork:', overlayError); + } + + // For extracted images, don't try to set overlay position until user selects one + // The overlay should already be enabled from the restoreProgress function + + // Update UI to show position selection needed + Utils.showAlert('Extracted artwork loaded! Click on the canvas to select where to place it.', 'success'); + updateUI('ready', 'default'); // Use 'ready' instead of 'waitingPosition' to avoid translation issues + + // Enable relevant buttons + selectPosBtn.disabled = false; + if (state.colorsChecked) { + resizeBtn.disabled = false; + } + + // Set up position selection like the regular selectPosBtn + const tempFetch = async (url, options) => { + if ( + typeof url === 'string' && + url.includes('/s0/pixel/') && + options && + options.method === 'POST' + ) { + let coords; + try { + const body = JSON.parse(options.body); + coords = body.coords; + } catch (e) { + return window.originalFetch(url, options); + } + + const tileMatches = url.match(/\/s0\/pixel\/(\-?\d+)\/(\-?\d+)/); + if (tileMatches && coords && coords.length >= 2) { + const tileX = parseInt(tileMatches[1]); + const tileY = parseInt(tileMatches[2]); + const pixelX = coords[0]; + const pixelY = coords[1]; + + state.startPosition = { x: pixelX, y: pixelY }; + state.region = { x: tileX, y: tileY }; + state.selectingPosition = false; + + console.log('🎯 Position selected for extracted artwork:', { + startPosition: state.startPosition, + region: state.region + }); + + // Restore original fetch + window.fetch = window.originalFetch; + + // Update overlay position with validation + try { + if (state.startPosition && state.region && overlayManager) { + await overlayManager.setPosition(state.startPosition, state.region); + console.log('βœ… Overlay position updated successfully'); + } else { + console.warn('⚠️ Cannot set overlay position: missing startPosition or region'); + } + } catch (positionError) { + console.error('❌ Failed to set overlay position:', positionError); + } + + startBtn.disabled = false; + selectPosBtn.textContent = Utils.t('selectPosition'); + updateUI('ready', 'success'); + Utils.showAlert('Position selected! Ready to start painting.', 'success'); + } + } + return window.originalFetch(url, options); + }; + + // Store original fetch and replace with position capture + if (!window.originalFetch) { + window.originalFetch = window.fetch; + } + window.fetch = tempFetch; + } + + } catch (error) { + console.error('❌ Failed to load extracted artwork:', error); + Utils.showAlert(`Failed to load extracted artwork: ${error.message}`, 'error'); + updateUI('ready', 'default'); + } + }); + } + if (resizeBtn) { resizeBtn.addEventListener('click', () => { if (state.imageLoaded && state.imageData.processor && state.colorsChecked) { @@ -5260,7 +5588,17 @@ function getText(key, params) { }; state.lastPosition = { x: 0, y: 0 }; - await overlayManager.setPosition(state.startPosition, state.region); + // Update overlay position with validation + try { + if (state.startPosition && state.region && overlayManager) { + await overlayManager.setPosition(state.startPosition, state.region); + console.log('βœ… Regular overlay position updated successfully'); + } else { + console.warn('⚠️ Cannot set regular overlay position: missing startPosition or region'); + } + } catch (positionError) { + console.error('❌ Failed to set regular overlay position:', positionError); + } if (state.imageLoaded) { startBtn.disabled = false; @@ -5307,7 +5645,7 @@ function getText(key, params) { const savedPaintedPixels = state.paintedPixels; // Store original value state.paintedPixels = 0; console.log(`πŸ”„ First start this session - reset progress counter for accurate tracking (was: ${savedPaintedPixels})`); - updateStats(); // Update UI to show 0 progress + await updateStats(); // Update UI to show 0 progress } else { console.log('πŸ”„ Continuing session - pre-filtering already done, keeping current progress'); } @@ -5340,6 +5678,8 @@ function getText(key, params) { } else { startBtn.disabled = true; uploadBtn.disabled = false; + const loadExtractedBtn = document.getElementById('loadExtractedBtn'); + if (loadExtractedBtn) loadExtractedBtn.disabled = false; selectPosBtn.disabled = false; resizeBtn.disabled = false; } @@ -5604,7 +5944,7 @@ function getText(key, params) { if (startFromX > 0 || startFromY > 0) { console.log(`πŸ”„ Filtering coordinates to resume from position (${startFromX}, ${startFromY})`); let startIndex = -1; - + // Find the starting position in the coordinate list for (let i = 0; i < coords.length; i++) { const [x, y] = coords[i]; @@ -5613,7 +5953,7 @@ function getText(key, params) { break; } } - + if (startIndex >= 0) { // Resume from the found position (skip all previous coordinates) const filteredCoords = coords.slice(startIndex); @@ -5644,18 +5984,18 @@ function getText(key, params) { state.paintedPixels++; Utils.markPixelPainted(p.x, p.y, pixelBatch.regionX, pixelBatch.regionY); }); - + // IMPORTANT: Decrement charges locally to match Acc-Switch.js behavior state.displayCharges = Math.max(0, state.displayCharges - batchSize); state.preciseCurrentCharges = Math.max(0, state.preciseCurrentCharges - batchSize); - + state.fullChargeData = { ...state.fullChargeData, spentSinceShot: state.fullChargeData.spentSinceShot + batchSize, }; - updateStats(); + await updateStats(); // Update account list with new charges - updateCurrentAccountInList(); + await updateCurrentAccountInList(); // Progress tracking removed from UI to reduce visual clutter Utils.performSmartSave(); @@ -5676,38 +6016,38 @@ function getText(key, params) { async function processImage() { console.log('πŸš€ Starting auto-swap enabled painting workflow'); - + try { // Main painting cycle - repeats until image complete or stopped while (!state.stopFlag) { console.log('πŸ“‹ Phase 1: Starting painting session'); const paintingResult = await executePaintingSession(); - + if (paintingResult === 'completed') { console.log('πŸŽ‰ Image painting completed!'); break; } - + if (paintingResult === 'stopped') { console.log('⏹️ Painting stopped by user'); break; } - + if (paintingResult === 'charges_depleted') { if (!CONFIG.autoSwap) { // Original workflow: cooldown period console.log('⏱️ Phase 2: Entering cooldown period (auto-swap disabled)'); const cooldownResult = await executeCooldownPeriod(); - + if (cooldownResult === 'stopped') { console.log('⏹️ Cooldown stopped by user'); break; } - + // Phase 3: Regenerate token for next painting session console.log('πŸ”‘ Phase 3: Regenerating token for next session'); const tokenResult = await regenerateTokenForNewSession(); - + if (!tokenResult) { console.log('❌ Failed to regenerate token, stopping'); state.stopFlag = true; @@ -5716,16 +6056,16 @@ function getText(key, params) { } else { // Auto-swap workflow: simplified logic console.log('πŸ”„ Auto-swap enabled: checking account switching options'); - + // Retrieve fresh accounts list each time to avoid stale data const accounts = JSON.parse(localStorage.getItem("accounts")) || []; console.log(`πŸ“Š Retrieved ${accounts.length} accounts from localStorage`); - + if (accounts.length <= 1) { console.log('πŸ“‹ Only one account available, using standard cooldown'); const cooldownResult = await executeCooldownPeriod(); if (cooldownResult === 'stopped') break; - + const tokenResult = await regenerateTokenForNewSession(); if (!tokenResult) { state.stopFlag = true; @@ -5733,15 +6073,16 @@ function getText(key, params) { } } else { // Debug current state - console.log(`πŸ“Š Account Status - Current Index: ${state.accountIndex}, Total: ${accounts.length}`); - - // Check if we're at the last account in the cycle - const isLastAccount = state.accountIndex >= accounts.length - 1; - console.log(`πŸ” Is last account? ${isLastAccount} (index ${state.accountIndex} of ${accounts.length - 1})`); - + console.log(`πŸ“Š Account Status - Current ID: ${state.originalAccountOrder[state.currentActiveIndex]?.orderId}, Total IDs: ${state.originalAccountOrder.length}`); + + // Check if we're at the last ID in the sequence + const isLastAccount = state.currentActiveIndex >= state.originalAccountOrder.length - 1; + console.log(`πŸ” Is last ID? ${isLastAccount} (ID ${state.originalAccountOrder[state.currentActiveIndex]?.orderId} of ${state.originalAccountOrder.length})`); + if (!isLastAccount) { - // Switch to next account immediately (no cooldown) - console.log(`πŸ”„ Switching to next account (${state.accountIndex + 1}/${accounts.length})`); + // Switch to next ID immediately (no cooldown) + const nextId = state.originalAccountOrder[state.currentActiveIndex + 1]?.orderId; + console.log(`πŸ”„ Switching to next ID ${nextId} (${state.currentActiveIndex + 2}/${state.originalAccountOrder.length})`); const switchResult = await switchToNextAccount(accounts); if (!switchResult) { console.log('❌ Account switch failed, stopping'); @@ -5751,32 +6092,50 @@ function getText(key, params) { // Continue painting with new account immediately continue; } else { - // Last account reached - use cooldown then switch to first account + // Last account reached - use cooldown then switch to ID 1 (first account) console.log('⏱️ Last account reached, entering cooldown period'); - console.log(`πŸ“Š Current account index: ${state.accountIndex}, Total accounts: ${accounts.length}`); - + console.log(`πŸ“Š Current ID: ${state.originalAccountOrder[state.currentActiveIndex]?.orderId}, Last ID: ${state.originalAccountOrder[state.originalAccountOrder.length - 1]?.orderId}`); + const cooldownResult = await executeCooldownPeriod(); if (cooldownResult === 'stopped') break; + + // After cooldown, switch to ID 1 (first account in order) + console.log('πŸ” Cooldown complete, switching to ID 1 (first account)'); + console.log(`πŸ”„ Before switch - Current ID: ${state.originalAccountOrder[state.currentActiveIndex]?.orderId}, Target: ID 1`); + + const firstAccountInfo = state.originalAccountOrder[0]; // ID 1 is always at index 0 + const firstAccountToken = firstAccountInfo?.token; - // After cooldown, switch to first account (index 0) - console.log('πŸ” Cooldown complete, switching to first account'); - console.log(`πŸ”„ Before switch - Current index: ${state.accountIndex}, Target: 0`); - - const switchResult = await switchToSpecificAccount(accounts[0], 0); - if (!switchResult) { - console.log('❌ Switch to first account failed, stopping'); + if (!firstAccountToken) { + console.log('❌ First account (ID 1) token not found, stopping'); state.stopFlag = true; break; } + + // Reset to first position (ID 1) + state.currentActiveIndex = 0; - console.log(`βœ… Successfully switched to first account. New index: ${state.accountIndex}`); + // Update accountIndex to match the position in accounts array + const accountsIndex = accounts.findIndex(token => token === firstAccountToken); + if (accountsIndex !== -1) { + state.accountIndex = accountsIndex; + } + + const switchResult = await switchToSpecificAccount(firstAccountToken, 0); + if (!switchResult) { + console.log('❌ Switch to ID 1 failed, stopping'); + state.stopFlag = true; + break; + } + + console.log(`βœ… Successfully switched to ID 1 (${firstAccountInfo.displayName}). Cycle restarted.`); // Continue painting with first account continue; } } } } - + console.log('πŸ”„ Cycle complete, starting next painting session'); } } finally { @@ -5793,7 +6152,7 @@ function getText(key, params) { // Check if we're working with restored data by looking for existing availableColors const isRestoredData = state.availableColors && state.availableColors.length > 0 && state.colorsChecked; - + if (!isRestoredData) { // Wait for original tiles to load if needed const tilesReady = await overlayManager.waitForTiles( @@ -5827,7 +6186,7 @@ function getText(key, params) { state.displayCharges = Math.floor(initialChargeCheck.charges); state.preciseCurrentCharges = initialChargeCheck.charges; state.cooldown = initialChargeCheck.cooldown; - + await updateStats(); if (state.displayCharges <= 0) { console.log('⚑ No charges available, skipping painting session'); return 'charges_depleted'; @@ -5852,26 +6211,27 @@ function getText(key, params) { // OPTIMIZATION: Pre-filter already painted pixels (happens only once per session) let eligibleCoords = []; let alreadyPaintedCount = 0; - + // Log resume information if applicable if (state.lastPosition.x > 0 || state.lastPosition.y > 0) { console.log(`πŸ”„ Resuming painting from position (${state.lastPosition.x}, ${state.lastPosition.y})`); console.log(`πŸ“Š Current progress: ${state.paintedPixels} pixels painted`); } - + if (!state.preFilteringDone) { console.log('πŸ” Pre-filtering already painted pixels (one-time detection for this session)...'); - + for (const [x, y] of coords) { const targetPixelInfo = checkPixelEligibility(x, y); - + if (!targetPixelInfo.eligible) { if (targetPixelInfo.reason !== 'alreadyPainted') { skippedPixels[targetPixelInfo.reason]++; + } continue; } - + // Check if already painted (only once per session) let absX = startX + x; let absY = startY + y; @@ -5879,7 +6239,7 @@ function getText(key, params) { let adderY = Math.floor(absY / 1000); let pixelX = absX % 1000; let pixelY = absY % 1000; - + try { const tilePixelRGBA = await overlayManager.getTilePixelColor( regionX + adderX, @@ -5906,20 +6266,20 @@ function getText(key, params) { } catch (e) { // If we can't check, include the pixel (better to attempt than skip) } - + // Add eligible unpainted pixel to list eligibleCoords.push([x, y, targetPixelInfo]); } - + // Mark pre-filtering as done for this session state.preFilteringDone = true; - + // Log pre-filtering results if (alreadyPaintedCount > 0) { console.log(`βœ“ Pre-filter complete: ${alreadyPaintedCount} already painted pixels detected and added to progress`); console.log('ℹ️ This detection will not happen again until a new image/save is loaded'); // Update UI to reflect the new progress immediately - updateStats(); + await updateStats(); } skippedPixels.alreadyPainted = alreadyPaintedCount; } else { @@ -5927,14 +6287,14 @@ function getText(key, params) { console.log('πŸ” Using existing pre-filter results (already done this session)'); for (const [x, y] of coords) { const targetPixelInfo = checkPixelEligibility(x, y); - + if (!targetPixelInfo.eligible) { if (targetPixelInfo.reason !== 'alreadyPainted') { skippedPixels[targetPixelInfo.reason]++; } continue; } - + // Only include pixels that haven't been marked as painted yet if (!Utils.isPixelPainted(x, y)) { eligibleCoords.push([x, y, targetPixelInfo]); @@ -5956,13 +6316,21 @@ function getText(key, params) { // Check if we have charges left (local count, no API call) if (state.displayCharges <= 0) { - console.log('⚑ No charges left (local count), ending painting session'); - if (pixelBatch && pixelBatch.pixels.length > 0) { - console.log(`🎯 Sending final batch with ${pixelBatch.pixels.length} pixels`); - await flushPixelBatch(pixelBatch); + // console.log("Try to buy paint charges"); + // await purchase("paint_charges"); + await updateStats(); + if (state.displayCharges <= 0) { + console.log('⚑ No charges left (local count), ending painting session'); + if (pixelBatch && pixelBatch.pixels.length > 0) { + console.log(`🎯 Sending final batch with ${pixelBatch.pixels.length} pixels`); + await flushPixelBatch(pixelBatch); + } + state.lastPosition = { x, y }; + return 'charges_depleted'; } - state.lastPosition = { x, y }; - return 'charges_depleted'; + // else { + // console.log(`πŸ”‹ Charges after purchase: ${state.displayCharges}, continuing painting`); + // } } let absX = startX + x; @@ -5989,7 +6357,7 @@ function getText(key, params) { state.stopFlag = true; return 'stopped'; } - updateStats(); + await updateStats(); } pixelBatch = { @@ -6019,7 +6387,7 @@ function getText(key, params) { return 'stopped'; } pixelBatch.pixels = []; - updateStats(); + await updateStats(); } } @@ -6058,7 +6426,7 @@ function getText(key, params) { while (!state.stopFlag) { chargeCheckCount++; - + const { charges, cooldown } = await WPlaceService.getCharges(); state.displayCharges = Math.floor(charges); state.preciseCurrentCharges = charges; @@ -6067,7 +6435,7 @@ function getText(key, params) { if (state.displayCharges >= state.cooldownChargeThreshold) { console.log(`βœ… Cooldown target reached: ${state.displayCharges}/${state.cooldownChargeThreshold}`); NotificationManager.maybeNotifyChargesReached(true); - updateStats(); + await updateStats(); return 'target_reached'; } @@ -6081,7 +6449,7 @@ function getText(key, params) { // Smart delay calculation to reduce API calls const chargesNeeded = state.cooldownChargeThreshold - state.displayCharges; const estimatedWaitTime = chargesNeeded * state.cooldown; - + // Use longer delays during cooldown to prevent rate limiting let delayTime; if (chargeCheckCount < 3) { @@ -6114,16 +6482,16 @@ function getText(key, params) { // Phase 3: Regenerate token for new painting session async function regenerateTokenForNewSession() { console.log('πŸ”‘ Regenerating token for new painting session'); - + try { // Force regenerate token for new session await ensureToken(true); // forceRefresh = true - + if (!getTurnstileToken()) { console.error('❌ Failed to generate token for new session'); return false; } - + console.log('βœ… Token regenerated successfully for new session'); return true; } catch (error) { @@ -6135,7 +6503,7 @@ function getText(key, params) { // Finalize painting process cleanup async function finalizePaintingProcess() { console.log('🧹 Finalizing painting process'); - + if (window._chargesInterval) { clearInterval(window._chargesInterval); window._chargesInterval = null; @@ -6339,12 +6707,12 @@ function getText(key, params) { data = await res.json(); } catch (_) { } console.error('❌ 403 Forbidden. Token invalid during painting - regeneration allowed.'); - + // 403 errors during painting allow token regeneration per workflow requirements console.log('οΏ½ Token invalid (403) during painting - regenerating token as allowed by workflow'); setTurnstileToken(null); createTokenPromise(); - + // Attempt to regenerate token immediately const newToken = await ensureToken(true); if (newToken) { @@ -6655,6 +7023,14 @@ function getText(key, params) { console.log('βœ… Upload Image button enabled after initial setup'); } + // Enable Load Extracted button at the same time as Upload button + const loadExtractedBtn = document.getElementById('loadExtractedBtn'); + if (loadExtractedBtn) { + loadExtractedBtn.disabled = false; + loadExtractedBtn.title = ''; + console.log('βœ… Load Extracted button enabled after initial setup'); + } + // Show a notification that file operations are now available Utils.showAlert(Utils.t('fileOperationsAvailable'), 'success'); } @@ -6896,30 +7272,30 @@ function getText(key, params) { console.error("An error occurred during the purchase:", e); } } - function swapAccountTrigger(token) { + async function swapAccountTrigger(token) { // STRICT GUARD: Only allow account switching during active painting sessions OR controlled refresh if (!state.running && !state.isFetchingAllAccounts) { console.warn('πŸ”’ Account switching blocked - only allowed during active painting or controlled refresh'); console.warn('πŸ”’ Current state.running:', state.running, 'state.isFetchingAllAccounts:', state.isFetchingAllAccounts); return false; } - + localStorage.removeItem("lp"); if (!token) { console.error('❌ Cannot swap account: token is null or undefined'); return false; } - + console.log(`πŸ”„ Triggering account swap with token: ${token.substring(0, 20)}...`); console.log('πŸ“€ Sending setCookie message to extension...'); - + try { window.postMessage({ source: 'my-userscript', type: 'setCookie', value: token }, '*'); - + console.log('βœ… setCookie message sent successfully'); return true; } catch (error) { @@ -6965,7 +7341,7 @@ function getText(key, params) { Utils.showAlert("Already fetching account details.", "warning"); return; } - + state.isFetchingAllAccounts = true; const refreshBtn = document.getElementById('refreshAllAccountsBtn'); @@ -7007,16 +7383,16 @@ function getText(key, params) { // Temporarily disable the strict guard for controlled refresh operation const originalRunningState = state.running; state.running = true; // Temporarily enable to allow controlled switching - + console.log('πŸ”“ Temporarily enabling controlled account switching for refresh'); for (let i = 0; i < accountsTokens.length; i++) { const token = accountsTokens[i]; - + console.log(`πŸ“‹ Fetching details for account ${i + 1}/${accountsTokens.length}`); - + // Controlled account switch for data fetching - const swapResult = swapAccountTrigger(token); + const swapResult = await swapAccountTrigger(token); if (!swapResult) { console.warn(`⚠️ Failed to trigger swap for account ${i + 1}`); continue; @@ -7026,7 +7402,7 @@ function getText(key, params) { let retries = 0; let accountData = null; while (retries < 8 && !accountData) { - await Utils.sleep(1000); + // await Utils.sleep(1000); try { const fetchedInfo = await WPlaceService.fetchCheck(); const chargesInfo = await WPlaceService.getCharges(); @@ -7047,14 +7423,14 @@ function getText(key, params) { if (accountData) { const actualName = accountData.Username || accountData.name || `User${accountData.ID}`; const isCurrentAccount = accountData.ID === originalAccountData.id; - + state.allAccountsInfo.push({ ...accountData, token, displayName: actualName, isCurrent: isCurrentAccount }); - + console.log(`βœ… Fetched details for: ${actualName} (${accountData.Charges}/${accountData.Max} charges)`); } else { // Fallback for failed accounts @@ -7068,14 +7444,14 @@ function getText(key, params) { }); console.warn(`❌ Failed to fetch details for account ${i + 1}`); } - + renderAccountsList(); } // Restore original state and switch back to original account state.running = originalRunningState; console.log('πŸ”’ Restored original running state and switching back to original account'); - + } catch (error) { console.error("Error fetching all account details:", error); if (accountsListArea) accountsListArea.innerHTML = `
Error loading accounts.
`; @@ -7085,10 +7461,10 @@ function getText(key, params) { console.log('πŸ”„ Switching back to original account...'); const originalRunningState = state.running; state.running = true; // Temporarily enable for switch back - - swapAccountTrigger(originalToken); + + await swapAccountTrigger(originalToken); await Utils.sleep(2000); - + // Verify we're back on the original account try { const verifyData = await WPlaceService.getCharges(); @@ -7098,7 +7474,7 @@ function getText(key, params) { } catch (e) { console.warn('⚠️ Could not verify switch back to original account'); } - + state.running = originalRunningState; // Restore state } @@ -7113,12 +7489,54 @@ function getText(key, params) { } } - // Sort accounts to place current account first - state.allAccountsInfo.sort((a, b) => { - if (a.isCurrent && !b.isCurrent) return -1; - if (!a.isCurrent && b.isCurrent) return 1; - return 0; - }); + // Keep original account order - do NOT rearrange the array + // Instead, assign IDs based on current account being ID 1 + const currentAccountIndex = state.allAccountsInfo.findIndex(acc => acc.isCurrent); + + if (currentAccountIndex !== -1) { + console.log(`🎯 Current account found at array position ${currentAccountIndex}`); + + // Assign IDs: current account gets ID 1, others get sequential IDs + state.allAccountsInfo.forEach((acc, index) => { + if (acc.isCurrent) { + acc.orderId = 1; // Current account always gets ID 1 + } else { + // Other accounts get IDs based on their position relative to current + const relativePosition = index > currentAccountIndex ? + (index - currentAccountIndex + 1) : + (state.allAccountsInfo.length - currentAccountIndex + index + 1); + acc.orderId = relativePosition; + } + }); + + // Sort the display order by orderId for UI, but keep original array intact + const sortedForDisplay = [...state.allAccountsInfo].sort((a, b) => a.orderId - b.orderId); + + // Create the switching order array (ID 1 first, then sequential) + state.originalAccountOrder = sortedForDisplay; + + // Find the current account's position in the switching order (should be 0 since it's ID 1) + state.currentActiveIndex = 0; // Current account (ID 1) is always first in switching order + state.accountIndex = currentAccountIndex; // Keep track of original array position + + console.log(`πŸ“‹ Account IDs assigned:`); + state.allAccountsInfo.forEach((acc) => { + console.log(` Array pos ${state.allAccountsInfo.indexOf(acc)}: ID ${acc.orderId} - ${acc.displayName}${acc.isCurrent ? ' (CURRENT)' : ''}`); + }); + + console.log(`πŸ“‹ Switching order (by ID):`); + state.originalAccountOrder.forEach((acc, idx) => { + console.log(` Switch pos ${idx}: ID ${acc.orderId} - ${acc.displayName}${acc.isCurrent ? ' (CURRENT)' : ''}`); + }); + } else { + console.warn('⚠️ No current account found, using original order'); + state.allAccountsInfo.forEach((acc, index) => { + acc.orderId = index + 1; + }); + state.originalAccountOrder = [...state.allAccountsInfo]; + state.currentActiveIndex = 0; + state.accountIndex = 0; + } await updateStats(); renderAccountsList(); @@ -7128,18 +7546,22 @@ function getText(key, params) { refreshBtn.innerHTML = ''; refreshBtn.disabled = false; } - + console.log('βœ… Account list refresh completed with full details'); } } // Function to update current account charges in the account list - function updateCurrentAccountInList() { + async function updateCurrentAccountInList() { if (state.allAccountsInfo.length === 0) return; - + // Find current account in the list and update its charges const currentAccountInList = state.allAccountsInfo.find(acc => acc.isCurrent); if (currentAccountInList) { + const { charges, cooldown } = await WPlaceService.getCharges(); + state.displayCharges = Math.floor(charges); + state.preciseCurrentCharges = charges; + await updateStats(); currentAccountInList.Charges = Math.floor(state.displayCharges || state.preciseCurrentCharges || 0); currentAccountInList.Max = state.maxCharges; // Re-render the account list to show updated charges @@ -7150,37 +7572,40 @@ function getText(key, params) { // Function to update current account spotlight when switching during painting async function updateCurrentAccountSpotlight() { if (state.allAccountsInfo.length === 0) return; - + await Utils.sleep(500); // Wait a bit for the switch to take effect try { - // Get current account info const currentAccountData = await WPlaceService.getCharges(); - const currentAccountInfo = await WPlaceService.fetchCheck(); + console.log("Current account after switch:", currentAccountData); + console.log(`πŸ” Switched to account with ID: ${currentAccountData.id}`); // Update all accounts to not be current state.allAccountsInfo.forEach(acc => acc.isCurrent = false); - - // Find and mark the new current account + + // Find and mark the new current account in the original array const newCurrentAccount = state.allAccountsInfo.find(acc => acc.ID === currentAccountData.id); if (newCurrentAccount) { + const currentAccountInfo = await WPlaceService.fetchCheck(); newCurrentAccount.isCurrent = true; newCurrentAccount.Charges = Math.floor(currentAccountData.charges); newCurrentAccount.Max = Math.floor(currentAccountData.max); newCurrentAccount.Droplets = Math.floor(currentAccountData.droplets); newCurrentAccount.displayName = currentAccountInfo.Username || currentAccountInfo.name || newCurrentAccount.displayName; + + console.log(`🎯 Updated current account spotlight: ${newCurrentAccount.displayName} (ID ${newCurrentAccount.orderId})`); - console.log(`🎯 Updated current account spotlight: ${newCurrentAccount.displayName}`); + // Update the switching order array to reflect the current account change + const switchingOrderIndex = state.originalAccountOrder.findIndex(acc => acc.orderId === newCurrentAccount.orderId); + if (switchingOrderIndex !== -1) { + state.originalAccountOrder[switchingOrderIndex] = newCurrentAccount; + // DON'T update currentActiveIndex here - it's managed by switchToNextAccount() + console.log(`🎯 Updated account data at switching order index ${switchingOrderIndex} (ID ${newCurrentAccount.orderId})`); + console.log(`πŸ“Š Current state: activeIndex=${state.currentActiveIndex}, expectedIndex=${switchingOrderIndex}`); + } } - - // Sort to place current account first - state.allAccountsInfo.sort((a, b) => { - if (a.isCurrent && !b.isCurrent) return -1; - if (!a.isCurrent && b.isCurrent) return 1; - return 0; - }); - + // Re-render the account list to show new current account renderAccountsList(); - + } catch (error) { console.warn('⚠️ Failed to update account spotlight:', error); } @@ -7196,14 +7621,49 @@ function getText(key, params) { return; } - state.allAccountsInfo.forEach((info, index) => { + // Render accounts in switching order (sorted by orderId) + state.originalAccountOrder.forEach((info, switchIndex) => { const item = document.createElement('div'); - item.className = `wplace-account-item ${info.isCurrent ? 'is-current' : ''}`; + + // Determine if this is the current account or next in sequence + const isCurrentAccount = info.isCurrent; + const isNextInSequence = state.currentActiveIndex !== -1 && + ((state.currentActiveIndex + 1) % state.originalAccountOrder.length) === switchIndex; + + let itemClasses = 'wplace-account-item'; + if (isCurrentAccount) { + itemClasses += ' current'; + } else if (isNextInSequence) { + itemClasses += ' next-in-sequence'; + } + + item.className = itemClasses; - // Create ordering number element + // Create ordering number element with order ID const orderNumber = document.createElement('div'); orderNumber.className = 'wplace-account-number'; - orderNumber.textContent = index + 1; + orderNumber.textContent = info.orderId; // Always show the assigned order ID + + // Add visual indicator for current position in sequence + if (isCurrentAccount) { + orderNumber.style.background = '#2ecc71'; // Green color + orderNumber.style.color = 'white'; + orderNumber.style.boxShadow = '0 0 10px rgba(46, 204, 113, 0.8)'; + orderNumber.title = `Order ID ${info.orderId} - Currently active account`; + + // Add a subtle pulsing effect to the account number for extra visibility + orderNumber.style.animation = 'currentAccountNumberPulse 1.5s ease-in-out infinite alternate'; + } else if (isNextInSequence) { + orderNumber.style.background = '#f39c12'; // Orange color + orderNumber.style.color = 'white'; + orderNumber.style.boxShadow = '0 0 8px rgba(243, 156, 18, 0.6)'; + orderNumber.title = `Order ID ${info.orderId} - Next account in switching sequence`; + } else { + orderNumber.style.background = ''; // Reset to default + orderNumber.style.boxShadow = ''; // Remove any glow + orderNumber.title = `Order ID ${info.orderId}`; + orderNumber.style.animation = ''; // Remove any animation + } const displayName = info.displayName || `Account ${index + 1}`; @@ -7239,55 +7699,96 @@ function getText(key, params) { // Account switching helper functions async function switchToNextAccount(accounts) { - const previousIndex = state.accountIndex; - state.accountIndex = (state.accountIndex + 1) % accounts.length; + const previousActiveIndex = state.currentActiveIndex; - // Get account names for better logging - const prevAccountInfo = state.allAccountsInfo && state.allAccountsInfo[previousIndex] - ? state.allAccountsInfo[previousIndex].displayName - : `Account ${previousIndex + 1}`; - const nextAccountInfo = state.allAccountsInfo && state.allAccountsInfo[state.accountIndex] - ? state.allAccountsInfo[state.accountIndex].displayName - : `Account ${state.accountIndex + 1}`; + // Move to next account in strict ID order (next position in switching order) + const nextIndex = (state.currentActiveIndex + 1) % state.originalAccountOrder.length; - console.log(`πŸ”„ Switching from account ${previousIndex + 1} (${prevAccountInfo}) to account ${state.accountIndex + 1} (${nextAccountInfo}) (${state.accountIndex + 1}/${accounts.length})`); + // Get the account info from switching order + const nextAccountInfo = state.originalAccountOrder[nextIndex]; + const prevAccountInfo = state.originalAccountOrder[previousActiveIndex]; - const nextToken = accounts[state.accountIndex]; - console.log(`πŸ”‘ Next token: ${nextToken ? nextToken.substring(0, 20) + '...' : 'INVALID'}`); + // Strict validation: ensure ID 1 always goes to ID 2 + if (prevAccountInfo?.orderId === 1 && nextAccountInfo?.orderId !== 2) { + console.error(`❌ STRICT ORDER VIOLATION: ID 1 must switch to ID 2, but found ID ${nextAccountInfo?.orderId}`); + // Find ID 2 account + const id2Account = state.originalAccountOrder.find(acc => acc.orderId === 2); + if (id2Account) { + const id2Index = state.originalAccountOrder.indexOf(id2Account); + console.log(`πŸ”„ Correcting: Forcing switch from ID 1 to ID 2 (index ${id2Index})`); + state.currentActiveIndex = id2Index; + } else { + console.error(`❌ FATAL: No ID 2 account found in switching order!`); + return false; + } + } else { + state.currentActiveIndex = nextIndex; + } - if (!nextToken) { - console.warn('⚠️ Invalid token, skipping...'); + // Re-get account info after potential correction + const finalNextAccountInfo = state.originalAccountOrder[state.currentActiveIndex]; + + console.log(`πŸ”„ Switching from ID ${prevAccountInfo?.orderId} (${prevAccountInfo?.displayName || 'Unknown'}) to ID ${finalNextAccountInfo?.orderId} (${finalNextAccountInfo?.displayName || 'Unknown'})`); + console.log(`πŸ“ Index progression: ${previousActiveIndex} β†’ ${state.currentActiveIndex} (order: ${prevAccountInfo?.orderId} β†’ ${finalNextAccountInfo?.orderId})`); + + // Find the token for this account by matching the account in the original allAccountsInfo array + const originalAccountIndex = state.allAccountsInfo.findIndex(acc => acc.orderId === finalNextAccountInfo?.orderId); + if (originalAccountIndex === -1) { + console.warn(`⚠️ Could not find account with ID ${finalNextAccountInfo?.orderId} in original array`); return false; } - return await switchToSpecificAccount(nextToken, state.accountIndex); + const nextToken = finalNextAccountInfo?.token; + console.log(`πŸ”‘ Next token for ID ${finalNextAccountInfo?.orderId}: ${nextToken ? nextToken.substring(0, 20) + '...' : 'INVALID'}`); + console.log(`πŸ” Expected account: ${finalNextAccountInfo?.displayName} (ID ${finalNextAccountInfo?.orderId})`); + + if (!nextToken) { + console.warn(`⚠️ Invalid token for ID ${finalNextAccountInfo?.orderId}, skipping...`); + return false; + } + + // Update accountIndex to match the position in the accounts token array + const accountsIndex = accounts.findIndex(token => token === nextToken); + if (accountsIndex !== -1) { + state.accountIndex = accountsIndex; + console.log(`πŸ”— Found token at accounts array index: ${accountsIndex}`); + console.log(`πŸ” Token from accounts[${accountsIndex}]: ${accounts[accountsIndex].substring(0, 20)}...`); + } else { + console.warn(`⚠️ Token not found in accounts array for ID ${finalNextAccountInfo?.orderId}`); + console.warn(`πŸ” Available accounts tokens: ${accounts.map((t, i) => `[${i}] ${t.substring(0, 20)}...`).join(', ')}`); + return false; + } + + return await switchToSpecificAccount(nextToken, state.currentActiveIndex); } - async function switchToSpecificAccount(token, accountIndex) { - // Get account name for better logging - const accountInfo = state.allAccountsInfo && state.allAccountsInfo[accountIndex] - ? state.allAccountsInfo[accountIndex].displayName - : `Account ${accountIndex + 1}`; - - console.log(`πŸ”„ Attempting to switch to account at index ${accountIndex} (${accountInfo})`); + async function switchToSpecificAccount(token, originalOrderIndex) { + // Get account name and ID for better logging + const accountInfo = state.originalAccountOrder && state.originalAccountOrder[originalOrderIndex] + ? state.originalAccountOrder[originalOrderIndex] + : null; + const accountName = accountInfo?.displayName || `Account ${originalOrderIndex + 1}`; + const accountId = accountInfo?.orderId || (originalOrderIndex + 1); + + console.log(`πŸ”„ Attempting to switch to ID ${accountId} (${accountName})`); console.log(`πŸ”‘ Using token: ${token.substring(0, 20)}...`); - - swapAccountTrigger(token); - + + await swapAccountTrigger(token); + // Give more time for the initial swap to process (like Acc-Switch.js) - console.log('⏳ Waiting 5 seconds for initial swap processing...'); - await new Promise(resolve => setTimeout(resolve, 5000)); - + // console.log('⏳ Waiting 5 seconds for initial swap processing...'); + // await new Promise(resolve => setTimeout(resolve, 5000)); + let maxRetries = 20; // Match Acc-Switch.js retry count let retryCount = 0; let swapSuccess = false; - + while (!swapSuccess && retryCount < maxRetries) { console.log(`⏳ Waiting for account swap... (Attempt ${retryCount + 1}/${maxRetries})`); - + // Wait for a short period before checking (like Acc-Switch.js) await new Promise(resolve => setTimeout(resolve, 1000)); - + try { // Use the same verification method as Acc-Switch.js await fetchAccount(); @@ -7296,32 +7797,32 @@ function getText(key, params) { } catch (error) { console.warn('❌ Account swap not yet successful. Retrying...', error); retryCount++; - + // Re-trigger swap every 5 attempts if (retryCount % 5 === 0) { console.log('πŸ”„ Re-triggering account swap...'); - swapAccountTrigger(token); + await swapAccountTrigger(token); await new Promise(resolve => setTimeout(resolve, 2000)); } } } - + if (swapSuccess) { const { charges, cooldown } = await WPlaceService.getCharges(); state.displayCharges = Math.floor(charges); state.preciseCurrentCharges = charges; state.cooldown = cooldown; - state.accountIndex = accountIndex; + // Don't update state.accountIndex here as it should be managed by the calling function Utils.performSmartSave(); - updateStats(); - + await updateStats(); + // Update account list spotlight to show new current account await updateCurrentAccountSpotlight(); - - console.log(`βœ… Successfully switched to account ${accountIndex + 1} (${accountInfo}) with ${Math.floor(charges)} charges`); + + console.log(`βœ… Successfully switched to ID ${accountId} (${accountName}) with ${Math.floor(charges)} charges`); return true; } else { - console.error(`❌ Failed to swap to account ${accountIndex + 1} (${accountInfo}) after multiple retries.`); + console.error(`❌ Failed to swap to ID ${accountId} (${accountName}) after multiple retries.`); return false; } } @@ -7331,7 +7832,7 @@ function getText(key, params) { // Wait for all global managers to be available let attempts = 0; const maxAttempts = 50; // 5 seconds max wait - + while (attempts < maxAttempts) { const managers = { utilsManager: window.globalUtilsManager, @@ -7339,21 +7840,21 @@ function getText(key, params) { overlayManager: window.globalOverlayManager, tokenManager: window.globalTokenManager }; - + const missingManagers = Object.entries(managers) .filter(([name, manager]) => !manager) .map(([name]) => name); - + if (missingManagers.length === 0) { console.log('βœ… All global managers are available, initializing UI...'); break; } - + console.log(`⏳ Waiting for managers: ${missingManagers.join(', ')} (attempt ${attempts + 1}/${maxAttempts})`); await new Promise(resolve => setTimeout(resolve, 100)); attempts++; } - + if (attempts >= maxAttempts) { console.warn('⚠️ Some global managers not available after waiting, proceeding anyway...'); console.log('Available managers:', { @@ -7363,10 +7864,10 @@ function getText(key, params) { tokenManager: !!window.globalTokenManager }); } - + return createUI(); } - + waitForDependenciesAndInitialize().then(() => { // Generate token automatically after UI is ready setTimeout(initializeTokenGenerator, 1000); diff --git a/Extension/scripts/Auto-Repair.js b/Extension/scripts/Auto-Repair.js index a4ef4ee..1b4e0f2 100644 --- a/Extension/scripts/Auto-Repair.js +++ b/Extension/scripts/Auto-Repair.js @@ -364,10 +364,14 @@ // NUEVA LΓ“GICA DE INYECCIΓ“N - ACTUALIZADA DESDE new.txt function inject(callback) { - const script = document.createElement('script'); - script.textContent = `(${callback})();`; - document.documentElement?.appendChild(script); - script.remove(); + try { + const script = document.createElement('script'); + script.textContent = `(${callback})();`; + document.documentElement?.appendChild(script); + script.remove(); + } catch (error) { + console.error('❌ Injection error:', error); + } } inject(() => { @@ -593,7 +597,7 @@ } } catch { } } - console.error(`❌ Could not find Pawtect chunk: `, error); + console.error(`❌ Could not find Pawtect chunk with string: ${str}`); } // Audio notification system @@ -1788,11 +1792,10 @@ currentRgb: currentPixel.slice(0, 3), isDamagedTransparent: false }); - wrongColorPixelsDetected++; + wrongColorPixelsDetected++; - if (!state.autonomousMode || wrongColorPixelsDetected <= 10) { - Utils.addDebugLog(`Color damage at (${x},${y}): expected color ${targetColor.id}, found color ${currentColor.id}`, 'warning'); - } + if (!state.autonomousMode || wrongColorPixelsDetected <= 10) { + Utils.addDebugLog(`Color damage at (${x},${y}): expected color ${targetColor.id}, found color ${currentColor.id}`, 'warning'); } } diff --git a/Extension/themes/acrylic.css b/Extension/themes/acrylic.css index c096e97..b80032b 100644 --- a/Extension/themes/acrylic.css +++ b/Extension/themes/acrylic.css @@ -15,8 +15,7 @@ border-radius: var(--wplace-radius); box-shadow: none; backdrop-filter: var(--wplace-backdrop); - border: var(--wplace-border-width) var(--wplace-border-style) - var(--wplace-border-color); + border: var(--wplace-border-width) var(--wplace-border-style) var(--wplace-border-color); min-width: 250px; } @@ -41,8 +40,7 @@ border-radius: var(--wplace-radius); box-shadow: none; backdrop-filter: var(--wplace-backdrop); - border: var(--wplace-border-width) var(--wplace-border-style) - var(--wplace-border-color); + border: var(--wplace-border-width) var(--wplace-border-style) var(--wplace-border-color); min-width: 300px; } @@ -54,7 +52,7 @@ .wplace-theme-acrylic .wplace-content { display: flex; flex-direction: column; - gap: 10px; + /* gap: 10px; */ padding: 10px; } @@ -71,8 +69,7 @@ border-radius: var(--wplace-radius); box-shadow: none; backdrop-filter: var(--wplace-backdrop); - border: var(--wplace-border-width) var(--wplace-border-style) - var(--wplace-border-color); + border: var(--wplace-border-width) var(--wplace-border-style) var(--wplace-border-color); } .wplace-theme-acrylic .wplace-stats { @@ -91,8 +88,7 @@ border-radius: var(--wplace-radius) !important; box-shadow: none !important; backdrop-filter: var(--wplace-backdrop) !important; - border: var(--wplace-border-width) var(--wplace-border-style) - var(--wplace-border-color) !important; + border: var(--wplace-border-width) var(--wplace-border-style) var(--wplace-border-color) !important; max-width: 400px !important; } @@ -630,10 +626,10 @@ /* Account switching UI styles */ .wplace-theme-acrylic .wplace-section { - margin: 15px 0; + margin: 5px; border-radius: var(--wplace-radius); background: rgba(255, 255, 255, 0.08); - padding: 15px; + padding: 5px; border: 1px solid rgba(255, 255, 255, 0.15); backdrop-filter: var(--wplace-backdrop); } @@ -644,7 +640,7 @@ gap: 8px; font-weight: 600; color: var(--wplace-text); - margin-bottom: 10px; + margin: 2px; font-size: 14px; } @@ -688,55 +684,57 @@ box-shadow: 0 2px 8px rgba(0, 0, 0, 0.3); } -.wplace-theme-acrylic .wplace-switch input:checked + .wplace-slider-round { +.wplace-theme-acrylic .wplace-switch input:checked+.wplace-slider-round { background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); } -.wplace-theme-acrylic .wplace-switch input:checked + .wplace-slider-round:before { +.wplace-theme-acrylic .wplace-switch input:checked+.wplace-slider-round:before { transform: translateX(26px); } /* Account list container */ .wplace-theme-acrylic .accounts-list-container { - max-height: 200px; + max-height: 500px; overflow-y: auto; - margin-top: 10px; + /* margin-top: 10px; */ padding: 0 6px; } /* Individual account item */ .wplace-theme-acrylic .wplace-account-item { - display: flex; - justify-content: space-between; - align-items: center; - padding: 6px 8px; - margin: 4px 0; - background: rgba(255, 255, 255, 0.05); - border-radius: 8px; - border: 1px solid rgba(255, 255, 255, 0.15); - transition: all 0.3s ease; - backdrop-filter: blur(10px); - position: relative; + display: flex; + justify-content: space-between; + align-items: center; + padding: 6px 8px; + margin: 4px 0; + background: rgba(255, 255, 255, 0.05); + border-radius: 8px; + border: 1px solid rgba(255, 255, 255, 0.15); + transition: all 0.3s ease; + backdrop-filter: blur(10px); + position: relative; } /* Account ordering number */ .wplace-theme-acrylic .wplace-account-number { - position: absolute; - top: 3px; - left: 3px; - width: 16px; - height: 16px; - background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); - color: white; - border-radius: 50%; - display: flex; - align-items: center; - justify-content: center; - font-size: 9px; - font-weight: bold; - z-index: 1; - box-shadow: 0 2px 4px rgba(0, 0, 0, 0.3); -}.wplace-theme-acrylic .wplace-account-item:hover { + position: absolute; + top: 3px; + left: 3px; + width: 16px; + height: 16px; + background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); + color: white; + border-radius: 50%; + display: flex; + align-items: center; + justify-content: center; + font-size: 9px; + font-weight: bold; + z-index: 1; + box-shadow: 0 2px 4px rgba(0, 0, 0, 0.3); +} + +.wplace-theme-acrylic .wplace-account-item:hover { background: rgba(255, 255, 255, 0.12); border-color: rgba(255, 255, 255, 0.3); transform: translateY(-1px); @@ -751,9 +749,11 @@ /* Account details */ .wplace-theme-acrylic .wplace-account-details { - flex: 1; - margin-left: 25px; -}.wplace-theme-acrylic .wplace-account-name { + flex: 1; + margin-left: 25px; +} + +.wplace-theme-acrylic .wplace-account-name { font-weight: 600; color: var(--wplace-text); font-size: 13px; @@ -779,4 +779,4 @@ .wplace-theme-acrylic .wplace-account-stats .fas.fa-tint { color: #00bfff; -} +} \ No newline at end of file