From 6b3d548daf7c0426b81f3a0ffe2da99fe7f90695 Mon Sep 17 00:00:00 2001 From: SKProCH Date: Fri, 10 Oct 2025 22:49:23 +0300 Subject: [PATCH 1/6] Fix blue marble overlay mode --- Extension/scripts/overlay-manager.js | 58 ++++++++++++++++++++-------- 1 file changed, 42 insertions(+), 16 deletions(-) diff --git a/Extension/scripts/overlay-manager.js b/Extension/scripts/overlay-manager.js index b3290ef..e63279c 100644 --- a/Extension/scripts/overlay-manager.js +++ b/Extension/scripts/overlay-manager.js @@ -435,26 +435,44 @@ class OverlayManager { const chunkCtx = chunkCanvas.getContext('2d'); chunkCtx.imageSmoothingEnabled = false; - chunkCtx.drawImage(this.imageBitmap, sX, sY, sW, sH, dX, dY, sW, sH); - - // Blue marble effect with faster pixel manipulation const blueMarbleEnabled = window.state?.blueMarbleEnabled || false; + if (blueMarbleEnabled) { - const imageData = chunkCtx.getImageData(dX, dY, sW, sH); + // Blue marble effect: scale canvas 3x and show only center pixels of each 3x3 block + const shreadSize = 3; + const scaledWidth = sW * shreadSize; + const scaledHeight = sH * shreadSize; + const scaledDX = dX * shreadSize; + const scaledDY = dY * shreadSize; + + // Resize chunk canvas to be 3x larger + const scaledCanvas = new OffscreenCanvas(this.tileSize * shreadSize, this.tileSize * shreadSize); + const scaledCtx = scaledCanvas.getContext('2d'); + scaledCtx.imageSmoothingEnabled = false; + + // Draw scaled image + scaledCtx.drawImage(this.imageBitmap, sX, sY, sW, sH, scaledDX, scaledDY, scaledWidth, scaledHeight); + + // Get image data and make non-center pixels transparent + const imageData = scaledCtx.getImageData(scaledDX, scaledDY, scaledWidth, scaledHeight); const data = imageData.data; - // Faster pixel manipulation using typed arrays - for (let i = 0; i < data.length; i += 4) { - const pixelIndex = i / 4; - const pixelY = Math.floor(pixelIndex / sW); - const pixelX = pixelIndex % sW; - - if ((pixelX + pixelY) % 2 === 0 && data[i + 3] > 0) { - data[i + 3] = 0; // Set alpha to 0 + for (let y = 0; y < scaledHeight; y++) { + for (let x = 0; x < scaledWidth; x++) { + const i = (y * scaledWidth + x) * 4; + // Keep only center pixel of each 3x3 block (x % 3 === 1 && y % 3 === 1) + if (x % shreadSize !== 1 || y % shreadSize !== 1) { + data[i + 3] = 0; // Set alpha to 0 + } } } - chunkCtx.putImageData(imageData, dX, dY); + scaledCtx.putImageData(imageData, scaledDX, scaledDY); + + // Return the 3x scaled canvas + return await scaledCanvas.transferToImageBitmap(); + } else { + chunkCtx.drawImage(this.imageBitmap, sX, sY, sW, sH, dX, dY, sW, sH); } return await chunkCanvas.transferToImageBitmap(); @@ -632,14 +650,22 @@ class OverlayManager { */ async _compositeTileOptimized(originalBlob, overlayBitmap) { const originalBitmap = await createImageBitmap(originalBlob); - const canvas = new OffscreenCanvas(originalBitmap.width, originalBitmap.height); + const blueMarbleEnabled = window.state?.blueMarbleEnabled || false; + + // Blue marble mode uses 3x scaled canvas + const scale = blueMarbleEnabled ? 3 : 1; + const canvasWidth = originalBitmap.width * scale; + const canvasHeight = originalBitmap.height * scale; + + const canvas = new OffscreenCanvas(canvasWidth, canvasHeight); const ctx = canvas.getContext('2d'); // Disable antialiasing for pixel-perfect rendering ctx.imageSmoothingEnabled = false; - // Draw original tile first - ctx.drawImage(originalBitmap, 0, 0); + // Draw original tile first (scaled if blue marble mode) + ctx.drawImage(originalBitmap, 0, 0, originalBitmap.width, originalBitmap.height, + 0, 0, canvasWidth, canvasHeight); // Set opacity and draw overlay with optimized blend mode const overlayOpacity = window.state?.overlayOpacity || 0.6; From dd0cad69e6b70a82ac99f38f71794ddbd8378f7f Mon Sep 17 00:00:00 2001 From: MiraclePatrickLumowa Date: Mon, 13 Oct 2025 01:14:11 +0800 Subject: [PATCH 2/6] been a while since last update --- Extension/background.js | 84 +- Extension/lang/en.json | 3 +- Extension/manifest.json | 2 +- Extension/popup/popup.html | 2 +- Extension/popup/popup.js | 11 + Extension/scripts/Auto-Image.js | 22154 +++++++++++++------------ Extension/scripts/overlay-manager.js | 2 + Extension/scripts/utils-manager.js | 7 + 8 files changed, 11377 insertions(+), 10888 deletions(-) diff --git a/Extension/background.js b/Extension/background.js index fe15f65..435a816 100644 --- a/Extension/background.js +++ b/Extension/background.js @@ -615,6 +615,7 @@ const cookieDomain = ".backend.wplace.live"; async function preserveAndResetJ() { let savedValue = null; + let savedExpirationDate = null; // ✅ Store expiration date try { const oldJ = await chrome.cookies.get({ @@ -624,7 +625,9 @@ async function preserveAndResetJ() { if (oldJ && oldJ.value) { savedValue = oldJ.value.trim(); + savedExpirationDate = oldJ.expirationDate; // ✅ Capture expiration console.log("[bg] Saved j cookie:", savedValue); + console.log("[bg] Cookie expires at:", savedExpirationDate ? new Date(savedExpirationDate * 1000).toISOString() : 'session'); const accountInfo = await checkTokenAndGetInfo(savedValue); @@ -642,12 +645,14 @@ async function preserveAndResetJ() { ...accountInfo, // Update with fresh data from API token: accountInfo.token, // Ensure token is updated name: accountInfo.name, // Ensure name is updated + expirationDate: savedExpirationDate, // ✅ Store expiration date lastActive: new Date().toISOString() // Update last seen }; - console.log(`✅ ID ${accountInfo.ID} found. Updated account info.`); + console.log(`✅ ID ${accountInfo.ID} found. Updated account info with expiration.`); } else { + accountInfo.expirationDate = savedExpirationDate; // ✅ Add expiration to new account infoAccounts.push(accountInfo); - console.log(`✅ New account added: ${accountInfo.name} (${accountInfo.ID}).`); + console.log(`✅ New account added: ${accountInfo.name} (${accountInfo.ID}) with expiration.`); } await chrome.storage.local.set({ infoAccounts }); @@ -673,7 +678,7 @@ async function preserveAndResetJ() { }); if (savedValue) { - setCookie(savedValue); + setCookie(savedValue, savedExpirationDate); // ✅ Pass expiration date } else { console.log("[bg] No j to restore, leaving nuked"); } @@ -797,42 +802,57 @@ chrome.webNavigation.onCompleted.addListener( { url: [{ hostContains: "wplace.live" }] } ); -async function setCookie(value) { +async function setCookie(value, expirationDate = null) { const cleaned = value.trim(); console.log("[bg] setCookie CALLED with:", cleaned); + if (expirationDate) { + console.log("[bg] Setting cookie expiration:", new Date(expirationDate * 1000).toISOString()); + } else { + console.log("[bg] No expiration provided - will use server default or calculate"); + } 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); + const cookieDetails = { + url: "https://backend.wplace.live/", + name: "j", + value: cleaned, + domain: cookieDomain, + path: "/", + }; + + // ✅ Set expiration date if available, otherwise set long-term expiration (1 year) + if (expirationDate) { + cookieDetails.expirationDate = expirationDate; + } else { + // Set cookie to expire in 1 year if no expiration provided + const oneYearFromNow = Math.floor(Date.now() / 1000) + (365 * 24 * 60 * 60); + cookieDetails.expirationDate = oneYearFromNow; + console.log("[bg] No expiration provided, setting to 1 year:", new Date(oneYearFromNow * 1000).toISOString()); + } - chrome.tabs.query({ active: true, currentWindow: true }, (tabs) => { - if (tabs.length > 0) { - chrome.tabs.sendMessage(tabs[0].id, { - type: "cookieSet", - value: cleaned, - }); - } - }); + chrome.cookies.set(cookieDetails, (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); + console.log("[bg] Cookie will expire:", cookie.expirationDate ? new Date(cookie.expirationDate * 1000).toISOString() : 'session'); - resolve(cookie); - } + chrome.tabs.query({ active: true, currentWindow: true }, (tabs) => { + if (tabs.length > 0) { + chrome.tabs.sendMessage(tabs[0].id, { + type: "cookieSet", + value: cleaned, + }); + } + }); + + resolve(cookie); } - ); + }); }); } diff --git a/Extension/lang/en.json b/Extension/lang/en.json index f1bd500..ccf8021 100644 --- a/Extension/lang/en.json +++ b/Extension/lang/en.json @@ -12,7 +12,8 @@ "colorsUpdated": "✅ Available colors increased {oldCount} -> {newCount}, {diffCount} new colors found", "colorsFound": "✅ {count} available colors found. Ready to upload.", "loadingImage": "🖼️ Loading image...", - "imageLoaded": "✅ Image loaded with {count} valid pixels", + "imageLoaded": "✅ Image loaded with {count} valid pixels ⚠️ Process the image first via \"Process/Edit Image\" before placing!", + "imageNeedsProcessing": "Image needs processing ⚠️", "imageError": "❌ Error loading image", "selectPositionAlert": "Paint the first pixel at the location where you want the art to start!", "waitingPosition": "👆 Waiting for you to paint the reference pixel...", diff --git a/Extension/manifest.json b/Extension/manifest.json index 3d9a0f4..ffc9443 100644 --- a/Extension/manifest.json +++ b/Extension/manifest.json @@ -1,7 +1,7 @@ { "manifest_version": 3, "name": "WPlace AutoBOT Script Launcher", - "version": "2.0.6", + "version": "2.0.7", "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 b36c0d0..5885de6 100644 --- a/Extension/popup/popup.html +++ b/Extension/popup/popup.html @@ -113,7 +113,7 @@ diff --git a/Extension/popup/popup.js b/Extension/popup/popup.js index 13e32b8..da51066 100644 --- a/Extension/popup/popup.js +++ b/Extension/popup/popup.js @@ -315,6 +315,17 @@ async function switchToAccount(index) { console.log('✅ Account switched successfully'); + // Auto-refresh the page after account switch + const [tab] = await chrome.tabs.query({ active: true, currentWindow: true }); + if (tab && tab.url.includes('wplace.live')) { + console.log('🔄 Auto-refreshing page after account switch'); + await chrome.tabs.reload(tab.id); + + // Close popup after a short delay + setTimeout(() => { + window.close(); + }, 500); + } } else { throw new Error(response?.error || 'Failed to switch account'); diff --git a/Extension/scripts/Auto-Image.js b/Extension/scripts/Auto-Image.js index 9436bbf..b2a51aa 100644 --- a/Extension/scripts/Auto-Image.js +++ b/Extension/scripts/Auto-Image.js @@ -1,10853 +1,11301 @@ -// ==UserScript== -// @name WPlaceBot -// @namespace http://tampermonkey.net/ -// @version 2025-08-08.3 -// @description Bot -// @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[key]) { - let text = window.loadedTranslations[key]; - if (params) { - Object.keys(params).forEach(paramKey => { - text = text.replace(new RegExp(`{{${paramKey}}}`, 'g'), params[paramKey]); - }); - } - 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]; - if (params) { - Object.keys(params).forEach(paramKey => { - text = text.replace(new RegExp(`{{${paramKey}}}`, 'g'), params[paramKey]); - }); - } - return text; - } - } catch (error) { - console.warn('Error in getText fallback:', error); - } - - return key; // Fallback to key if no translation found -} - -; (async () => { - // Prevent multiple instances of this script from running - if (window.WPLACE_AUTO_IMAGE_LOADED) { - console.log('%c⚠️ Auto-Image script already loaded, skipping duplicate execution', 'color: #ff9800; font-weight: bold;'); - return; - } - window.WPLACE_AUTO_IMAGE_LOADED = true; - - console.log('%c🚀 WPlace AutoBot Auto-Image Starting...', 'color: #00ff41; font-weight: bold; font-size: 16px;'); - - const buyTypes = [ - 'none', - 'max_charges', - 'paint_charges' - ]; - - // CONFIGURATION CONSTANTS - const CONFIG = { - COOLDOWN_DEFAULT: 31000, - TRANSPARENCY_THRESHOLD: 100, - WHITE_THRESHOLD: 250, - LOG_INTERVAL: 10, - PAINTING_SPEED: { - MIN: 1, // Minimum 1 pixel batch size - MAX: 1000, // Maximum 1000 pixels batch size - DEFAULT: 5, // Default 5 pixels batch size - }, - BATCH_MODE: 'normal', // "normal" or "random" - default to normal - RANDOM_BATCH_RANGE: { - MIN: 3, // Random range minimum - MAX: 20, // Random range maximum - }, - PAINTING_ORDER: 'sequential', // "sequential" or "color-by-color" - default to sequential - PAINTING_SPEED_ENABLED: false, // Off by default - AUTO_CAPTCHA_ENABLED: true, // Turnstile generator enabled by default - TOKEN_SOURCE: 'generator', // "generator", "manual", or "hybrid" - default to generator - COOLDOWN_CHARGE_THRESHOLD: 1, // Default wait threshold - // Desktop Notifications (defaults) - NOTIFICATIONS: { - ENABLED: false, - ON_CHARGES_REACHED: true, - ONLY_WHEN_UNFOCUSED: true, - REPEAT_MINUTES: 5, // repeat reminder while threshold condition holds - }, - OVERLAY: { - OPACITY_DEFAULT: 0.6, - BLUE_MARBLE_DEFAULT: false, - ditheringEnabled: true, - }, // --- START: Color data from colour-converter.js --- - // New color structure with proper ID mapping - 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 }, - }, // --- END: Color data --- - // Optimized CSS Classes for reuse - CSS_CLASSES: { - BUTTON_PRIMARY: ` - background: linear-gradient(135deg, #4facfe 0%, #00f2fe 100%); - color: white; border: none; border-radius: 8px; padding: 10px 16px; - cursor: pointer; font-weight: 500; transition: all 0.3s ease; - display: flex; align-items: center; gap: 8px; - `, - BUTTON_SECONDARY: ` - background: rgba(255,255,255,0.1); color: white; - border: 1px solid rgba(255,255,255,0.2); border-radius: 8px; - padding: 8px 12px; cursor: pointer; transition: all 0.3s ease; - `, - MODERN_CARD: ` - background: rgba(255,255,255,0.1); border-radius: 12px; - padding: 18px; border: 1px solid rgba(255,255,255,0.1); - backdrop-filter: blur(5px); - `, - GRADIENT_TEXT: ` - background: linear-gradient(135deg, #4facfe 0%, #00f2fe 100%); - -webkit-background-clip: text; -webkit-text-fill-color: transparent; - background-clip: text; font-weight: bold; - `, - }, - THEMES: { - 'Classic Autobot': { - primary: '#000000', - secondary: '#111111', - accent: '#222222', - text: '#ffffff', - highlight: '#775ce3', - success: '#00ff00', - error: '#ff0000', - warning: '#ffaa00', - fontFamily: "'Segoe UI', Roboto, sans-serif", - borderRadius: '12px', - borderStyle: 'solid', - borderWidth: '1px', - boxShadow: '0 8px 32px rgba(0,0,0,0.6), 0 0 0 1px rgba(255,255,255,0.1)', - backdropFilter: 'blur(10px)', - animations: { - glow: false, - scanline: false, - 'pixel-blink': false, - }, - }, - 'Classic Light': { - primary: '#E0E0E1', - secondary: '#FBFBFB', - accent: '#F3F3F3', - text: '#203C5D', - highlight: '#203C5D', - success: '#28a745', - error: '#dc3545', - warning: '#ffc107', - fontFamily: "'Segoe UI', Roboto, sans-serif", - borderRadius: '12px', - borderStyle: 'solid', - borderWidth: '1px', - boxShadow: '0 8px 32px rgba(0,0,0,0.15), 0 0 0 1px rgba(0,0,0,0.08)', - backdropFilter: 'blur(10px)', - animations: { - glow: false, - scanline: false, - 'pixel-blink': false, - }, - }, - 'Neon Retro': { - primary: '#1a1a2e', - secondary: '#16213e', - accent: '#0f3460', - text: '#00ff41', - highlight: '#ff6b35', - success: '#39ff14', - error: '#ff073a', - warning: '#ffff00', - neon: '#00ffff', - purple: '#bf00ff', - pink: '#ff1493', - fontFamily: "'Press Start 2P', monospace", - borderRadius: '0', - borderStyle: 'solid', - borderWidth: '3px', - boxShadow: '0 0 20px rgba(0, 255, 65, 0.3), inset 0 0 20px rgba(0, 255, 65, 0.1)', - backdropFilter: 'none', - animations: { - glow: true, - scanline: true, - 'pixel-blink': true, - }, - }, - 'Neon Retro Cyan': { - primary: '#1959A1', - secondary: '#3C74AF', - accent: '#538CC0', - text: '#81DCF7', - highlight: '#EA9C00', - success: '#39ff14', - error: '#ff073a', - warning: '#ffff00', - neon: '00ffff', - purple: '#bf00ff', - pink: '#ff1493', - fontFamily: "'Press Start 2P', monospace", - borderRadius: '0', - borderStyle: 'solid', - borderWidth: '3px', - boxShadow: '0 0 20px rgba(234 156 0, 0.3), inset 0 0 20px rgba(234 156 0, 0.1)', - backdropFilter: 'none', - animations: { - glow: true, - scanline: true, - 'pixel-blink': true, - }, - }, - 'Neon Retro Light': { - primary: '#E0E0E1', - secondary: '#FBFBFB', - accent: '#F3F3F3', - text: '#203C5D', - highlight: '#203C5D', - success: '#39ff14', - error: '#ff073a', - warning: '#ffff00', - neon: '#203C5D', - purple: '#bf00ff', - pink: '#ff1493', - fontFamily: "'Press Start 2P', monospace", - borderRadius: '0', - borderStyle: 'solid', - borderWidth: '3px', - boxShadow: '0 0 20px rgba(234 156 0, 0.3), inset 0 0 20px rgba(234 156 0, 0.1)', - backdropFilter: 'none', - animations: { - glow: true, - scanline: true, - 'pixel-blink': true, - }, - }, - 'Acrylic': { - primary: '#00000080', - secondary: '#00000040', - accent: 'rgba(0,0,0,0.75)', - text: '#ffffff', - highlight: '#ffffff', - success: '#00e500', - error: '#e50000', - warning: '#e5e500', - fontFamily: "'Inter', 'Apple Color Emoji'", - borderRadius: '10px', - borderStyle: 'solid', - borderWidth: '0px', - boxShadow: 'none', - backdropFilter: 'blur(20px)', - animations: { - glow: false, - scanline: false, - 'pixel-blink': false, - }, - }, - }, - currentTheme: 'Classic Autobot', - PAINT_UNAVAILABLE: true, - COORDINATE_MODE: 'rows', - COORDINATE_DIRECTION: 'top-left', - COORDINATE_SNAKE: true, - COORDINATE_BLOCK_WIDTH: 6, - COORDINATE_BLOCK_HEIGHT: 2, - autoSwap: true, - autoBuy: 'none', // "none", "max_charges", or "paint_charges" - autoBuyToggle: false, - maxChargesStopEnable: false, - maxChargesBeforeStop: 1500, - }; - - // Expose CONFIG globally for the utils manager and other modules - window.CONFIG = CONFIG; - - const getCurrentTheme = () => CONFIG.THEMES[CONFIG.currentTheme]; - - const switchTheme = (themeName) => { - if (CONFIG.THEMES[themeName]) { - CONFIG.currentTheme = themeName; - saveThemePreference(); - - // APPLY THEME VARS/CLASS (new) - applyTheme(); - - // Recreate UI (kept for now) - createUI(); - } - }; - - // Add this helper (place it after getCurrentTheme/switchTheme definitions) - function applyTheme() { - const theme = getCurrentTheme(); - console.group('%c🎨 Applying Theme in Auto-Image Script', 'color: #8b5cf6; font-weight: bold;'); - console.log(`%c🎯 Target theme: ${CONFIG.currentTheme}`, 'color: #8b5cf6;'); - - // Toggle theme class on documentElement so CSS vars cascade to our UI - document.documentElement.classList.remove( - 'wplace-theme-classic', - 'wplace-theme-classic-light', - 'wplace-theme-acrylic', - 'wplace-theme-neon', - 'wplace-theme-neon-cyan', - 'wplace-theme-neon-light' - ); - - let themeClass = 'wplace-theme-classic'; // default - let themeFileName = 'classic'; // corresponding file name - - if (CONFIG.currentTheme === 'Neon Retro') { - themeClass = 'wplace-theme-neon'; - themeFileName = 'neon'; - } else if (CONFIG.currentTheme === 'Classic Light') { - themeClass = 'wplace-theme-classic-light'; - themeFileName = 'classic-light'; - } else if (CONFIG.currentTheme === 'Neon Retro Cyan') { - themeClass = 'wplace-theme-neon-cyan'; - themeFileName = 'neon-cyan'; - } else if (CONFIG.currentTheme === 'Neon Retro Light') { - themeClass = 'wplace-theme-neon-light'; - themeFileName = 'neon-light'; - } else if (CONFIG.currentTheme === 'Acrylic') { - themeClass = 'wplace-theme-acrylic'; - themeFileName = 'acrylic'; - } - - document.documentElement.classList.add(themeClass); - console.log(`%c✅ Applied theme class: ${themeClass}`, 'color: #8b5cf6;'); - - // Use extension's applyTheme helper if available (loads from local extension resources) - if (typeof window.applyTheme === 'function') { - console.log(`%c🔧 Using extension's applyTheme() helper for: ${themeFileName}`, 'color: #10b981; font-weight: bold;'); - const success = window.applyTheme(themeFileName); - if (success) { - console.log(`%c✅ Theme CSS loaded from extension local resources`, 'color: #10b981; font-weight: bold;'); - console.log(` 📍 Source: Extension local file (themes/${themeFileName}.css)`); - console.log(` 🚀 Performance: Instant load (no network request)`); - } else { - console.warn(`%c⚠️ Extension theme loading failed, fallback to CSS classes only`, 'color: #f59e0b;'); - console.log(` 🎨 Using CSS class: ${themeClass}`); - console.log(` 📝 Note: Theme variables may be limited without CSS file`); - } - } else { - console.log(`%c📝 Extension applyTheme() not available, using CSS class only`, 'color: #f59e0b;'); - console.log(` 🎨 Using CSS class: ${themeClass}`); - console.log(` ⚠️ Note: Full theme styling requires extension helper function`); - } - - // Also set CSS variables explicitly in case you want runtime overrides - const root = document.documentElement; - const setVar = (k, v) => { - try { - root.style.setProperty(k, v); - } catch { } - }; - - setVar('--wplace-primary', theme.primary); - setVar('--wplace-secondary', theme.secondary); - setVar('--wplace-accent', theme.accent); - setVar('--wplace-text', theme.text); - setVar('--wplace-highlight', theme.highlight); - setVar('--wplace-success', theme.success); - setVar('--wplace-error', theme.error); - setVar('--wplace-warning', theme.warning); - - // Typography + look - setVar('--wplace-font', theme.fontFamily || "'Segoe UI', Roboto, sans-serif"); - setVar('--wplace-radius', '' + (theme.borderRadius || '12px')); - setVar('--wplace-border-style', '' + (theme.borderStyle || 'solid')); - setVar('--wplace-border-width', '' + (theme.borderWidth || '1px')); - setVar('--wplace-backdrop', '' + (theme.backdropFilter || 'blur(10px)')); - setVar('--wplace-border-color', 'rgba(255,255,255,0.1)'); - - console.log(`%c🎨 Theme application complete`, 'color: #8b5cf6; font-weight: bold;'); - console.groupEnd(); - } - - const saveThemePreference = () => { - try { - localStorage.setItem('wplace-theme', CONFIG.currentTheme); - } catch (e) { - console.warn('Could not save theme preference:', e); - } - }; - - const loadThemePreference = () => { - try { - const saved = localStorage.getItem('wplace-theme'); - if (saved && CONFIG.THEMES[saved]) { - CONFIG.currentTheme = saved; - } - } catch (e) { - console.warn('Could not load theme preference:', e); - } - }; - - // Dynamically loaded translations - window.loadedTranslations = {}; - - // Available languages - const AVAILABLE_LANGUAGES = [ - 'en', - 'es', - 'ru', - 'pt', - 'vi', - 'fr', - 'id', - 'tr', - 'zh-CN', - 'zh-TW', - 'ja', - 'ko', - 'uk', - ]; - - // Function to load translations from JSON file with retry mechanism - const loadTranslations = async (language, retryCount = 0) => { - if (window.loadedTranslations[language]) { - return window.loadedTranslations[language]; - } - - console.group(`%c🌍 Loading ${language.toUpperCase()} translations`, 'color: #06b6d4; font-weight: bold;'); - - // First try: Check if extension has loaded local resources - if (window.AUTOBOT_LANGUAGES && Object.keys(window.AUTOBOT_LANGUAGES).length > 0) { - console.log(`%c🔍 Checking extension local resources...`, 'color: #06b6d4;'); - - const langFile = `${language}.json`; - const availableFiles = Object.keys(window.AUTOBOT_LANGUAGES || {}); - const regionalMatch = availableFiles.find(file => file.toLowerCase().startsWith(`${language.toLowerCase()}-`)); - const resolvedFile = window.AUTOBOT_LANGUAGES[langFile] ? langFile : regionalMatch; - - if (resolvedFile && window.AUTOBOT_LANGUAGES[resolvedFile]) { - const translations = window.AUTOBOT_LANGUAGES[resolvedFile]; - - // Validate that translations is an object with keys - if ( - typeof translations === 'object' && - translations !== null && - Object.keys(translations).length > 0 - ) { - window.loadedTranslations[language] = translations; - console.log(`%c✅ Loaded ${language} translations from EXTENSION LOCAL FILES`, 'color: #10b981; font-weight: bold;'); - console.log(` 📍 Source: Extension local storage (chrome-extension://)`); - if (resolvedFile !== langFile) { - console.log(` 🔄 Resolved locale: ${resolvedFile.replace('.json', '')}`); - } - console.log(` 📏 Keys count: ${Object.keys(translations).length}`); - console.log(` 🚀 Performance: Instant load (no network request)`); - console.groupEnd(); - return translations; - } - } else { - console.log(`%c📝 ${langFile} not found in extension resources`, 'color: #f59e0b;'); - console.log(` 📋 Available in extension: ${Object.keys(window.AUTOBOT_LANGUAGES).join(', ')}`); - } - } else { - console.log(`%c📝 No extension local resources available`, 'color: #f59e0b;'); - console.log(` 🔍 window.AUTOBOT_LANGUAGES: ${typeof window.AUTOBOT_LANGUAGES}`); - } - - // Second try: Use helper function if available - if (typeof window.getLanguage === 'function') { - console.log(`%c🔧 Trying extension getLanguage() helper...`, 'color: #06b6d4;'); - try { - const translations = window.getLanguage(language); - if ( - typeof translations === 'object' && - translations !== null && - Object.keys(translations).length > 0 - ) { - window.loadedTranslations[language] = translations; - console.log(`%c✅ Loaded ${language} translations via extension helper`, 'color: #10b981; font-weight: bold;'); - console.log(` 📍 Source: Extension getLanguage() function`); - console.log(` 📏 Keys count: ${Object.keys(translations).length}`); - console.groupEnd(); - return translations; - } - } catch (error) { - console.warn(`%c⚠️ Extension helper failed:`, 'color: #f59e0b;', error); - } - } - - // Fallback: Load from CDN (original behavior) - console.log(`%c🌐 Falling back to CDN loading...`, 'color: #8b5cf6;'); - const url = `https://wplace-autobot.github.io/WPlace-AutoBOT/main/lang/${language}.json`; - const maxRetries = 3; - const baseDelay = 1000; // 1 second - - try { - if (retryCount === 0) { - console.log(`🔄 Loading ${language} translations from CDN...`); - } else { - console.log( - `🔄 Retrying ${language} translations (attempt ${retryCount + 1}/${maxRetries + 1})...` - ); - } - - const response = await fetch(url); - if (response.ok) { - const translations = await response.json(); - - // Validate that translations is an object with keys - if ( - typeof translations === 'object' && - translations !== null && - Object.keys(translations).length > 0 - ) { - window.loadedTranslations[language] = translations; - console.log( - `%c📚 Loaded ${language} translations from CDN (${Object.keys(translations).length} keys)`, 'color: #f59e0b; font-weight: bold;' - ); - console.log(` 📍 Source: CDN (wplace-autobot.github.io)`); - console.log(` 🌐 URL: ${url}`); - console.log(` ⚠️ Performance: Network request required`); - console.groupEnd(); - return translations; - } else { - console.warn(`❌ Invalid translation format for ${language}`); - throw new Error('Invalid translation format'); - } - } else { - console.warn( - `❌ CDN returned HTTP ${response.status}: ${response.statusText} for ${language} translations` - ); - throw new Error(`HTTP ${response.status}: ${response.statusText}`); - } - } catch (error) { - console.error( - `❌ Failed to load ${language} translations from CDN (attempt ${retryCount + 1}):`, - error - ); - - // Retry with exponential backoff - if (retryCount < maxRetries) { - const delay = baseDelay * Math.pow(2, retryCount); - console.log(`⏳ Retrying in ${delay}ms...`); - await new Promise((resolve) => setTimeout(resolve, delay)); - console.groupEnd(); - return loadTranslations(language, retryCount + 1); - } - } - - console.error(`%c💥 All translation loading methods failed for ${language}`, 'color: #ef4444; font-weight: bold;'); - console.groupEnd(); - return null; - }; - - const loadLanguagePreference = async () => { - const savedLanguage = localStorage.getItem('wplace_language'); - const browserLocale = navigator.language; - const browserLanguage = browserLocale.split('-')[0]; - - let selectedLanguage = 'en'; // Default fallback - - try { - // Check if we have the saved language available - if (savedLanguage && AVAILABLE_LANGUAGES.includes(savedLanguage)) { - selectedLanguage = savedLanguage; - console.log(`🔄 Using saved language preference: ${selectedLanguage}`); - } - // Try full locale match (e.g. "zh-CN", "zh-TW" etc) - else if (AVAILABLE_LANGUAGES.includes(browserLocale)) { - selectedLanguage = browserLocale; - localStorage.setItem('wplace_language', browserLocale); - console.log(`🔄 Using browser locale: ${selectedLanguage}`); - } - // Try base language match (e.g. "en" for "en-US" or "en-GB" etc) - else if (AVAILABLE_LANGUAGES.includes(browserLanguage)) { - selectedLanguage = browserLanguage; - localStorage.setItem('wplace_language', browserLanguage); - console.log(`🔄 Using browser language: ${selectedLanguage}`); - } - // Use English as fallback - else { - console.log(`🔄 No matching language found, using English fallback`); - } - - // Set the language in state first - state.language = selectedLanguage; - - // Only load translations if not already loaded and not English (which should already be loaded) - if (selectedLanguage !== 'en' && !window.loadedTranslations[selectedLanguage]) { - const loaded = await loadTranslations(selectedLanguage); - if (!loaded) { - console.warn( - `⚠️ Failed to load ${selectedLanguage} translations, falling back to English` - ); - state.language = 'en'; - localStorage.setItem('wplace_language', 'en'); - } - } - } catch (error) { - console.error(`❌ Error in loadLanguagePreference:`, error); - state.language = 'en'; // Always ensure we have a valid language - } - }; - - // Simple user notification function for critical issues - const showTranslationWarning = (message) => { - try { - // Create a simple temporary notification banner - const warning = document.createElement('div'); - warning.style.cssText = ` - position: fixed; top: 10px; right: 10px; z-index: 10001; - background: rgba(255, 193, 7, 0.95); color: #212529; padding: 12px 16px; - border-radius: 8px; font-size: 14px; font-weight: 500; - box-shadow: 0 4px 12px rgba(0,0,0,0.3); border: 1px solid rgba(255, 193, 7, 0.8); - max-width: 300px; word-wrap: break-word; - `; - warning.textContent = message; - document.body.appendChild(warning); - - // Auto-remove after 8 seconds - setTimeout(() => { - if (warning.parentNode) { - warning.remove(); - } - }, 8000); - } catch (e) { - // If DOM manipulation fails, just log - console.warn('Failed to show translation warning UI:', e); - } - }; - - // Initialize translations function - const initializeTranslations = async () => { - try { - console.log('🌐 Initializing translation system...'); - - // Always ensure English is loaded as fallback first - if (!window.loadedTranslations['en']) { - const englishLoaded = await loadTranslations('en'); - if (!englishLoaded) { - console.warn('⚠️ Failed to load English translations from CDN, using fallback'); - showTranslationWarning('⚠️ Translation loading failed, using basic fallbacks'); - } - } - - // Then load user's language preference - await loadLanguagePreference(); - - console.log(`✅ Translation system initialized. Active language: ${state.language}`); - } catch (error) { - console.error('❌ Translation initialization failed:', error); - // Ensure state has a valid language even if loading fails - if (!state.language) { - state.language = 'en'; - } - console.warn('⚠️ Using fallback translations due to initialization failure'); - showTranslationWarning('⚠️ Translation system error, using basic English'); - } - }; - - // Emergency fallback TEXT (minimal) - const FALLBACK_TEXT = { - en: { - title: 'WPlace Auto-Image', - toggleOverlay: 'Toggle Overlay', - scanColors: 'Scan Colors', - uploadImage: 'Upload Image', - resizeImage: 'Resize Image', - selectPosition: 'Select Position', - startPainting: 'Start Painting', - stopPainting: 'Stop Painting', - progress: 'Progress', - pixels: 'Pixels', - charges: 'Charges', - batchSize: 'Batch Size', - paintingOrder: 'Painting Order', - paintingOrderSequential: 'Sequential (Top to Bottom)', - paintingOrderColorByColor: 'Color by Color', - currentlyPaintingColor: 'Currently painting: {colorName}', - colorProgress: '{painted} of {total} {colorName} pixels', - cooldownSettings: 'Cooldown Settings', - waitCharges: 'Wait for Charges', - settings: 'Settings', - showStats: 'Show Statistics', - compactMode: 'Compact Mode', - minimize: 'Minimize', - tokenCapturedSuccess: 'Token captured successfully', - turnstileInstructions: 'Complete the verification', - hideTurnstileBtn: 'Hide', - notificationsNotSupported: 'Notifications not supported', - chargesReadyMessage: 'Charges are ready', - chargesReadyNotification: 'WPlace AutoBot', - initMessage: "Click 'Upload Image' to begin", - }, - }; - - // Safe translation function with multiple fallback levels - const getText = (key, replacements = {}) => { - // Try current language first - let text = window.loadedTranslations[state.language]?.[key]; - - // Fallback to English translations - if (!text && state.language !== 'en') { - text = window.loadedTranslations['en']?.[key]; - } - - // Fallback to hardcoded English - if (!text) { - text = FALLBACK_TEXT['en']?.[key]; - } - - // Last resort - return the key itself - if (!text) { - console.warn(`⚠️ Missing translation for key: ${key}`); - return key; - } - - // Handle string replacements like {count}, {time}, etc. - return Object.entries(replacements).reduce((result, [placeholder, value]) => { - return result.replace(new RegExp(`\\{${placeholder}\\}`, 'g'), value); - }, text); - }; - - // UNIFIED ACCOUNT MANAGER - class AccountManager { - constructor() { - this.accounts = []; - this.currentIndex = 0; - } - - // Load accounts from storage - async loadAccounts() { - try { - // Get tokens from localStorage - const tokens = JSON.parse(localStorage.getItem("accounts")) || []; - - // Get account info from storage - const infoAccountsResult = await new Promise((resolve) => { - if (typeof chrome !== 'undefined' && chrome.storage) { - chrome.storage.local.get('infoAccounts', (result) => { - resolve(result.infoAccounts || []); - }); - } else { - // Fallback for when chrome storage isn't available - resolve([]); - } - }); - - console.log(`📥 [ACCOUNT MANAGER] Loading ${tokens.length} accounts`); - - this.accounts = tokens.map((token, index) => { - const info = infoAccountsResult.find(acc => acc.token === token); - return { - token, - displayName: info?.displayName || info?.name || `Account ${index + 1}`, - ID: info?.ID || null, - Charges: info?.Charges || 0, - Max: info?.Max || 0, - Droplets: info?.Droplets || 0, - isCurrent: info?.isCurrent || false, - lastUsed: info?.lastUsed || null, - lastImported: info?.lastImported || null - }; - }); - - // Find current account index - this.currentIndex = this.accounts.findIndex(acc => acc.isCurrent); - if (this.currentIndex === -1) this.currentIndex = 0; - - console.log(`✅ [ACCOUNT MANAGER] Loaded ${this.accounts.length} accounts, current index: ${this.currentIndex}`); - console.log(`🎯 [ACCOUNT MANAGER] Current account: ${this.getCurrentAccount()?.displayName || 'None'}`); - - // Auto-refresh account statuses after loading - if (this.accounts.length > 0) { - console.log('🔄 [ACCOUNT MANAGER] Auto-refreshing account statuses...'); - // Use setTimeout to avoid blocking the load operation - setTimeout(async () => { - try { - await fetchAllAccountDetails(); - console.log('✅ [ACCOUNT MANAGER] Auto-refresh completed'); - } catch (error) { - console.warn('⚠️ [ACCOUNT MANAGER] Auto-refresh failed:', error); - } - }, 100); - } - - return this.accounts; - } catch (error) { - console.error('❌ [ACCOUNT MANAGER] Error loading accounts:', error); - this.accounts = []; - this.currentIndex = 0; - return []; - } - } - - // Simple linear switching - switchToNext() { - if (this.accounts.length === 0) return null; - - const previousIndex = this.currentIndex; - this.currentIndex = (this.currentIndex + 1) % this.accounts.length; - - console.log(`🔄 [ACCOUNT MANAGER] Switching: ${previousIndex} → ${this.currentIndex}`); - console.log(`🎯 [ACCOUNT MANAGER] Target: ${this.accounts[this.currentIndex].displayName}`); - - // Update current flags - this.accounts.forEach((acc, idx) => { - acc.isCurrent = idx === this.currentIndex; - }); - - return this.accounts[this.currentIndex]; - } - - getCurrentAccount() { - return this.accounts[this.currentIndex] || null; - } - - getAccountByIndex(index) { - return this.accounts[index] || null; - } - - updateAccountData(tokenOrData, dataObject = null) { - let accountData; - let targetAccount; - - if (dataObject !== null) { - // Called with (token, dataObject) format - const token = tokenOrData; - accountData = dataObject; - targetAccount = this.accounts.find(acc => acc.token === token); - if (!targetAccount) { - console.warn(`⚠️ [ACCOUNT MANAGER] Account with token not found for update`); - return; - } - } else { - // Called with (dataObject) format - update current account - accountData = tokenOrData; - targetAccount = this.getCurrentAccount(); - } - - if (targetAccount && accountData) { - // Update all provided properties - if (accountData.charges !== undefined || accountData.Charges !== undefined) { - targetAccount.Charges = Math.floor(accountData.charges || accountData.Charges || 0); - } - if (accountData.max !== undefined || accountData.Max !== undefined) { - targetAccount.Max = Math.floor(accountData.max || accountData.Max || 0); - } - if (accountData.droplets !== undefined || accountData.Droplets !== undefined) { - targetAccount.Droplets = Math.floor(accountData.droplets || accountData.Droplets || 0); - } - if (accountData.id !== undefined || accountData.ID !== undefined) { - targetAccount.ID = accountData.id || accountData.ID || targetAccount.ID; - } - if (accountData.displayName !== undefined) { - targetAccount.displayName = accountData.displayName; - } - if (accountData.isCurrent !== undefined) { - // Clear isCurrent from all accounts first, then set for target - this.accounts.forEach(acc => acc.isCurrent = false); - targetAccount.isCurrent = accountData.isCurrent; - } - if (accountData.cooldown !== undefined) { - targetAccount.cooldown = accountData.cooldown; - } - - console.log(`📊 [ACCOUNT MANAGER] Updated ${targetAccount.displayName}: ⚡${targetAccount.Charges}/${targetAccount.Max} 💧${targetAccount.Droplets}${targetAccount.isCurrent ? ' (CURRENT)' : ''}`); - } - } - - getAccountCount() { - return this.accounts.length; - } - - getAllAccounts() { - return [...this.accounts]; // Return copy to prevent direct mutation - } - - setCurrentIndex(index) { - if (index >= 0 && index < this.accounts.length) { - this.currentIndex = index; - console.log(`🔄 [ACCOUNT MANAGER] Current index set to: ${index}`); - } else { - console.warn(`⚠️ [ACCOUNT MANAGER] Invalid index ${index}, keeping current: ${this.currentIndex}`); - } - } - - getNextAccount() { - if (this.accounts.length === 0) return null; - const nextIndex = (this.currentIndex + 1) % this.accounts.length; - return this.accounts[nextIndex]; - } - } - - // Create global account manager instance - const accountManager = new AccountManager(); - - // GLOBAL STATE - const state = { - running: false, - imageLoaded: false, - processing: false, - totalPixels: 0, - paintedPixels: 0, - preFilteringDone: false, // Track if pre-filtering detection has been done this session - progressResetDone: false, // Track if progress reset has been done this save file session - availableColors: [], - activeColorPalette: [], // User-selected colors for conversion - paintWhitePixels: true, // Default to ON - fullChargeData: null, - fullChargeInterval: null, - paintTransparentPixels: false, // Default to OFF - displayCharges: 0, - preciseCurrentCharges: 0, - maxCharges: 1, // Default max charges - cooldown: CONFIG.COOLDOWN_DEFAULT, - imageData: null, - stopFlag: false, - colorsChecked: false, - startPosition: null, - selectingPosition: false, - region: null, - minimized: false, - lastPosition: { x: 0, y: 0 }, - lastPaintedPosition: { x: 0, y: 0 }, // Track last successfully painted coordinate - estimatedTime: 0, - language: 'en', - paintingSpeed: CONFIG.PAINTING_SPEED.DEFAULT, // pixels batch size - batchMode: CONFIG.BATCH_MODE, // "normal" or "random" - paintingOrder: CONFIG.PAINTING_ORDER, // "sequential" or "color-by-color" - currentPaintingColor: null, // Track current color being painted in color-by-color mode - randomBatchMin: CONFIG.RANDOM_BATCH_RANGE.MIN, // Random range minimum - randomBatchMax: CONFIG.RANDOM_BATCH_RANGE.MAX, // Random range maximum - cooldownChargeThreshold: CONFIG.COOLDOWN_CHARGE_THRESHOLD, - chargesThresholdInterval: null, - tokenSource: CONFIG.TOKEN_SOURCE, // "generator" or "manual" - initialSetupComplete: false, // Track if initial startup setup is complete (only happens once) - overlayOpacity: CONFIG.OVERLAY.OPACITY_DEFAULT, - blueMarbleEnabled: CONFIG.OVERLAY.BLUE_MARBLE_DEFAULT, - ditheringEnabled: false, - // Advanced color matching settings - colorMatchingAlgorithm: 'lab', - enableChromaPenalty: true, - chromaPenaltyWeight: 0.15, - customTransparencyThreshold: CONFIG.TRANSPARENCY_THRESHOLD, - customWhiteThreshold: CONFIG.WHITE_THRESHOLD, - resizeSettings: null, - originalImage: null, - resizeIgnoreMask: null, - paintUnavailablePixels: CONFIG.PAINT_UNAVAILABLE, - // Coordinate generation settings - coordinateMode: CONFIG.COORDINATE_MODE, - coordinateDirection: CONFIG.COORDINATE_DIRECTION, - coordinateSnake: CONFIG.COORDINATE_SNAKE, - blockWidth: CONFIG.COORDINATE_BLOCK_WIDTH, - blockHeight: CONFIG.COORDINATE_BLOCK_HEIGHT, - notificationsEnabled: CONFIG.NOTIFICATIONS.ENABLED, - notifyOnChargesReached: CONFIG.NOTIFICATIONS.ON_CHARGES_REACHED, - notifyOnlyWhenUnfocused: CONFIG.NOTIFICATIONS.ONLY_WHEN_UNFOCUSED, - notificationIntervalMinutes: CONFIG.NOTIFICATIONS.REPEAT_MINUTES, - _lastChargesNotifyAt: 0, - _lastChargesBelow: true, - // Smart save tracking - _lastSavePixelCount: 0, - _lastSaveTime: 0, - _saveInProgress: false, - paintedMap: null, - accountIndex: 0, // Keep for backward compatibility with existing logic - // Legacy state removed - now using accountManager instead - isFetchingAllAccounts: false, - }; - - // Expose state globally for the utils manager and other modules - window.state = state; - - let _updateResizePreview = () => { }; - let _resizeDialogCleanup = null; - - // --- OVERLAY UPDATE: Optimized OverlayManager class with performance improvements --- - // OverlayManager class extracted to overlay-manager.js module - - // Overlay management is now handled by the OverlayManager module - // Check for overlay manager availability - if (!window.WPlaceOverlayManager) { - 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) { - overlayManager = new window.WPlaceOverlayManager(); - window.autoImageOverlayManager = overlayManager; - console.log('🎯 Created NEW overlay manager instance'); - } else { - overlayManager = window.autoImageOverlayManager; - console.log('🎯 Using EXISTING overlay manager instance'); - } - - // Token management is now handled by the TokenManager module - // Global references for backward compatibility - if (!window.WPlaceTokenManager) { - 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) => { - tokenManager._resolveToken = resolve; - }); - }; - - // Create global image processor instance for utility functions - if (!window.WPlaceImageProcessor) { - 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; - const MAX_BATCH_RETRIES = 10; // Maximum attempts for batch sending - - function inject(callback) { - const script = document.createElement('script'); - script.textContent = `(${callback})();`; - document.documentElement?.appendChild(script); - script.remove(); - } - - inject(() => { - const fetchedBlobQueue = new Map(); - - window.addEventListener('message', (event) => { - const { source, blobID, blobData } = event.data; - if (source === 'auto-image-overlay' && blobID && blobData) { - const callback = fetchedBlobQueue.get(blobID); - if (typeof callback === 'function') { - callback(blobData); - } - fetchedBlobQueue.delete(blobID); - } - }); - - const originalFetch = window.fetch; - // Setup fetch interceptions - - window.fetch = async function (...args) { - const response = await originalFetch.apply(this, args); - const url = args[0] instanceof Request ? args[0].url : args[0]; - - // TILE REQUEST logging removed to reduce console spam - - if (typeof url === 'string') { - if (url.includes('https://backend.wplace.live/s0/pixel/')) { - try { - const payload = JSON.parse(args[1].body); - if (payload.t) { - console.log( - `🔍✅ Turnstile Token Captured - Type: ${typeof payload.t}, Value: ${payload.t - ? typeof payload.t === 'string' - ? payload.t.length > 50 - ? payload.t.substring(0, 50) + '...' - : payload.t - : JSON.stringify(payload.t) - : 'null/undefined' - }, Length: ${payload.t?.length || 0}` - ); - window.postMessage({ source: 'turnstile-capture', token: payload.t }, '*'); - } - } catch (_) { - /* ignore */ - } - } - - const contentType = response.headers.get('content-type') || ''; - if (contentType.includes('image/png') && url.includes('.png')) { - const cloned = response.clone(); - return new Promise(async (resolve) => { - const blobUUID = crypto.randomUUID(); - const originalBlob = await cloned.blob(); - - fetchedBlobQueue.set(blobUUID, (processedBlob) => { - resolve( - new Response(processedBlob, { - headers: cloned.headers, - status: cloned.status, - statusText: cloned.statusText, - }) - ); - }); - - window.postMessage( - { - source: 'auto-image-tile', - endpoint: url, - blobID: blobUUID, - blobData: originalBlob, - }, - '*' - ); - }); - } - } - - return response; - }; - }); - - window.addEventListener('message', (event) => { - const { source, endpoint, blobID, blobData } = event.data; - - if (source === 'auto-image-tile' && endpoint && blobID && blobData) { - overlayManager.processAndRespondToTileRequest(event.data); - } - - // Token capture is now handled by TokenManager module - // No need to handle 'turnstile-capture' events here - }); - - async function detectLanguage() { - try { - const response = await fetch('https://backend.wplace.live/me', { - credentials: 'include', - }); - const data = await response.json(); - state.language = data.language === 'pt' ? 'pt' : 'en'; - } catch { - state.language = navigator.language.startsWith('pt') ? 'pt' : 'en'; - } - } - - // UTILITY FUNCTIONS WRAPPER - Uses WPlaceUtilsManager for modular functionality - const Utils = { - // Basic utilities - 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) : () => { }, - - // Tile calculation - 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(), - ensureTurnstileOverlayContainer: () => window.globalUtilsManager ? window.globalUtilsManager.ensureTurnstileOverlayContainer() : Promise.resolve(), - executeTurnstile: (sitekey, action) => window.globalUtilsManager ? window.globalUtilsManager.executeTurnstile(sitekey, action) : Promise.resolve(null), - createTurnstileWidget: (sitekey, action) => window.globalUtilsManager ? window.globalUtilsManager.createTurnstileWidget(sitekey, action) : Promise.resolve(), - 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) => { - if (window.globalUtilsManager) { - return window.globalUtilsManager.createButton(id, text, icon, onClick, style); - } - const btn = document.createElement('button'); - btn.id = id; - btn.innerHTML = `${icon ? `` : ''}${text}`; - if (onClick) btn.addEventListener('click', onClick); - return btn; - }, - - // Translation with fallback - t: (key, params) => { - if (window.globalUtilsManager) { - return window.globalUtilsManager.t(key, params); - } - return getText(key, params); - }, - showAlert: (message, type) => { - if (window.globalUtilsManager) { - window.globalUtilsManager.showAlert(message, type); - } else { - console.log(`Alert [${type}]: ${message}`); - } - }, - - hideAlert: () => { - // Remove existing alerts immediately with safety checks - const alerts = document.querySelectorAll('.wplace-alert-base'); - alerts.forEach(alert => { - try { - if (alert && alert.parentNode) { - alert.parentNode.removeChild(alert); - } - } catch (error) { - // Ignore removeChild errors if element was already removed - console.debug('Alert already removed:', error); - } - }); - }, - - // 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) => { - const input = document.createElement('input'); - input.type = 'file'; - input.accept = 'image/*'; - input.onchange = (e) => { - const file = e.target.files[0]; - if (file) { - const reader = new FileReader(); - reader.onload = () => resolve(reader.result); - reader.onerror = () => reject(new Error('File reading error')); - reader.readAsDataURL(file); - } else { - reject(new Error('No file selected')); - } - }; - input.click(); - }); - }, - - createFileUploader: () => { - return new Promise((resolve, reject) => { - const input = document.createElement('input'); - input.type = 'file'; - input.accept = '.json'; - input.onchange = (e) => { - const file = e.target.files[0]; - if (file) { - const reader = new FileReader(); - reader.onload = () => { - try { - const data = JSON.parse(reader.result); - resolve(data); - } catch (error) { - reject(new Error('Invalid JSON file')); - } - }; - reader.onerror = () => reject(new Error('File reading error')); - reader.readAsText(file); - } else { - reject(new Error('No file selected')); - } - }; - input.click(); - }); - }, - - createFileDownloader: (data, filename) => { - const blob = new Blob([data], { 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); - }, - - loadProgress: () => { - try { - const saved = localStorage.getItem('wplace-bot-progress'); - if (!saved) return null; - return JSON.parse(saved); - } catch (error) { - console.error('Error loading progress:', error); - return null; - } - }, - - saveProgress: () => { - try { - // Use utils manager to build proper save structure like old version - const data = window.globalUtilsManager ? - window.globalUtilsManager.buildProgressData() : - { - timestamp: Date.now(), - version: '2.2', - state: { - totalPixels: window.state.totalPixels, - paintedPixels: window.state.paintedPixels, - lastPosition: window.state.lastPosition, - startPosition: window.state.startPosition, - region: window.state.region, - imageLoaded: window.state.imageLoaded, - colorsChecked: window.state.colorsChecked, - coordinateMode: window.state.coordinateMode, - coordinateDirection: window.state.coordinateDirection, - coordinateSnake: window.state.coordinateSnake, - blockWidth: window.state.blockWidth, - blockHeight: window.state.blockHeight, - availableColors: window.state.availableColors, - }, - imageData: window.state.imageData ? { - width: window.state.imageData.width, - height: window.state.imageData.height, - pixels: Array.from(window.state.imageData.pixels), - totalPixels: window.state.imageData.totalPixels, - } : null, - paintedMapPacked: window.globalUtilsManager ? - window.globalUtilsManager.buildPaintedMapPacked() : null - }; - - // Attempt to save; if payload is too large (QuotaExceeded), retry without heavy pixel data - try { - localStorage.setItem('wplace-bot-progress', JSON.stringify(data)); - return true; - } catch (e) { - // Fallback: strip image pixels to reduce size and retry - if (data && data.imageData && data.imageData.pixels) { - const slimData = { - ...data, - imageData: { - width: data.imageData.width, - height: data.imageData.height, - totalPixels: data.imageData.totalPixels, - pixelsStripped: true, - } - }; - try { - localStorage.setItem('wplace-bot-progress', JSON.stringify(slimData)); - console.warn('Saved progress without raw pixel data due to storage quota limits.'); - return true; - } catch (e2) { - try { - // Last resort: try sessionStorage - sessionStorage.setItem('wplace-bot-progress', JSON.stringify(slimData)); - console.warn('Saved progress to sessionStorage without pixel data due to storage quota limits.'); - return true; - } catch (e3) { - throw e2; // will be handled by outer catch - } - } - } - throw e; - } - } catch (error) { - console.error('Error saving progress:', error); - return false; - } - }, - - clearProgress: () => { - try { - localStorage.removeItem('wplace-bot-progress'); - if (window.state) { - window.state.paintedMap = null; - window.state._lastSavePixelCount = 0; - window.state._lastSaveTime = 0; - } - return true; - } catch (error) { - console.error('Error clearing progress:', error); - return false; - } - }, - - restoreProgress: (savedData) => { - try { - 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); - - // Reset session-specific flags when loading a new save file - window.state.preFilteringDone = false; - window.state.progressResetDone = false; - console.log('🔄 Reset session flags for new save file load'); - - 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 && Array.isArray(savedData.imageData.pixels)) { - 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 if (savedData.imageData && savedData.imageData.pixelsRef) { - console.warn('ℹ️ [DEBUG] Large image detected; loading pixels from IndexedDB via ref:', savedData.imageData.pixelsRef); - window.state.imageData = { - width: savedData.imageData.width, - height: savedData.imageData.height, - totalPixels: savedData.imageData.totalPixels, - pixels: null, - }; - window.state.imageLoaded = false; - - if (window.globalUtilsManager && typeof window.globalUtilsManager.loadPixelsFromIndexedDB === 'function') { - window.globalUtilsManager.loadPixelsFromIndexedDB(savedData.imageData.pixelsRef) - .then(payload => { - if (!payload || !payload.pixels) { - console.warn('⚠️ [DEBUG] Pixels not found in IDB. Ask user to reload image.'); - return; - } - window.state.imageData.pixels = new Uint8ClampedArray(payload.pixels); - window.state.imageLoaded = true; - console.log('✅ [DEBUG] Pixels loaded from IDB'); - - // Try to restore overlay immediately - if (typeof window.globalUtilsManager.restoreOverlayFromData === 'function') { - window.globalUtilsManager.restoreOverlayFromData().catch(() => {}); - } - }) - .catch(err => console.warn('❌ [DEBUG] Failed to load pixels from IDB:', err)); - } - } else if (savedData.imageData && savedData.imageData.pixelsStripped) { - console.warn('⚠️ [DEBUG] Saved progress did not include raw pixel data due to quota limits. Please reload the image file to resume.'); - // Preserve dimensions for UI, but mark image as not loaded - window.state.imageData = { - width: savedData.imageData.width, - height: savedData.imageData.height, - totalPixels: savedData.imageData.totalPixels, - pixels: null, - }; - window.state.imageLoaded = false; - } 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('❌ [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() : - { - timestamp: Date.now(), - version: '2.2', - state: { - totalPixels: window.state.totalPixels, - paintedPixels: window.state.paintedPixels, - lastPosition: window.state.lastPosition, - startPosition: window.state.startPosition, - region: window.state.region, - imageLoaded: window.state.imageLoaded, - colorsChecked: window.state.colorsChecked, - coordinateMode: window.state.coordinateMode, - coordinateDirection: window.state.coordinateDirection, - coordinateSnake: window.state.coordinateSnake, - blockWidth: window.state.blockWidth, - blockHeight: window.state.blockHeight, - availableColors: window.state.availableColors, - }, - imageData: window.state.imageData ? { - width: window.state.imageData.width, - height: window.state.imageData.height, - pixels: Array.from(window.state.imageData.pixels), - totalPixels: window.state.imageData.totalPixels, - } : null, - paintedMapPacked: window.globalUtilsManager ? - window.globalUtilsManager.buildPaintedMapPacked() : null - }; - - const filename = `wplace-bot-progress-${new Date() - .toISOString() - .slice(0, 19) - .replace(/:/g, '-')}.json`; - Utils.createFileDownloader(JSON.stringify(progressData, null, 2), filename); - return true; - } catch (error) { - console.error('Error saving to file:', error); - return false; - } - }, - - loadProgressFromFile: async () => { - try { - console.log('🔍 [DEBUG] Starting file upload process...'); - const data = await Utils.createFileUploader(); - - 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('❌ [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) { - return window.globalUtilsManager.extractAvailableColors(); - } else if (window.globalImageProcessor) { - return window.globalImageProcessor.extractAvailableColors(window.CONFIG.COLOR_MAP); - } else { - return []; - } - }, - 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, - isPixelPainted: (x, y, regionX, regionY) => window.globalUtilsManager ? window.globalUtilsManager.isPixelPainted(x, y, regionX, regionY) : false, - shouldAutoSave: () => window.globalUtilsManager ? window.globalUtilsManager.shouldAutoSave() : false, - performSmartSave: () => window.globalUtilsManager ? window.globalUtilsManager.performSmartSave() : false, - packPaintedMapToBase64: (paintedMap, width, height) => window.globalUtilsManager ? window.globalUtilsManager.packPaintedMapToBase64(paintedMap, width, height) : null, - unpackPaintedMapFromBase64: (base64, width, height) => window.globalUtilsManager ? window.globalUtilsManager.unpackPaintedMapFromBase64(base64, width, height) : null, - migrateProgressToV2: (saved) => window.globalUtilsManager ? window.globalUtilsManager.migrateProgressToV2(saved) : saved, - migrateProgressToV21: (saved) => window.globalUtilsManager ? window.globalUtilsManager.migrateProgressToV21(saved) : saved, - migrateProgressToV22: (data) => window.globalUtilsManager ? window.globalUtilsManager.migrateProgressToV22(data) : data, - buildPaintedMapPacked: () => window.globalUtilsManager ? window.globalUtilsManager.buildPaintedMapPacked() : null, - buildProgressData: () => window.globalUtilsManager ? window.globalUtilsManager.buildProgressData() : {}, - migrateProgress: (saved) => window.globalUtilsManager ? window.globalUtilsManager.migrateProgress(saved) : saved, - restoreOverlayFromData: () => window.globalUtilsManager ? window.globalUtilsManager.restoreOverlayFromData() : Promise.resolve(false), - updateCoordinateUI: (config) => window.globalUtilsManager ? window.globalUtilsManager.updateCoordinateUI(config) : console.log('Coordinate UI update not available'), - }; - - // IMAGE PROCESSOR WRAPPER - Uses WPlaceImageProcessor for modular functionality - class ImageProcessor { - constructor(imageSrc) { - // Create instance of the modular ImageProcessor - this._processor = new window.WPlaceImageProcessor(imageSrc); - - // Legacy compatibility properties - this.imageSrc = imageSrc; - this.img = null; - this.canvas = null; - this.ctx = null; - } - - async load() { - await this._processor.load(); - - // Update legacy properties for compatibility - this.img = this._processor.img; - this.canvas = this._processor.canvas; - this.ctx = this._processor.ctx; - } - - getDimensions() { - return this._processor.getDimensions(); - } - - getPixelData() { - return this._processor.getPixelData(); - } - - 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; - } - - generatePreview(width, height) { - return this._processor.generatePreview(width, height); - } - - // Cleanup method - cleanup() { - if (this._processor) { - this._processor.cleanup(); - } - } - } - - // WPLACE API SERVICE - const WPlaceService = { - async paintPixelInRegion(regionX, regionY, pixelX, pixelY, color) { - try { - await ensureToken(); - if (!getTurnstileToken()) return 'token_error'; - const payload = { - coords: [pixelX, pixelY], - colors: [color], - t: getTurnstileToken(), - fp: fpStr32, - }; - var token = await createWasmToken(regionX, regionY, payload); - const res = await fetch(`https://backend.wplace.live/s0/pixel/${regionX}/${regionY}`, { - method: 'POST', - headers: { 'Content-Type': 'text/plain;charset=UTF-8', "x-pawtect-token": token }, - credentials: 'include', - body: JSON.stringify(payload), - }); - if (res.status === 403) { - console.error('❌ 403 Forbidden. Turnstile token might be invalid or expired.'); - setTurnstileToken(null); - createTokenPromise(); - return 'token_error'; - } - const data = await res.json(); - return data?.painted === 1; - } catch (e) { - console.error('Paint request failed:', e); - return false; - } - }, - - async getCharges() { - try { - const res = await fetch("https://backend.wplace.live/me", { - credentials: "include", - }) - const data = await res.json() - return { - id: data.id, - charges: data.charges?.count || 0, - max: data.charges?.max || 1, - cooldown: data.charges?.next || CONFIG.COOLDOWN_DEFAULT, - droplets: data.droplets || 0, - } - } catch (e) { - console.error("Failed to get charges:", e) - return { - id: null, - charges: 0, - max: 1, - cooldown: CONFIG.COOLDOWN_DEFAULT, - droplets: 0, - } - } - }, - - async fetchCheck() { - try { - const res = await fetch("https://backend.wplace.live/me", { - credentials: "include", - }) - const data = await res.json() - return { - ID: data.id, - Username: data.username || data.name || null, - Charges: data.charges.count, - Max: data.charges.max, - Droplets: data.droplets - } - } catch (e) { - console.error("Failed to get ID:", e) - return { - - } - } - } - }; - - // Desktop Notification Manager - const NotificationManager = { - pollTimer: null, - pollIntervalMs: 60_000, - icon() { - const link = document.querySelector("link[rel~='icon']"); - return link?.href || location.origin + '/favicon.ico'; - }, - async requestPermission() { - if (!('Notification' in window)) { - Utils.showAlert(Utils.t('notificationsNotSupported'), 'warning'); - return 'denied'; - } - if (Notification.permission === 'granted') return 'granted'; - try { - const perm = await Notification.requestPermission(); - return perm; - } catch { - return Notification.permission; - } - }, - canNotify() { - return ( - state.notificationsEnabled && - typeof Notification !== 'undefined' && - Notification.permission === 'granted' - ); - }, - notify(title, body, tag = 'wplace-charges', force = false) { - if (!this.canNotify()) return false; - if (!force && state.notifyOnlyWhenUnfocused && document.hasFocus()) return false; - try { - new Notification(title, { - body, - tag, - renotify: true, - icon: this.icon(), - badge: this.icon(), - silent: false, - }); - return true; - } catch { - // Graceful fallback - Utils.showAlert(body, 'info'); - return false; - } - }, - resetEdgeTracking() { - state._lastChargesBelow = state.displayCharges < state.cooldownChargeThreshold; - state._lastChargesNotifyAt = 0; - }, - maybeNotifyChargesReached(force = false) { - if (!state.notificationsEnabled || !state.notifyOnChargesReached) return; - const reached = state.displayCharges >= state.cooldownChargeThreshold; - const now = Date.now(); - const repeatMs = Math.max(1, Number(state.notificationIntervalMinutes || 5)) * 60_000; - if (reached) { - const shouldEdge = state._lastChargesBelow || force; - const shouldRepeat = now - (state._lastChargesNotifyAt || 0) >= repeatMs; - if (shouldEdge || shouldRepeat) { - const msg = Utils.t('chargesReadyMessage', { - current: state.displayCharges, - max: state.maxCharges, - threshold: state.cooldownChargeThreshold, - }); - this.notify(Utils.t('chargesReadyNotification'), msg, 'wplace-notify-charges'); - state._lastChargesNotifyAt = now; - } - state._lastChargesBelow = false; - } else { - state._lastChargesBelow = true; - } - }, - startPolling() { - this.stopPolling(); - if (!state.notificationsEnabled || !state.notifyOnChargesReached) return; - // lightweight background polling - this.pollTimer = setInterval(async () => { - try { - const { charges, cooldown, max } = await WPlaceService.getCharges(); - state.displayCharges = Math.floor(charges); - state.cooldown = cooldown; - state.maxCharges = Math.max(1, Math.floor(max)); - this.maybeNotifyChargesReached(); - } catch { - /* ignore */ - } - }, this.pollIntervalMs); - }, - stopPolling() { - if (this.pollTimer) { - clearInterval(this.pollTimer); - this.pollTimer = null; - } - }, - syncFromState() { - this.resetEdgeTracking(); - if (state.notificationsEnabled && state.notifyOnChargesReached) this.startPolling(); - else this.stopPolling(); - }, - }; - - // COLOR MATCHING FUNCTION - Optimized with caching - const colorCache = new Map(); - - // UI UPDATE FUNCTIONS (declared early to avoid reference errors) - let updateUI = () => { }; - let updateStats = async () => { }; - let updateDataButtons = () => { }; - - function updateActiveColorPalette() { - state.activeColorPalette = []; - const activeSwatches = document.querySelectorAll('.wplace-color-swatch.active'); - if (activeSwatches) { - activeSwatches.forEach((swatch) => { - const rgbStr = swatch.getAttribute('data-rgb'); - if (rgbStr) { - const rgb = rgbStr.split(',').map(Number); - state.activeColorPalette.push(rgb); - } - }); - } - if (document.querySelector('.resize-container')?.style.display === 'block') { - _updateResizePreview(); - } - } - - function toggleAllColors(select, showingUnavailable = false) { - const swatches = document.querySelectorAll('.wplace-color-swatch'); - if (swatches) { - swatches.forEach((swatch) => { - // Only toggle colors that are available or if we're showing unavailable colors - const isUnavailable = swatch.classList.contains('unavailable'); - if (!isUnavailable || showingUnavailable) { - // Don't try to select unavailable colors - if (!isUnavailable) { - swatch.classList.toggle('active', select); - } - } - }); - } - updateActiveColorPalette(); - } - - function unselectAllPaidColors() { - const swatches = document.querySelectorAll('.wplace-color-swatch'); - if (swatches) { - swatches.forEach((swatch) => { - const colorId = parseInt(swatch.getAttribute('data-color-id'), 10); - if (!isNaN(colorId) && colorId >= 32) { - swatch.classList.toggle('active', false); - } - }); - } - updateActiveColorPalette(); - } - - function initializeColorPalette(container) { - const colorsContainer = container.querySelector('#colors-container'); - const showAllToggle = container.querySelector('#showAllColorsToggle'); - if (!colorsContainer) return; - - // Use already captured colors from state (captured during upload) - // Don't re-fetch colors here, use what was captured when user clicked upload - if (!state.availableColors || state.availableColors.length === 0) { - // If no colors have been captured yet, show message - colorsContainer.innerHTML = `
${Utils.t( - 'uploadImageFirst' - )}
`; - return; - } - - function populateColors(showUnavailable = false) { - colorsContainer.innerHTML = ''; - let availableCount = 0; - let totalCount = 0; - - // Convert COLOR_MAP to array and filter out transparent - const allColors = Object.values(CONFIG.COLOR_MAP).filter((color) => color.rgb !== null); - - allColors.forEach((colorData) => { - const { id, name, rgb } = colorData; - const rgbKey = `${rgb.r},${rgb.g},${rgb.b}`; - totalCount++; - - // Check if this color is available in the captured colors - const isAvailable = state.availableColors.some( - (c) => c.rgb[0] === rgb.r && c.rgb[1] === rgb.g && c.rgb[2] === rgb.b - ); - - // If not showing all colors and this color is not available, skip it - if (!showUnavailable && !isAvailable) { - return; - } - - if (isAvailable) availableCount++; - - const colorItem = Utils.createElement('div', { - className: 'wplace-color-item', - }); - const swatch = Utils.createElement('button', { - className: `wplace-color-swatch ${!isAvailable ? 'unavailable' : ''}`, - title: `${name} (ID: ${id})${!isAvailable ? ' (Unavailable)' : ''}`, - 'data-rgb': rgbKey, - 'data-color-id': id, - }); - swatch.style.backgroundColor = `rgb(${rgb.r}, ${rgb.g}, ${rgb.b})`; - - // Make unavailable colors visually distinct - if (!isAvailable) { - swatch.style.opacity = '0.4'; - swatch.style.filter = 'grayscale(50%)'; - swatch.disabled = true; - } else { - // Select available colors by default - swatch.classList.add('active'); - } - - const nameLabel = Utils.createElement( - 'span', - { - className: 'wplace-color-item-name', - style: !isAvailable ? 'color: #888; font-style: italic;' : '', - }, - name + (!isAvailable ? ' (N/A)' : '') - ); - - // Only add click listener for available colors - if (isAvailable) { - swatch.addEventListener('click', () => { - swatch.classList.toggle('active'); - updateActiveColorPalette(); - }); - } - - colorItem.appendChild(swatch); - colorItem.appendChild(nameLabel); - colorsContainer.appendChild(colorItem); - }); - - updateActiveColorPalette(); - } - - // Initialize with only available colors - populateColors(false); - - // Add toggle functionality - if (showAllToggle) { - showAllToggle.addEventListener('change', (e) => { - populateColors(e.target.checked); - }); - } - - container - .querySelector('#selectAllBtn') - ?.addEventListener('click', () => toggleAllColors(true, showAllToggle?.checked)); - container - .querySelector('#unselectAllBtn') - ?.addEventListener('click', () => toggleAllColors(false, showAllToggle?.checked)); - container - .querySelector('#unselectPaidBtn') - ?.addEventListener('click', () => unselectAllPaidColors()); - } - - async function handleCaptcha(allowGeneration = true) { - const startTime = performance.now(); - - // Check user's token source preference - if (state.tokenSource === 'manual') { - console.log('🎯 Manual token source selected - using pixel placement automation'); - if (!allowGeneration) { - console.log('❌ Token generation disabled during processing'); - return null; - } - return await tokenManager.handleCaptchaFallback(); - } - - // Generator mode (pure) or Hybrid mode - try generator first - try { - // Use optimized token generation with automatic sitekey detection - const { sitekey, token: preGeneratedToken } = await Utils.obtainSitekeyAndToken(); - - if (!sitekey) { - throw new Error('No valid sitekey found'); - } - - console.log('🔑 Generating Turnstile token for sitekey:', sitekey); - console.log( - '🧭 UA:', - navigator.userAgent.substring(0, 50) + '...', - 'Platform:', - navigator.platform - ); - - // Add additional checks before token generation - if (!window.turnstile) { - await Utils.loadTurnstile(); - } - - let token = null; - - // ✅ Reuse pre-generated token if available and valid - if ( - preGeneratedToken && - typeof preGeneratedToken === 'string' && - preGeneratedToken.length > 20 - ) { - console.log('♻️ Reusing pre-generated token from sitekey detection phase'); - token = preGeneratedToken; - } - // ✅ Or use globally cached token if still valid - else if (isTokenValid()) { - console.log('♻️ Using existing cached token (from previous operation)'); - token = getTurnstileToken(); - } - // ✅ Otherwise generate a new one (only if allowed) - else { - if (!allowGeneration) { - console.log('❌ Token expired/missing but generation disabled during processing'); - return null; - } - console.log('🔐 No valid pre-generated or cached token, creating new one...'); - token = await Utils.executeTurnstile(sitekey, 'paint'); - if (token) { - setTurnstileToken(token); - } - } - - console.log( - `🔍 Token received - Type: ${typeof token}, Value: ${token - ? typeof token === 'string' - ? token.length > 50 - ? token.substring(0, 50) + '...' - : token - : JSON.stringify(token) - : 'null/undefined' - }, Length: ${token?.length || 0}` - ); - - // ✅ Final validation - if (typeof token === 'string' && token.length > 20) { - const duration = Math.round(performance.now() - startTime); - console.log(`✅ Turnstile token generated successfully in ${duration}ms`); - return token; - } else { - throw new Error( - `Invalid or empty token received - Type: ${typeof token}, Value: ${JSON.stringify( - token - )}, Length: ${token?.length || 0}` - ); - } - } catch (error) { - const duration = Math.round(performance.now() - startTime); - console.error(`❌ Turnstile token generation failed after ${duration}ms:`, error); - - // Fallback to manual pixel placement for hybrid mode - if (state.tokenSource === 'hybrid') { - console.log( - '🔄 Hybrid mode: Generator failed, automatically switching to manual pixel placement...' - ); - const fbToken = await tokenManager.handleCaptchaFallback(); - return fbToken; - } else { - // Pure generator mode - don't fallback, just fail - throw error; - } - } - } - - async function createUI() { - await detectLanguage(); - - const existingContainer = document.getElementById('wplace-image-bot-container'); - const existingStats = document.getElementById('wplace-stats-container'); - const existingSettings = document.getElementById('wplace-settings-container'); - const existingResizeContainer = document.querySelector('.resize-container'); - const existingResizeOverlay = document.querySelector('.resize-overlay'); - - if (existingContainer) existingContainer.remove(); - if (existingStats) existingStats.remove(); - if (existingSettings) existingSettings.remove(); - if (existingResizeContainer) existingResizeContainer.remove(); - if (existingResizeOverlay) existingResizeOverlay.remove(); - - loadThemePreference(); - await initializeTranslations(); - - const theme = getCurrentTheme(); - applyTheme(); // <- new: set CSS vars and theme class before building UI - - function appendLinkOnce(href, attributes = {}) { - // Check if a link with the same href already exists in the document head - const exists = Array.from(document.head.querySelectorAll('link')).some( - (link) => link.href === href - ); - if (exists) return; - - // Create a new link element - const link = document.createElement('link'); - link.rel = 'stylesheet'; - link.href = href; - - // Add any additional attributes (e.g., data-* attributes) - for (const [key, value] of Object.entries(attributes)) { - link.setAttribute(key, value); - } - - // Append the link element to the document head - document.head.appendChild(link); - } - - appendLinkOnce('https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css'); - - if (theme.fontFamily.includes('Press Start 2P')) { - appendLinkOnce('https://fonts.googleapis.com/css2?family=Press+Start+2P&display=swap'); - } - - // Load auto-image-styles.css - prioritize extension local resources - console.group('%c🎨 Loading Auto-Image Styles', 'color: #8b5cf6; font-weight: bold;'); - - let stylesLoaded = false; - - // First try: Check if extension has loaded local CSS resources - if (window.AUTOBOT_THEMES && window.AUTOBOT_THEMES['auto-image-styles.css']) { - console.log('%c🔍 Found auto-image-styles.css in extension local resources!', 'color: #10b981; font-weight: bold;'); - - // Check if it's already injected by the extension - const existingStyle = document.getElementById('autobot-auto-image-styles'); - if (existingStyle) { - console.log('%c✅ Styles already injected by extension - using local version', 'color: #10b981; font-weight: bold;'); - console.log(' 📍 Source: Extension local file (already in DOM)'); - console.log(' 🆔 Element ID: autobot-auto-image-styles'); - stylesLoaded = true; - } else { - // Inject it ourselves - const styleElement = document.createElement('style'); - styleElement.id = 'autobot-auto-image-styles-script'; - styleElement.setAttribute('data-wplace-theme', 'true'); - styleElement.textContent = window.AUTOBOT_THEMES['auto-image-styles.css']; - document.head.appendChild(styleElement); - - console.log('%c✅ Injected auto-image-styles.css from extension local resources', 'color: #10b981; font-weight: bold;'); - console.log(' 📍 Source: Extension local file'); - console.log(' 📏 Size: ' + window.AUTOBOT_THEMES['auto-image-styles.css'].length + ' characters'); - console.log(' 🆔 Element ID: autobot-auto-image-styles-script'); - stylesLoaded = true; - } - } else { - console.log('%c📝 auto-image-styles.css not found in extension resources', 'color: #f59e0b;'); - console.log(' 🔍 window.AUTOBOT_THEMES:', typeof window.AUTOBOT_THEMES); - if (window.AUTOBOT_THEMES) { - console.log(' 📋 Available themes:', Object.keys(window.AUTOBOT_THEMES)); - } - } - - // Fallback: Load from CDN if local resources not available - if (!stylesLoaded) { - console.log('%c🌐 Falling back to CDN loading...', 'color: #8b5cf6;'); - appendLinkOnce( - 'https://wplace-autobot.github.io/WPlace-AutoBOT/main/auto-image-styles.css', - { 'data-wplace-theme': 'true' } - ); - console.log('%c📚 Loaded auto-image-styles.css from CDN (fallback)', 'color: #f59e0b; font-weight: bold;'); - console.log(' 📍 Source: CDN (wplace-autobot.github.io)'); - console.log(' 🌐 URL: https://wplace-autobot.github.io/WPlace-AutoBOT/main/auto-image-styles.css'); - console.log(' ⚠️ Performance: Network request required'); - } - - console.groupEnd(); - - // Ensure default theme is loaded from extension resources - if (window.applyTheme && typeof window.applyTheme === 'function') { - console.group('%c🎨 Loading Default Theme from Extension', 'color: #8b5cf6; font-weight: bold;'); - - // Determine the current theme file name - let defaultTheme = 'classic'; // fallback - if (CONFIG.currentTheme === 'Neon Retro') { - defaultTheme = 'neon'; - } else if (CONFIG.currentTheme === 'Neon Retro Cyan') { - defaultTheme = 'neon-cyan'; - } else if (CONFIG.currentTheme === 'Neon Retro Light') { - defaultTheme = 'neon-light'; - } else if (CONFIG.currentTheme === 'Classic Light') { - defaultTheme = 'classic-light'; - } else if (CONFIG.currentTheme === 'Acrylic') { - defaultTheme = 'acrylic'; - } - - console.log(`%c🎯 Loading theme: ${defaultTheme} (${CONFIG.currentTheme})`, 'color: #8b5cf6;'); - const success = window.applyTheme(defaultTheme); - - if (success) { - console.log(`%c✅ Default theme loaded from extension local resources`, 'color: #10b981; font-weight: bold;'); - console.log(` 📍 Source: Extension local file (themes/${defaultTheme}.css)`); - console.log(` 🚀 Performance: Instant load (no CDN request)`); - console.log(` 🎨 Theme: ${CONFIG.currentTheme}`); - } else { - console.warn(`%c⚠️ Failed to load theme ${defaultTheme} from extension`, 'color: #f59e0b;'); - console.log(` 📝 Note: Theme CSS classes will still work`); - } - - console.groupEnd(); - } else { - console.warn(`%c⚠️ Extension applyTheme() function not available`, 'color: #f59e0b; font-weight: bold;'); - console.log(` 📝 Themes will be limited to CSS classes only`); - } - - const container = document.createElement('div'); - container.id = 'wplace-image-bot-container'; - container.innerHTML = ` -
-
- - ${Utils.t('title')} -
-
- - - - -
-
-
- -
-
- ${Utils.t('initMessage')} -
-
-
-
-
- - -
-
🖼️ Image Management
-
-
- - -
-
- - -
-
- -
-
-
- - -
-
🎮 Painting Control
-
-
- - -
-
- -
-
-
- - -
-
⏱️ ${Utils.t('cooldownSettings')}
-
- -
-
- -
-
- - - - ${Utils.t('charges')} - -
-
-
-
- - -
-
💾 Data Management
-
-
- - -
-
- - -
-
-
-
- `; - - const statsContainer = document.createElement('div'); - statsContainer.id = 'wplace-stats-container'; - statsContainer.style.display = 'none'; - statsContainer.innerHTML = ` -
-
- - ${Utils.t('paintingStats')} -
-
- - -
-
-
-
-
-
-
${Utils.t('initMessage')}
-
-
-
- -
-
-
- - Account Swapper -
- -
-
- -
-
-
- - Auto Buy Charges -
-
-
- - - -
-
-
- -
-
- - All Accounts - -
-
-
Click the icon to load accounts.
-
-
-
- `; - - function initPillSelector() { - const buttons = statsContainer.querySelectorAll(".pill-btn"); - const highlight = statsContainer.querySelector(".pill-highlight"); - - buttons.forEach((btn, index) => { - btn.addEventListener("click", () => { - highlight.style.transform = `translateX(${index * 100}%)`; - - buttons.forEach((b) => b.classList.remove("active")); - btn.classList.add("active"); - - CONFIG.autoBuy = btn.dataset.mode; - if (CONFIG.autoBuy === 'none') { - console.log("AutoBuy disabled"); - CONFIG.autoBuyToggle = false; - } - else { - CONFIG.autoBuyToggle = true; - console.log("AutoBuy enabled"); - } - console.log("AutoBuy mode set to:", CONFIG.autoBuy); - }); - }); - } - - initPillSelector(); - - // Modern Settings Container with Theme Support - // Use the theme variable already declared at the top of createUI function - const settingsContainer = document.createElement('div'); - settingsContainer.id = 'wplace-settings-container'; - - // Apply theme-based styling - const themeBackground = theme.primary - ? `linear-gradient(135deg, ${theme.primary} 0%, ${theme.secondary || theme.primary} 100%)` - : `linear-gradient(135deg, #667eea 0%, #764ba2 100%)`; - - settingsContainer.className = 'wplace-settings-container-base'; - // Apply theme-specific background - settingsContainer.style.background = themeBackground; - settingsContainer.style.cssText += ` - min-width: 420px; - max-width: 480px; - z-index: 99999; - color: ${theme.text || 'white'}; - font-family: ${theme.fontFamily || "'Segoe UI', Tahoma, Geneva, Verdana, sans-serif"}; - box-shadow: ${theme.boxShadow || '0 20px 40px rgba(0,0,0,0.3), 0 0 0 1px rgba(255,255,255,0.1)' - }; - backdrop-filter: ${theme.backdropFilter || 'blur(10px)'}; - overflow: hidden; - animation: settings-slide-in 0.4s ease-out; - ${theme.animations?.glow - ? ` - box-shadow: ${theme.boxShadow || '0 20px 40px rgba(0,0,0,0.3)'}, - 0 0 30px ${theme.highlight || theme.neon || '#00ffff'}; - ` - : '' - } - `; - - // noinspection CssInvalidFunction - settingsContainer.innerHTML = ` -
-
-

- - ${Utils.t('settings')} -

- -
-
- -
- - -
- -
- -

- Generator mode creates tokens automatically. Hybrid mode falls back to manual when generator fails. Manual mode only uses pixel placement. -

-
-
- - -
- - -
- - -
- -
- -
-
- Overlay Opacity -
${Math.round(state.overlayOpacity * 100)}%
-
- -
- - -
-
- - -
- - -
- - - - - - -
-
- - -
- - - -
- - -
- - -
- - -
- - -
-
- ${Utils.t('batchSize')} -
-
-
- -
-
-
- - - - pixels -
-
-
-
- ${CONFIG.PAINTING_SPEED.MIN} - ${CONFIG.PAINTING_SPEED.MAX} -
-
- - -
-
-
- - -
-
- - -
-
-

- 🎲 Random batch size between min and max values -

-
- - - -
- - -
- - - -
- - -
- - -
- - -
- - -
- -
- - -
-
-
- - -
-
- - -
-
-

- 🧱 Block dimensions for block-based generation modes -

-
-
- - -
- -
- - - -
- ${Utils.t('repeatEvery')} - - ${Utils.t('minutesPl')} -
-
- - -
-
-
- - -
- -
- -
-
- - -
- -
- -
-
-
- - - - - `; - - const resizeContainer = document.createElement('div'); - resizeContainer.className = 'resize-container'; - resizeContainer.innerHTML = ` -

${Utils.t('resizeImage')}

-
- - - - -
- -
- - - - -
- - - - - - - 100% -
- Drag to pan • Pinch to zoom • Double‑tap to zoom -
-
-
- -
-
-
- - -
-
-
-
-
-
-
- -
- - 1 -
-
-
- -
- - 1 -
-
-
-
- -
- - - -
-
- - - Shift = Row • Alt = Column -
-
- -
-
-  Color Palette -
-
-
- -
-
- - - -
-
-
-
- -
-
-  Advanced Color Matching -
-
- - -
-
- ${Utils.t('chromaWeight')} - ${state.chromaPenaltyWeight} -
- -
- -
- - -
- -
-
- -
- - - -
- `; - - const resizeOverlay = document.createElement('div'); - resizeOverlay.className = 'resize-overlay'; - - document.body.appendChild(container); - document.body.appendChild(resizeOverlay); - document.body.appendChild(resizeContainer); - document.body.appendChild(statsContainer); - document.body.appendChild(settingsContainer); - - // Show the main container after all elements are appended - container.style.display = 'block'; - - const uploadBtn = container.querySelector('#uploadBtn'); - const resizeBtn = container.querySelector('#resizeBtn'); - const selectPosBtn = container.querySelector('#selectPosBtn'); - const startBtn = container.querySelector('#startBtn'); - const stopBtn = container.querySelector('#stopBtn'); - const saveBtn = container.querySelector('#saveBtn'); - const loadBtn = container.querySelector('#loadBtn'); - const saveToFileBtn = container.querySelector('#saveToFileBtn'); - const loadFromFileBtn = container.querySelector('#loadFromFileBtn'); - - container.querySelectorAll('.wplace-section-title').forEach((title) => { - // Add a right-side arrow if it doesn't exist - if (!title.querySelector('i.arrow')) { - const arrow = document.createElement('i'); - arrow.className = 'fas fa-chevron-down arrow'; // FontAwesome down arrow - title.appendChild(arrow); - } - - // Click event to toggle collapse/expand of the section - title.addEventListener('click', () => { - const section = title.parentElement; - section.classList.toggle('collapsed'); - }); - }); - - // Disable load/upload buttons until initial setup is complete (startup only) - if (loadBtn) { - loadBtn.disabled = !state.initialSetupComplete; - loadBtn.title = state.initialSetupComplete - ? '' - : '🔄 Waiting for initial setup to complete...'; - } - if (loadFromFileBtn) { - loadFromFileBtn.disabled = !state.initialSetupComplete; - loadFromFileBtn.title = state.initialSetupComplete - ? '' - : '🔄 Waiting for initial setup to complete...'; - } - if (uploadBtn) { - uploadBtn.disabled = !state.initialSetupComplete; - uploadBtn.title = state.initialSetupComplete - ? '' - : '🔄 Waiting for initial setup to complete...'; - } - - const minimizeBtn = container.querySelector('#minimizeBtn'); - const compactBtn = container.querySelector('#compactBtn'); - const statsBtn = container.querySelector('#statsBtn'); - const toggleOverlayBtn = container.querySelector('#toggleOverlayBtn'); - const statusText = container.querySelector('#statusText'); - const progressBar = container.querySelector('#progressBar'); - const statsArea = statsContainer.querySelector('#statsArea'); - const content = container.querySelector('.wplace-content'); - const closeStatsBtn = statsContainer.querySelector('#closeStatsBtn'); - const refreshChargesBtn = statsContainer.querySelector('#refreshChargesBtn'); - const cooldownSlider = container.querySelector('#cooldownSlider'); - const cooldownInput = container.querySelector('#cooldownInput'); - const cooldownDecrease = container.querySelector('#cooldownDecrease'); - const cooldownIncrease = container.querySelector('#cooldownIncrease'); - const cooldownValue = container.querySelector('#cooldownValue'); - - if (!uploadBtn || !selectPosBtn || !startBtn || !stopBtn) { - console.error('Some UI elements not found:', { - uploadBtn: !!uploadBtn, - selectPosBtn: !!selectPosBtn, - startBtn: !!startBtn, - stopBtn: !!stopBtn, - }); - } - - if (!statsContainer || !statsArea || !closeStatsBtn) { - // Note: base CSS now aligns with this layout: main panel at left:20px (width 280), stats at left:330px. - } - - const header = container.querySelector('.wplace-header'); - - makeDraggable(container); - - function makeDraggable(element) { - let pos1 = 0, - pos2 = 0, - pos3 = 0, - pos4 = 0; - let isDragging = false; - const header = - element.querySelector('.wplace-header') || element.querySelector('.wplace-settings-header'); - - if (!header) { - console.warn('No draggable header found for element:', element); - return; - } - - header.onmousedown = dragMouseDown; - - function dragMouseDown(e) { - if (e.target.closest('.wplace-header-btn') || e.target.closest('button')) return; - - e.preventDefault(); - isDragging = true; - - const rect = element.getBoundingClientRect(); - - element.style.transform = 'none'; - element.style.top = rect.top + 'px'; - element.style.left = rect.left + 'px'; - - pos3 = e.clientX; - pos4 = e.clientY; - element.classList.add('wplace-dragging'); - document.onmouseup = closeDragElement; - document.onmousemove = elementDrag; - - document.body.style.userSelect = 'none'; - } - - function elementDrag(e) { - if (!isDragging) return; - - e.preventDefault(); - pos1 = pos3 - e.clientX; - pos2 = pos4 - e.clientY; - pos3 = e.clientX; - pos4 = e.clientY; - - let newTop = element.offsetTop - pos2; - let newLeft = element.offsetLeft - pos1; - - const rect = element.getBoundingClientRect(); - const maxTop = window.innerHeight - rect.height; - const maxLeft = window.innerWidth - rect.width; - - newTop = Math.max(0, Math.min(newTop, maxTop)); - newLeft = Math.max(0, Math.min(newLeft, maxLeft)); - - element.style.top = newTop + 'px'; - element.style.left = newLeft + 'px'; - } - - function closeDragElement() { - isDragging = false; - element.classList.remove('wplace-dragging'); - document.onmouseup = null; - document.onmousemove = null; - document.body.style.userSelect = ''; - } - } - - makeDraggable(statsContainer); - makeDraggable(container); - - if (statsBtn && closeStatsBtn) { - statsBtn.addEventListener('click', () => { - const isVisible = statsContainer.style.display !== 'none'; - if (isVisible) { - statsContainer.style.display = 'none'; - statsBtn.innerHTML = ''; - statsBtn.title = Utils.t('showStats'); - } else { - statsContainer.style.display = 'block'; - statsBtn.innerHTML = ''; - statsBtn.title = Utils.t('hideStats'); - } - }); - - closeStatsBtn.addEventListener('click', () => { - statsContainer.style.display = 'none'; - statsBtn.innerHTML = ''; - statsBtn.title = Utils.t('showStats'); - }); - - if (refreshChargesBtn) { - refreshChargesBtn.addEventListener('click', async () => { - refreshChargesBtn.innerHTML = ''; - refreshChargesBtn.disabled = true; - - try { - await updateStats(); - await updateCurrentAccountInList(); - } catch (error) { - console.error('Error refreshing charges:', error); - } finally { - refreshChargesBtn.innerHTML = ''; - refreshChargesBtn.disabled = false; - } - }); - } - - // Account-related event handlers - const autoSwapToggle = statsContainer.querySelector('#autoSwapToggle'); - const refreshAllAccountsBtn = statsContainer.querySelector('#refreshAllAccountsBtn'); - - if (autoSwapToggle) { - autoSwapToggle.checked = CONFIG.autoSwap; - autoSwapToggle.addEventListener('change', (e) => { - CONFIG.autoSwap = e.target.checked; - console.log(`🔄 Auto-swap ${CONFIG.autoSwap ? 'enabled' : 'disabled'}`); - - // Handle autoBuy toggle dependency - const autoBuyToggle = statsContainer.querySelector('#autoBuyToggle'); - if (autoBuyToggle) { - if (!CONFIG.autoSwap) { - // If autoSwap is disabled, disable and uncheck autoBuy - autoBuyToggle.disabled = true; - autoBuyToggle.checked = false; - CONFIG.autoBuyToggle = false; - console.log(`💰 Auto-buy disabled (requires auto-swap)`); - } else { - // If autoSwap is enabled, enable autoBuy toggle - autoBuyToggle.disabled = false; - } - } - }); - } - - // Auto Buy toggle logic - const autoBuyToggle = statsContainer.querySelector('#autoBuyToggle'); - if (autoBuyToggle) { - autoBuyToggle.checked = CONFIG.autoBuyToggle; - autoBuyToggle.disabled = !CONFIG.autoSwap; // Disable if autoSwap is off - - autoBuyToggle.addEventListener('change', (e) => { - CONFIG.autoBuyToggle = e.target.checked; - console.log(`💰 Auto-buy ${CONFIG.autoBuyToggle ? 'enabled' : 'disabled'}`); - }); - } - - if (refreshAllAccountsBtn) { - refreshAllAccountsBtn.addEventListener('click', fetchAllAccountDetails); - } - } - if (statsContainer && statsBtn) { - // Stats container starts visible by default - user clicks button to hide - statsContainer.style.display = 'block'; - statsBtn.innerHTML = ''; - statsBtn.title = Utils.t('hideStats'); - } - - const settingsBtn = container.querySelector('#settingsBtn'); - const closeSettingsBtn = settingsContainer.querySelector('#closeSettingsBtn'); - const applySettingsBtn = settingsContainer.querySelector('#applySettingsBtn'); - - if (settingsBtn && closeSettingsBtn && applySettingsBtn) { - settingsBtn.addEventListener('click', () => { - const isVisible = settingsContainer.classList.contains('show'); - if (isVisible) { - settingsContainer.style.animation = 'settings-fade-out 0.3s ease-out forwards'; - settingsContainer.classList.remove('show'); - setTimeout(() => { - settingsContainer.style.animation = ''; - }, 300); - } else { - settingsContainer.style.top = '50%'; - settingsContainer.style.left = '50%'; - settingsContainer.style.transform = 'translate(-50%, -50%)'; - settingsContainer.classList.add('show'); - settingsContainer.style.animation = 'settings-slide-in 0.4s ease-out'; - } - }); - - closeSettingsBtn.addEventListener('click', () => { - settingsContainer.style.animation = 'settings-fade-out 0.3s ease-out forwards'; - settingsContainer.classList.remove('show'); - setTimeout(() => { - settingsContainer.style.animation = ''; - settingsContainer.style.top = '50%'; - settingsContainer.style.left = '50%'; - settingsContainer.style.transform = 'translate(-50%, -50%)'; - }, 300); - }); - - applySettingsBtn.addEventListener('click', () => { - // Sync advanced settings before save - const colorAlgorithmSelect = document.getElementById('colorAlgorithmSelect'); - if (colorAlgorithmSelect) state.colorMatchingAlgorithm = colorAlgorithmSelect.value; - const enableChromaPenaltyToggle = document.getElementById('enableChromaPenaltyToggle'); - if (enableChromaPenaltyToggle) - state.enableChromaPenalty = enableChromaPenaltyToggle.checked; - const chromaPenaltyWeightSlider = document.getElementById('chromaPenaltyWeightSlider'); - if (chromaPenaltyWeightSlider) - state.chromaPenaltyWeight = parseFloat(chromaPenaltyWeightSlider.value) || 0.15; - const transparencyThresholdInput = document.getElementById('transparencyThresholdInput'); - if (transparencyThresholdInput) { - const v = parseInt(transparencyThresholdInput.value, 10); - if (!isNaN(v) && v >= 0 && v <= 255) state.customTransparencyThreshold = v; - } - const whiteThresholdInput = document.getElementById('whiteThresholdInput'); - if (whiteThresholdInput) { - const v = parseInt(whiteThresholdInput.value, 10); - if (!isNaN(v) && v >= 200 && v <= 255) state.customWhiteThreshold = v; - } - // Update functional thresholds - CONFIG.TRANSPARENCY_THRESHOLD = state.customTransparencyThreshold; - CONFIG.WHITE_THRESHOLD = state.customWhiteThreshold; - // Notifications - const notifEnabledToggle = document.getElementById('notifEnabledToggle'); - const notifOnChargesToggle = document.getElementById('notifOnChargesToggle'); - const notifOnlyUnfocusedToggle = document.getElementById('notifOnlyUnfocusedToggle'); - const notifIntervalInput = document.getElementById('notifIntervalInput'); - if (notifEnabledToggle) state.notificationsEnabled = !!notifEnabledToggle.checked; - if (notifOnChargesToggle) state.notifyOnChargesReached = !!notifOnChargesToggle.checked; - if (notifOnlyUnfocusedToggle) - state.notifyOnlyWhenUnfocused = !!notifOnlyUnfocusedToggle.checked; - if (notifIntervalInput) { - const v = parseInt(notifIntervalInput.value, 10); - if (!isNaN(v) && v >= 1 && v <= 60) state.notificationIntervalMinutes = v; - } - saveBotSettings(); - Utils.showAlert(Utils.t('settingsSaved'), 'success'); - closeSettingsBtn.click(); - NotificationManager.syncFromState(); - }); - - makeDraggable(settingsContainer); - - const tokenSourceSelect = settingsContainer.querySelector('#tokenSourceSelect'); - if (tokenSourceSelect) { - tokenSourceSelect.addEventListener('change', (e) => { - state.tokenSource = e.target.value; - saveBotSettings(); - console.log(`🔑 Token source changed to: ${state.tokenSource}`); - const sourceNames = { - generator: 'Automatic Generator', - hybrid: 'Generator + Auto Fallback', - manual: 'Manual Pixel Placement', - }; - Utils.showAlert( - Utils.t('tokenSourceSet', { source: sourceNames[state.tokenSource] }), - 'success' - ); - }); - } - - // Painting order controls - const paintingOrderSelect = settingsContainer.querySelector('#paintingOrderSelect'); - if (paintingOrderSelect) { - paintingOrderSelect.addEventListener('change', (e) => { - state.paintingOrder = e.target.value; - saveBotSettings(); - console.log(`🎨 Painting order changed to: ${state.paintingOrder}`); - const orderNames = { - sequential: Utils.t('paintingOrderSequential'), - 'color-by-color': Utils.t('paintingOrderColorByColor'), - }; - Utils.showAlert( - `Painting order set to: ${orderNames[state.paintingOrder]}`, - 'success' - ); - }); - } - - // Batch mode controls - const batchModeSelect = settingsContainer.querySelector('#batchModeSelect'); - const normalBatchControls = settingsContainer.querySelector('#normalBatchControls'); - const randomBatchControls = settingsContainer.querySelector('#randomBatchControls'); - const randomBatchMin = settingsContainer.querySelector('#randomBatchMin'); - const randomBatchMax = settingsContainer.querySelector('#randomBatchMax'); - - if (batchModeSelect) { - batchModeSelect.addEventListener('change', (e) => { - state.batchMode = e.target.value; - - // Switch between normal and random controls - if (normalBatchControls && randomBatchControls) { - if (e.target.value === 'random') { - normalBatchControls.style.display = 'none'; - randomBatchControls.style.display = 'block'; - } else { - normalBatchControls.style.display = 'block'; - randomBatchControls.style.display = 'none'; - } - } - - saveBotSettings(); - console.log(`📦 Batch mode changed to: ${state.batchMode}`); - Utils.showAlert( - Utils.t('batchModeSet', { - mode: - state.batchMode === 'random' ? Utils.t('randomRange') : Utils.t('normalFixedSize'), - }), - 'success' - ); - }); - } - - if (randomBatchMin) { - randomBatchMin.addEventListener('input', (e) => { - const min = parseInt(e.target.value); - if (min >= 1 && min <= 1000) { - state.randomBatchMin = min; - // Ensure min doesn't exceed max - if (randomBatchMax && min > state.randomBatchMax) { - state.randomBatchMax = min; - randomBatchMax.value = min; - } - saveBotSettings(); - } - }); - } - - if (randomBatchMax) { - randomBatchMax.addEventListener('input', (e) => { - const max = parseInt(e.target.value); - if (max >= 1 && max <= 1000) { - state.randomBatchMax = max; - // Ensure max doesn't go below min - if (randomBatchMin && max < state.randomBatchMin) { - state.randomBatchMin = max; - randomBatchMin.value = max; - } - saveBotSettings(); - } - }); - } - - const languageSelect = settingsContainer.querySelector('#languageSelect'); - if (languageSelect) { - languageSelect.addEventListener('change', async (e) => { - const newLanguage = e.target.value; - state.language = newLanguage; - localStorage.setItem('wplace_language', newLanguage); - - // Load the new language translations - await loadTranslations(newLanguage); - - setTimeout(() => { - settingsContainer.style.display = 'none'; - createUI(); - }, 100); - }); - } - - const themeSelect = settingsContainer.querySelector('#themeSelect'); - if (themeSelect) { - themeSelect.addEventListener('change', (e) => { - const newTheme = e.target.value; - switchTheme(newTheme); - }); - } - - const overlayOpacitySlider = settingsContainer.querySelector('#overlayOpacitySlider'); - const overlayOpacityValue = settingsContainer.querySelector('#overlayOpacityValue'); - const enableBlueMarbleToggle = settingsContainer.querySelector('#enableBlueMarbleToggle'); - const settingsPaintWhiteToggle = settingsContainer.querySelector('#settingsPaintWhiteToggle'); - const settingsPaintTransparentToggle = settingsContainer.querySelector( - '#settingsPaintTransparentToggle' - ); - - if (overlayOpacitySlider && overlayOpacityValue) { - const updateOpacity = (newValue) => { - const opacity = parseFloat(newValue); - state.overlayOpacity = opacity; - overlayOpacitySlider.value = opacity; - overlayOpacityValue.textContent = `${Math.round(opacity * 100)}%`; - }; - - overlayOpacitySlider.addEventListener('input', (e) => { - updateOpacity(e.target.value); - }); - - // Add scroll-to-adjust for overlay opacity slider - Utils.createScrollToAdjust(overlayOpacitySlider, updateOpacity, 0, 1, 0.05); - } - - if (settingsPaintWhiteToggle) { - settingsPaintWhiteToggle.checked = state.paintWhitePixels; - settingsPaintWhiteToggle.addEventListener('change', (e) => { - state.paintWhitePixels = e.target.checked; - saveBotSettings(); - console.log(`🎨 Paint white pixels: ${state.paintWhitePixels ? 'ON' : 'OFF'}`); - const statusText = state.paintWhitePixels - ? 'White pixels in the template will be painted' - : 'White pixels will be skipped'; - Utils.showAlert(statusText, 'success'); - }); - } - - if (settingsPaintTransparentToggle) { - settingsPaintTransparentToggle.checked = state.paintTransparentPixels; - settingsPaintTransparentToggle.addEventListener('change', (e) => { - state.paintTransparentPixels = e.target.checked; - saveBotSettings(); - console.log( - `🎨 Paint transparent pixels: ${state.paintTransparentPixels ? 'ON' : 'OFF'}` - ); - const statusText = state.paintTransparentPixels - ? 'Transparent pixels in the template will be painted with the closest available color' - : 'Transparent pixels will be skipped'; - Utils.showAlert(statusText, 'success'); - }); - } - - // Speed controls - both slider and input - const speedSlider = settingsContainer.querySelector('#speedSlider'); - const speedInput = settingsContainer.querySelector('#speedInput'); - const speedDecrease = settingsContainer.querySelector('#speedDecrease'); - const speedIncrease = settingsContainer.querySelector('#speedIncrease'); - const speedValue = settingsContainer.querySelector('#speedValue'); - - if (speedSlider && speedInput && speedValue && speedDecrease && speedIncrease) { - const updateSpeed = (newValue) => { - const speed = Math.max(CONFIG.PAINTING_SPEED.MIN, Math.min(CONFIG.PAINTING_SPEED.MAX, parseInt(newValue))); - state.paintingSpeed = speed; - - // Update both controls (value shows in input, label shows unit only) - speedSlider.value = speed; - speedInput.value = speed; - speedValue.textContent = `pixels`; - - saveBotSettings(); - }; - - // Slider event listener - speedSlider.addEventListener('input', (e) => { - updateSpeed(e.target.value); - }); - - // Number input event listener - speedInput.addEventListener('input', (e) => { - updateSpeed(e.target.value); - }); - - // Decrease button - speedDecrease.addEventListener('click', () => { - updateSpeed(parseInt(speedInput.value) - 1); - }); - - // Increase button - speedIncrease.addEventListener('click', () => { - updateSpeed(parseInt(speedInput.value) + 1); - }); - - // Add scroll-to-adjust for speed slider - Utils.createScrollToAdjust(speedSlider, updateSpeed, CONFIG.PAINTING_SPEED.MIN, CONFIG.PAINTING_SPEED.MAX, 1); - } - - if (enableBlueMarbleToggle) { - enableBlueMarbleToggle.addEventListener('click', async () => { - state.blueMarbleEnabled = enableBlueMarbleToggle.checked; - if (state.imageLoaded && overlayManager.imageBitmap) { - Utils.showAlert(Utils.t('reprocessingOverlay'), 'info'); - await overlayManager.processImageIntoChunks(); - Utils.showAlert(Utils.t('overlayUpdated'), 'success'); - } - }); - } - - // Speed control toggle - const enableSpeedToggle = settingsContainer.querySelector('#enableSpeedToggle'); - if (enableSpeedToggle) { - enableSpeedToggle.addEventListener('change', (e) => { - CONFIG.PAINTING_SPEED_ENABLED = e.target.checked; - saveBotSettings(); - console.log(`⚡ Batch speed control: ${CONFIG.PAINTING_SPEED_ENABLED ? 'ON' : 'OFF'}`); - const statusText = CONFIG.PAINTING_SPEED_ENABLED - ? 'Batch speed control enabled - will use configured batch size' - : 'Batch speed control disabled - will use all available charges immediately'; - Utils.showAlert(statusText, 'success'); - }); - } - - // (Advanced color listeners moved outside to work with resize dialog) - // (Advanced color listeners moved outside to work with resize dialog) - // Notifications listeners - const notifPermBtn = settingsContainer.querySelector('#notifRequestPermBtn'); - const notifTestBtn = settingsContainer.querySelector('#notifTestBtn'); - if (notifPermBtn) { - notifPermBtn.addEventListener('click', async () => { - const perm = await NotificationManager.requestPermission(); - if (perm === 'granted') Utils.showAlert(Utils.t('notificationsEnabled'), 'success'); - else Utils.showAlert(Utils.t('notificationsPermissionDenied'), 'warning'); - }); - } - if (notifTestBtn) { - notifTestBtn.addEventListener('click', () => { - NotificationManager.notify( - Utils.t('testNotificationTitle'), - Utils.t('testNotificationMessage'), - 'wplace-notify-test', - true - ); - }); - } - } - - const widthSlider = resizeContainer.querySelector('#widthSlider'); - const heightSlider = resizeContainer.querySelector('#heightSlider'); - const widthValue = resizeContainer.querySelector('#widthValue'); - const heightValue = resizeContainer.querySelector('#heightValue'); - const keepAspect = resizeContainer.querySelector('#keepAspect'); - const paintWhiteToggle = resizeContainer.querySelector('#paintWhiteToggle'); - const paintTransparentToggle = resizeContainer.querySelector('#paintTransparentToggle'); - const zoomSlider = resizeContainer.querySelector('#zoomSlider'); - const zoomValue = resizeContainer.querySelector('#zoomValue'); - const zoomInBtn = resizeContainer.querySelector('#zoomInBtn'); - const zoomOutBtn = resizeContainer.querySelector('#zoomOutBtn'); - const zoomFitBtn = resizeContainer.querySelector('#zoomFitBtn'); - const zoomActualBtn = resizeContainer.querySelector('#zoomActualBtn'); - const panModeBtn = resizeContainer.querySelector('#panModeBtn'); - const panStage = resizeContainer.querySelector('#resizePanStage'); - const canvasStack = resizeContainer.querySelector('#resizeCanvasStack'); - const baseCanvas = resizeContainer.querySelector('#resizeCanvas'); - const maskCanvas = resizeContainer.querySelector('#maskCanvas'); - const baseCtx = baseCanvas.getContext('2d'); - const maskCtx = maskCanvas.getContext('2d'); - const confirmResize = resizeContainer.querySelector('#confirmResize'); - const cancelResize = resizeContainer.querySelector('#cancelResize'); - const editImageBtn = resizeContainer.querySelector('#editImageBtn'); - const downloadPreviewBtn = resizeContainer.querySelector('#downloadPreviewBtn'); - const clearIgnoredBtn = resizeContainer.querySelector('#clearIgnoredBtn'); - - // Coordinate generation controls with smart visibility - const coordinateModeSelect = settingsContainer.querySelector('#coordinateModeSelect'); - const coordinateDirectionSelect = settingsContainer.querySelector('#coordinateDirectionSelect'); - const coordinateSnakeToggle = settingsContainer.querySelector('#coordinateSnakeToggle'); - const directionControls = settingsContainer.querySelector('#directionControls'); - const snakeControls = settingsContainer.querySelector('#snakeControls'); - const blockControls = settingsContainer.querySelector('#blockControls'); - const blockWidthInput = settingsContainer.querySelector('#blockWidthInput'); - const blockHeightInput = settingsContainer.querySelector('#blockHeightInput'); - const paintUnavailablePixelsToggle = settingsContainer.querySelector( - '#paintUnavailablePixelsToggle' - ); - - if (paintUnavailablePixelsToggle) { - paintUnavailablePixelsToggle.checked = state.paintUnavailablePixels; - paintUnavailablePixelsToggle.addEventListener('change', (e) => { - state.paintUnavailablePixels = e.target.checked; - saveBotSettings(); - console.log(`🎨 Paint unavailable colors: ${state.paintUnavailablePixels ? 'ON' : 'OFF'}`); - const statusText = state.paintUnavailablePixels - ? 'Unavailable template colors will be painted with the closest available color' - : 'Unavailable template colors will be skipped'; - Utils.showAlert(statusText, 'success'); - }); - } - if (coordinateModeSelect) { - coordinateModeSelect.value = state.coordinateMode; - coordinateModeSelect.addEventListener('change', (e) => { - state.coordinateMode = e.target.value; - Utils.updateCoordinateUI({ - mode: state.coordinateMode, - directionControls, - snakeControls, - blockControls, - }); - saveBotSettings(); - console.log(`🔄 Coordinate mode changed to: ${state.coordinateMode}`); - Utils.showAlert(`Coordinate mode set to: ${state.coordinateMode}`, 'success'); - }); - } - - if (coordinateDirectionSelect) { - coordinateDirectionSelect.value = state.coordinateDirection; - coordinateDirectionSelect.addEventListener('change', (e) => { - state.coordinateDirection = e.target.value; - saveBotSettings(); - console.log(`🧭 Coordinate direction changed to: ${state.coordinateDirection}`); - Utils.showAlert(`Coordinate direction set to: ${state.coordinateDirection}`, 'success'); - }); - } - - if (coordinateSnakeToggle) { - coordinateSnakeToggle.checked = state.coordinateSnake; - coordinateSnakeToggle.addEventListener('change', (e) => { - state.coordinateSnake = e.target.checked; - saveBotSettings(); - console.log(`🐍 Snake pattern ${state.coordinateSnake ? 'enabled' : 'disabled'}`); - Utils.showAlert( - `Snake pattern ${state.coordinateSnake ? 'enabled' : 'disabled'}`, - 'success' - ); - }); - } - - if (blockWidthInput) { - blockWidthInput.value = state.blockWidth; - blockWidthInput.addEventListener('input', (e) => { - const width = parseInt(e.target.value); - if (width >= 1 && width <= 50) { - state.blockWidth = width; - saveBotSettings(); - } - }); - } - - if (blockHeightInput) { - blockHeightInput.value = state.blockHeight; - blockHeightInput.addEventListener('change', (e) => { - const height = parseInt(e.target.value); - if (height >= 1 && height <= 50) { - state.blockHeight = height; - saveBotSettings(); - } - }); - } - - if (compactBtn) { - compactBtn.addEventListener('click', () => { - container.classList.toggle('wplace-compact'); - const isCompact = container.classList.contains('wplace-compact'); - - if (isCompact) { - compactBtn.innerHTML = ''; - compactBtn.title = Utils.t('expandMode'); - } else { - compactBtn.innerHTML = ''; - compactBtn.title = Utils.t('compactMode'); - } - }); - } - - if (minimizeBtn) { - minimizeBtn.addEventListener('click', () => { - state.minimized = !state.minimized; - if (state.minimized) { - container.classList.add('wplace-minimized'); - content.classList.add('wplace-hidden'); - minimizeBtn.innerHTML = ''; - minimizeBtn.title = Utils.t('restore'); - } else { - container.classList.remove('wplace-minimized'); - content.classList.remove('wplace-hidden'); - minimizeBtn.innerHTML = ''; - minimizeBtn.title = Utils.t('minimize'); - } - saveBotSettings(); - }); - } - - if (toggleOverlayBtn) { - toggleOverlayBtn.addEventListener('click', () => { - const isEnabled = overlayManager.toggle(); - toggleOverlayBtn.classList.toggle('active', isEnabled); - toggleOverlayBtn.setAttribute('aria-pressed', isEnabled ? 'true' : 'false'); - Utils.showAlert(isEnabled ? Utils.t('overlayEnabled') : Utils.t('overlayDisabled'), 'info'); - }); - } - - if (state.minimized) { - container.classList.add('wplace-minimized'); - content.classList.add('wplace-hidden'); - if (minimizeBtn) { - minimizeBtn.innerHTML = ''; - minimizeBtn.title = Utils.t('restore'); - } - } else { - container.classList.remove('wplace-minimized'); - content.classList.remove('wplace-hidden'); - if (minimizeBtn) { - minimizeBtn.innerHTML = ''; - minimizeBtn.title = Utils.t('minimize'); - } - } - - if (saveBtn) { - saveBtn.addEventListener('click', () => { - if (!state.imageLoaded) { - Utils.showAlert(Utils.t('missingRequirements'), 'error'); - return; - } - - const success = Utils.saveProgress(); - if (success) { - updateUI('autoSaved', 'success'); - Utils.showAlert(Utils.t('autoSaved'), 'success'); - } else { - Utils.showAlert(Utils.t('errorSavingProgress'), 'error'); - } - }); - } - - if (loadBtn) { - loadBtn.addEventListener('click', async () => { - const savedData = Utils.loadProgress(); - if (!savedData) { - updateUI('noSavedData', 'warning'); - Utils.showAlert(Utils.t('noSavedData'), 'warning'); - return; - } - - // 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; - - if (!state.initialSetupComplete && !hasCompleteData) { - Utils.showAlert(Utils.t('pleaseWaitInitialSetup'), 'warning'); - return; - } - - const confirmLoad = confirm( - `${Utils.t('savedDataFound')}\n\n` + - `Saved: ${new Date(savedData.timestamp).toLocaleString()}\n` + - `Progress: ${savedData.state.paintedPixels}/${savedData.state.totalPixels} pixels` - ); - - if (confirmLoad) { - const success = Utils.restoreProgress(savedData); - if (success) { - // CRITICAL FIX: Set initial setup complete if save had complete data - if (hasCompleteData) { - state.initialSetupComplete = true; - } - - updateUI('dataLoaded', 'success'); - Utils.showAlert(Utils.t('dataLoaded'), 'success'); - updateDataButtons(); - - // CRITICAL FIX: Force immediate stats update after save loading - 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(async () => { - await updateStats(); - }, 100); - } - - // Restore overlay if image data was loaded from localStorage - Utils.restoreOverlayFromData().catch((error) => { - console.error('Failed to restore overlay from localStorage:', error); - }); - - 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; - } - - if (state.imageLoaded && state.startPosition && state.region && state.colorsChecked) { - startBtn.disabled = false; - } - } else { - Utils.showAlert(Utils.t('errorLoadingProgress'), 'error'); - } - } - }); - } - - if (saveToFileBtn) { - saveToFileBtn.addEventListener('click', () => { - const success = Utils.saveProgressToFile(); - if (success) { - updateUI('fileSaved', 'success'); - Utils.showAlert(Utils.t('fileSaved'), 'success'); - } else { - Utils.showAlert(Utils.t('fileError'), 'error'); - } - }); - } - - 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; - - 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; - } - - updateUI('fileLoaded', 'success'); - 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('❌ [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 { - console.error('❌ [DEBUG] General file error:', error.message); - Utils.showAlert(`File error: ${error.message}`, 'error'); - } - } - }); - } - - updateUI = (messageKey, type = 'default', params = {}, silent = false) => { - const message = Utils.t(messageKey, params); - statusText.textContent = message; - statusText.className = `wplace-status status-${type}`; - - if (!silent) { - // Trigger animation only when silent = false - statusText.style.animation = 'none'; - void statusText.offsetWidth; // trick to restart the animation - statusText.style.animation = 'slide-in 0.3s ease-out'; - } - }; - - function updateChargeStatsDisplay(intervalMs) { - const currentChargesEl = document.getElementById('wplace-stat-charges-value'); - const fullChargeEl = document.getElementById('wplace-stat-fullcharge-value'); - if (!fullChargeEl && !currentChargesEl) return; - if (!state.fullChargeData) { - fullChargeEl.textContent = '--:--:--'; - return; - } - - const { current, max, cooldownMs, startTime, spentSinceShot } = state.fullChargeData; - const elapsed = Date.now() - startTime; - - // total charges including elapsed time and spent during painting since snapshot - const chargesGained = elapsed / cooldownMs; - const rawCharges = current + chargesGained - spentSinceShot; - const cappedCharges = Math.min(rawCharges, max); - - // rounding with 0.95 threshold - let displayCharges; - const fraction = cappedCharges - Math.floor(cappedCharges); - if (fraction >= 0.95) { - displayCharges = Math.ceil(cappedCharges); - } else { - displayCharges = Math.floor(cappedCharges); - } - - state.displayCharges = Math.max(0, displayCharges); - state.preciseCurrentCharges = cappedCharges; - - const remainingMs = getMsToTargetCharges(cappedCharges, max, state.cooldown, intervalMs); - const timeText = Utils.msToTimeText(remainingMs); - - if (currentChargesEl) { - currentChargesEl.innerHTML = `${state.displayCharges} / ${state.maxCharges}`; - } - - if ( - state.displayCharges < state.cooldownChargeThreshold && - !state.stopFlag && - state.running - ) { - updateChargesThresholdUI(intervalMs); - } - - if (fullChargeEl) { - if (state.displayCharges >= max) { - fullChargeEl.innerHTML = `FULL`; - } else { - fullChargeEl.innerHTML = ` - ${timeText} - `; - } - } - } - - updateStats = async (isManualRefresh = false) => { - const isFirstCheck = !state.fullChargeData?.startTime; - - const minUpdateInterval = 60_000; - const maxUpdateInterval = 90_000; - const randomUpdateThreshold = - minUpdateInterval + Math.random() * (maxUpdateInterval - minUpdateInterval); - const timeSinceLastUpdate = Date.now() - (state.fullChargeData?.startTime || 0); - const isTimeToUpdate = timeSinceLastUpdate >= randomUpdateThreshold; - - const shouldCallApi = isFirstCheck || isTimeToUpdate; - - 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(); - - if (state.fullChargeInterval) { - clearInterval(state.fullChargeInterval); - state.fullChargeInterval = null; - } - const intervalMs = 1000; - state.fullChargeInterval = setInterval( - () => updateChargeStatsDisplay(intervalMs), - intervalMs - ); - - if (cooldownSlider && cooldownSlider.max !== state.maxCharges) { - cooldownSlider.max = state.maxCharges; - } - if (cooldownInput && cooldownInput.max !== state.maxCharges) { - cooldownInput.max = state.maxCharges; - } - - // // 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 { - batchSize = typeof calculateBatchSize === 'function' ? calculateBatchSize() : state.paintingSpeed || 5; - } 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}%`; - - imageStatsHTML = ` -
-
${Utils.t('progress')}
-
${progress}%
-
-
-
${Utils.t( - 'pixels' - )}
-
${state.paintedPixels}/${state.totalPixels}
-
- `; - } - - let colorSwatchesHTML = ''; - state.availableColors = state.availableColors.filter( - (c) => c.name !== 'Unknown CoIor NaN' && c.id !== null - ); - - const availableColors = Utils.extractAvailableColors(); - const newCount = Array.isArray(availableColors) ? availableColors.length : 0; - - if (newCount === 0 && isManualRefresh) { - Utils.showAlert(Utils.t('noColorsFound'), 'warning'); - } else if (newCount > 0 && state.availableColors.length < newCount) { - const oldCount = state.availableColors.length; - - Utils.showAlert( - Utils.t('colorsUpdated', { - oldCount, - newCount: newCount, - diffCount: newCount - oldCount, - }), - 'success' - ); - state.availableColors = availableColors; - } - if (state.colorsChecked) { - colorSwatchesHTML = state.availableColors - .map((color) => { - const rgbString = `rgb(${color.rgb.join(',')})`; - return `
`; - }) - .join(''); - } - - // Calculate multi-account statistics - let totalAllCharges = 0; - let totalMaxCharges = 0; - const accounts = accountManager.getAllAccounts(); - if (accounts.length > 0) { - // Use real-time charges for current account, stored data for others - const currentAccount = accountManager.getCurrentAccount(); - totalAllCharges = accounts.reduce((sum, acc) => { - if (currentAccount && acc.token === currentAccount.token) { - // Use real-time data for current account - return sum + Math.floor(state.displayCharges || state.preciseCurrentCharges || 0); - } else { - // Use stored data for other accounts - return sum + Math.floor(acc.Charges || 0); - } - }, 0); - totalMaxCharges = accounts.reduce((sum, acc) => { - if (currentAccount && acc.token === currentAccount.token) { - // Use real-time max charges for current account - return sum + Math.floor(state.maxCharges || acc.Max || 0); - } else { - // Use stored data for other accounts - return sum + Math.floor(acc.Max || 0); - } - }, 0); - } - - statsArea.innerHTML = ` - ${imageStatsHTML} - ${accounts.length > 0 ? ` -
-
- Total All Accounts Charges -
-
- ${totalAllCharges}/${totalMaxCharges} -
-
- ` : ''} -
-
- ${Utils.t('charges')} -
-
- ${state.displayCharges} / ${state.maxCharges} -
-
-
-
- ${Utils.t('fullChargeIn')} -
-
--:--:--
-
- ${state.colorsChecked - ? ` -
-
${Utils.t( - 'availableColors', - { count: state.availableColors.length } - )}
-
- ${colorSwatchesHTML} -
-
- ` - : '' - } - `; - - // should be after statsArea.innerHTML = '...'. todo make full stats ui update partial - updateChargeStatsDisplay(intervalMs); - }; - - updateDataButtons = () => { - const hasImageData = state.imageLoaded && state.imageData; - saveBtn.disabled = !hasImageData; - saveToFileBtn.disabled = !hasImageData; - }; - - updateDataButtons(); - - function showMoveArtworkPanel() { - // Create move artwork control panel like status panels - const movePanel = document.createElement('div'); - movePanel.id = 'moveArtworkPanel'; - movePanel.className = 'wplace-move-panel'; - - movePanel.innerHTML = ` -
-
- - Move Artwork -
-
- -
-
-
-
- -
- -
1px
- -
- -
-
- `; - - // No overlay - append directly to body like other panels - document.body.appendChild(movePanel); - - // Make the panel draggable - makeDraggable(movePanel); - - // Add event listeners for movement - let currentMessageTimeout = null; - - function moveArtwork(deltaX, deltaY) { - if (state.startPosition && state.region) { - const newX = state.startPosition.x + deltaX; - const newY = state.startPosition.y + deltaY; - - console.log(`🔄 Moving artwork from (${state.startPosition.x}, ${state.startPosition.y}) to (${newX}, ${newY})`); - - state.startPosition.x = newX; - state.startPosition.y = newY; - - // Clear any existing message timeout and alerts immediately - if (currentMessageTimeout) { - clearTimeout(currentMessageTimeout); - currentMessageTimeout = null; - } - Utils.hideAlert(); // Hide current alert immediately - - // Update overlay position if available - if (overlayManager) { - overlayManager.setPosition(state.startPosition, state.region) - .then(() => { - console.log('✅ Overlay position updated'); - Utils.showAlert(`Moved to position (${newX}, ${newY})`, 'success'); - // Set new timeout for this message - currentMessageTimeout = setTimeout(() => { - Utils.hideAlert(); - currentMessageTimeout = null; - }, 1500); // Message disappears after 1.5 seconds - }) - .catch(err => { - console.error('❌ Failed to update overlay:', err); - Utils.showAlert('Failed to update overlay position', 'error'); - }); - } else { - Utils.showAlert(`Position updated to (${newX}, ${newY})`, 'success'); - // Set new timeout for this message - currentMessageTimeout = setTimeout(() => { - Utils.hideAlert(); - currentMessageTimeout = null; - }, 1500); - } - } else { - Utils.showAlert('Please select a position first before moving artwork', 'warning'); - } - } - - document.getElementById('moveUp').addEventListener('click', () => moveArtwork(0, -1)); - document.getElementById('moveDown').addEventListener('click', () => moveArtwork(0, 1)); - document.getElementById('moveLeft').addEventListener('click', () => moveArtwork(-1, 0)); - document.getElementById('moveRight').addEventListener('click', () => moveArtwork(1, 0)); - - // Close panel - function closeMovePanel() { - if (currentMessageTimeout) { - clearTimeout(currentMessageTimeout); - currentMessageTimeout = null; - } - Utils.hideAlert(); - document.body.removeChild(movePanel); - } - - document.getElementById('closeMovePanel').addEventListener('click', closeMovePanel); - } - - function showResizeDialog(processor) { - let baseProcessor = processor; - let width, height; - if (state.originalImage?.dataUrl) { - baseProcessor = new ImageProcessor(state.originalImage.dataUrl); - width = state.originalImage.width; - height = state.originalImage.height; - } else { - const dims = processor.getDimensions(); - width = dims.width; - height = dims.height; - } - const aspectRatio = width / height; - - const rs = state.resizeSettings; - widthSlider.max = width * 2; - heightSlider.max = height * 2; - let initialW = width; - let initialH = height; - if ( - rs && - Number.isFinite(rs.width) && - Number.isFinite(rs.height) && - rs.width > 0 && - rs.height > 0 - ) { - initialW = rs.width; - initialH = rs.height; - } - // Clamp to slider ranges - initialW = Math.max( - parseInt(widthSlider.min, 10) || 10, - Math.min(initialW, parseInt(widthSlider.max, 10)) - ); - initialH = Math.max( - parseInt(heightSlider.min, 10) || 10, - Math.min(initialH, parseInt(heightSlider.max, 10)) - ); - widthSlider.value = initialW; - heightSlider.value = initialH; - widthValue.textContent = initialW; - heightValue.textContent = initialH; - zoomSlider.value = 1; - if (zoomValue) zoomValue.textContent = '100%'; - paintWhiteToggle.checked = state.paintWhitePixels; - paintTransparentToggle.checked = state.paintTransparentPixels; - - let _previewTimer = null; - let _previewJobId = 0; - let _isDraggingSize = false; - let _zoomLevel = 1; - let _ditherWorkBuf = null; - let _ditherEligibleBuf = null; - const ensureDitherBuffers = (n) => { - if (!_ditherWorkBuf || _ditherWorkBuf.length !== n * 3) - _ditherWorkBuf = new Float32Array(n * 3); - if (!_ditherEligibleBuf || _ditherEligibleBuf.length !== n) - _ditherEligibleBuf = new Uint8Array(n); - return { work: _ditherWorkBuf, eligible: _ditherEligibleBuf }; - }; - let _maskImageData = null; - let _maskData = null; - let _dirty = null; - const _resetDirty = () => { - _dirty = { minX: Infinity, minY: Infinity, maxX: -1, maxY: -1 }; - }; - const _markDirty = (x, y) => { - if (!_dirty) _resetDirty(); - if (x < _dirty.minX) _dirty.minX = x; - if (y < _dirty.minY) _dirty.minY = y; - if (x > _dirty.maxX) _dirty.maxX = x; - if (y > _dirty.maxY) _dirty.maxY = y; - }; - const _flushDirty = () => { - if (!_dirty || _dirty.maxX < _dirty.minX || _dirty.maxY < _dirty.minY) return; - const x = Math.max(0, _dirty.minX); - const y = Math.max(0, _dirty.minY); - const w = Math.min(maskCanvas.width - x, _dirty.maxX - x + 1); - const h = Math.min(maskCanvas.height - y, _dirty.maxY - y + 1); - if (w > 0 && h > 0) maskCtx.putImageData(_maskImageData, 0, 0, x, y, w, h); - _resetDirty(); - }; - const _ensureMaskOverlayBuffers = (w, h, rebuildFromMask = false) => { - if (!_maskImageData || _maskImageData.width !== w || _maskImageData.height !== h) { - _maskImageData = maskCtx.createImageData(w, h); - _maskData = _maskImageData.data; - rebuildFromMask = true; - } - if (rebuildFromMask) { - const m = state.resizeIgnoreMask; - const md = _maskData; - md.fill(0); - if (m) { - for (let i = 0; i < m.length; i++) - if (m[i]) { - const p = i * 4; - md[p] = 255; - md[p + 1] = 0; - md[p + 2] = 0; - md[p + 3] = 150; - } - } - maskCtx.putImageData(_maskImageData, 0, 0); - _resetDirty(); - } - }; - const ensureMaskSize = (w, h) => { - if (!state.resizeIgnoreMask || state.resizeIgnoreMask.length !== w * h) { - state.resizeIgnoreMask = new Uint8Array(w * h); - } - baseCanvas.width = w; - baseCanvas.height = h; - maskCanvas.width = w; - maskCanvas.height = h; - maskCtx.clearRect(0, 0, maskCanvas.width, maskCanvas.height); - // Ensure overlay buffers exist and rebuild from mask when dimensions change - _ensureMaskOverlayBuffers(w, h, true); - }; - _updateResizePreview = async () => { - const jobId = ++_previewJobId; - const newWidth = parseInt(widthSlider.value, 10); - const newHeight = parseInt(heightSlider.value, 10); - _zoomLevel = parseFloat(zoomSlider.value); - - widthValue.textContent = newWidth; - heightValue.textContent = newHeight; - - ensureMaskSize(newWidth, newHeight); - canvasStack.style.width = newWidth + 'px'; - canvasStack.style.height = newHeight + 'px'; - baseCtx.imageSmoothingEnabled = false; - - if (!state.availableColors || state.availableColors.length === 0) { - if (baseProcessor !== processor && (!baseProcessor.img || !baseProcessor.canvas)) { - await baseProcessor.load(); - } - baseCtx.clearRect(0, 0, newWidth, newHeight); - baseCtx.drawImage(baseProcessor.img, 0, 0, newWidth, newHeight); - // Draw existing mask overlay buffer - maskCtx.clearRect(0, 0, maskCanvas.width, maskCanvas.height); - if (_maskImageData) maskCtx.putImageData(_maskImageData, 0, 0); - updateZoomLayout(); - return; - } - - if (baseProcessor !== processor && (!baseProcessor.img || !baseProcessor.canvas)) { - await baseProcessor.load(); - } - baseCtx.clearRect(0, 0, newWidth, newHeight); - baseCtx.drawImage(baseProcessor.img, 0, 0, newWidth, newHeight); - const imgData = baseCtx.getImageData(0, 0, newWidth, newHeight); - const data = imgData.data; - - const tThresh = state.customTransparencyThreshold || CONFIG.TRANSPARENCY_THRESHOLD; - - const applyFSDither = () => { - const w = newWidth, - h = newHeight; - const n = w * h; - const { work, eligible } = ensureDitherBuffers(n); - for (let y = 0; y < h; y++) { - for (let x = 0; x < w; x++) { - const idx = y * w + x; - const i4 = idx * 4; - const r = data[i4], - g = data[i4 + 1], - b = data[i4 + 2], - a = data[i4 + 3]; - const isEligible = - (state.paintTransparentPixels || a >= tThresh) && - (state.paintWhitePixels || !Utils.isWhitePixel(r, g, b)); - eligible[idx] = isEligible ? 1 : 0; - work[idx * 3] = r; - work[idx * 3 + 1] = g; - work[idx * 3 + 2] = b; - if (!isEligible) { - data[i4 + 3] = 0; // transparent in preview overlay - } - } - } - - const diffuse = (nx, ny, er, eg, eb, factor) => { - if (nx < 0 || nx >= w || ny < 0 || ny >= h) return; - const nidx = ny * w + nx; - if (!eligible[nidx]) return; - const base = nidx * 3; - work[base] = Math.min(255, Math.max(0, work[base] + er * factor)); - work[base + 1] = Math.min(255, Math.max(0, work[base + 1] + eg * factor)); - work[base + 2] = Math.min(255, Math.max(0, work[base + 2] + eb * factor)); - }; - - for (let y = 0; y < h; y++) { - for (let x = 0; x < w; x++) { - const idx = y * w + x; - if (!eligible[idx]) continue; - const base = idx * 3; - const r0 = work[base], - g0 = work[base + 1], - b0 = work[base + 2]; - const [nr, ng, nb] = Utils.findClosestPaletteColor( - r0, - g0, - b0, - state.activeColorPalette - ); - const i4 = idx * 4; - data[i4] = nr; - data[i4 + 1] = ng; - data[i4 + 2] = nb; - data[i4 + 3] = 255; - - const er = r0 - nr; - const eg = g0 - ng; - const eb = b0 - nb; - - diffuse(x + 1, y, er, eg, eb, 7 / 16); - diffuse(x - 1, y + 1, er, eg, eb, 3 / 16); - diffuse(x, y + 1, er, eg, eb, 5 / 16); - diffuse(x + 1, y + 1, er, eg, eb, 1 / 16); - } - } - }; - - // Skip expensive dithering while user is dragging sliders - if (state.ditheringEnabled && !_isDraggingSize) { - applyFSDither(); - } else { - for (let i = 0; i < data.length; i += 4) { - const r = data[i], - g = data[i + 1], - b = data[i + 2], - a = data[i + 3]; - if ( - (!state.paintTransparentPixels && a < tThresh) || - (!state.paintWhitePixels && Utils.isWhitePixel(r, g, b)) - ) { - data[i + 3] = 0; - continue; - } - const [nr, ng, nb] = Utils.findClosestPaletteColor(r, g, b, state.activeColorPalette); - data[i] = nr; - data[i + 1] = ng; - data[i + 2] = nb; - data[i + 3] = 255; - } - } - - if (jobId !== _previewJobId) return; - baseCtx.putImageData(imgData, 0, 0); - maskCtx.clearRect(0, 0, maskCanvas.width, maskCanvas.height); - if (_maskImageData) maskCtx.putImageData(_maskImageData, 0, 0); - updateZoomLayout(); - }; - - const onWidthInput = () => { - if (keepAspect.checked) { - heightSlider.value = Math.round(parseInt(widthSlider.value, 10) / aspectRatio); - } - _updateResizePreview(); - const curW = parseInt(widthSlider.value, 10); - const curH = parseInt(heightSlider.value, 10); - state.resizeSettings = { - baseWidth: width, - baseHeight: height, - width: curW, - height: curH, - }; - saveBotSettings(); - // Auto-fit after size changes - const fit = typeof computeFitZoom === 'function' ? computeFitZoom() : 1; - if (!isNaN(fit) && isFinite(fit)) applyZoom(fit); - }; - - const onHeightInput = () => { - if (keepAspect.checked) { - widthSlider.value = Math.round(parseInt(heightSlider.value, 10) * aspectRatio); - } - _updateResizePreview(); - const curW = parseInt(widthSlider.value, 10); - const curH = parseInt(heightSlider.value, 10); - state.resizeSettings = { - baseWidth: width, - baseHeight: height, - width: curW, - height: curH, - }; - saveBotSettings(); - // Auto-fit after size changes - const fit = typeof computeFitZoom === 'function' ? computeFitZoom() : 1; - if (!isNaN(fit) && isFinite(fit)) applyZoom(fit); - }; - - paintWhiteToggle.onchange = (e) => { - state.paintWhitePixels = e.target.checked; - _updateResizePreview(); - saveBotSettings(); - }; - - paintTransparentToggle.onchange = (e) => { - state.paintTransparentPixels = e.target.checked; - _updateResizePreview(); - saveBotSettings(); - }; - - let panX = 0, - panY = 0; - const clampPan = () => { - const wrapRect = panStage?.getBoundingClientRect() || { - width: 0, - height: 0, - }; - const w = (baseCanvas.width || 1) * _zoomLevel; - const h = (baseCanvas.height || 1) * _zoomLevel; - if (w <= wrapRect.width) { - panX = Math.floor((wrapRect.width - w) / 2); - } else { - const minX = wrapRect.width - w; - panX = Math.min(0, Math.max(minX, panX)); - } - if (h <= wrapRect.height) { - panY = Math.floor((wrapRect.height - h) / 2); - } else { - const minY = wrapRect.height - h; - panY = Math.min(0, Math.max(minY, panY)); - } - }; - let _panRaf = 0; - const applyPan = () => { - if (_panRaf) return; - _panRaf = requestAnimationFrame(() => { - clampPan(); - canvasStack.style.transform = `translate3d(${Math.round( - panX - )}px, ${Math.round(panY)}px, 0) scale(${_zoomLevel})`; - _panRaf = 0; - }); - }; - - const updateZoomLayout = () => { - const w = baseCanvas.width || 1, - h = baseCanvas.height || 1; - baseCanvas.style.width = w + 'px'; - baseCanvas.style.height = h + 'px'; - maskCanvas.style.width = w + 'px'; - maskCanvas.style.height = h + 'px'; - canvasStack.style.width = w + 'px'; - canvasStack.style.height = h + 'px'; - applyPan(); - }; - const applyZoom = (z) => { - _zoomLevel = Math.max(0.05, Math.min(20, z || 1)); - zoomSlider.value = _zoomLevel; - updateZoomLayout(); - if (zoomValue) zoomValue.textContent = `${Math.round(_zoomLevel * 100)}%`; - }; - zoomSlider.addEventListener('input', () => { - applyZoom(parseFloat(zoomSlider.value)); - }); - if (zoomInBtn) - zoomInBtn.addEventListener('click', () => applyZoom(parseFloat(zoomSlider.value) + 0.1)); - if (zoomOutBtn) - zoomOutBtn.addEventListener('click', () => applyZoom(parseFloat(zoomSlider.value) - 0.1)); - const computeFitZoom = () => { - const wrapRect = panStage?.getBoundingClientRect(); - if (!wrapRect) return 1; - const w = baseCanvas.width || 1; - const h = baseCanvas.height || 1; - const margin = 10; - const scaleX = (wrapRect.width - margin) / w; - const scaleY = (wrapRect.height - margin) / h; - return Math.max(0.05, Math.min(20, Math.min(scaleX, scaleY))); - }; - if (zoomFitBtn) - zoomFitBtn.addEventListener('click', () => { - applyZoom(computeFitZoom()); - centerInView(); - }); - if (zoomActualBtn) - zoomActualBtn.addEventListener('click', () => { - applyZoom(1); - centerInView(); - }); - - const centerInView = () => { - if (!panStage) return; - const rect = panStage.getBoundingClientRect(); - const w = (baseCanvas.width || 1) * _zoomLevel; - const h = (baseCanvas.height || 1) * _zoomLevel; - panX = Math.floor((rect.width - w) / 2); - panY = Math.floor((rect.height - h) / 2); - applyPan(); - }; - - let isPanning = false; - let startX = 0, - startY = 0, - startPanX = 0, - startPanY = 0; - let allowPan = false; // Space key - let panMode = false; // Explicit pan mode toggle for touch/one-button mice - const isPanMouseButton = (e) => e.button === 1 || e.button === 2; - const setCursor = (val) => { - if (panStage) panStage.style.cursor = val; - }; - const isPanActive = (e) => panMode || allowPan || isPanMouseButton(e); - const updatePanModeBtn = () => { - if (!panModeBtn) return; - panModeBtn.classList.toggle('active', panMode); - panModeBtn.setAttribute('aria-pressed', panMode ? 'true' : 'false'); - }; - if (panModeBtn) { - updatePanModeBtn(); - panModeBtn.addEventListener('click', () => { - panMode = !panMode; - updatePanModeBtn(); - setCursor(panMode ? 'grab' : ''); - }); - } - if (panStage) { - panStage.addEventListener('contextmenu', (e) => { - if (allowPan) e.preventDefault(); - }); - window.addEventListener('keydown', (e) => { - if (e.code === 'Space') { - allowPan = true; - setCursor('grab'); - } - }); - window.addEventListener('keyup', (e) => { - if (e.code === 'Space') { - allowPan = false; - if (!isPanning) setCursor(''); - } - }); - panStage.addEventListener('mousedown', (e) => { - if (!isPanActive(e)) return; - e.preventDefault(); - isPanning = true; - startX = e.clientX; - startY = e.clientY; - startPanX = panX; - startPanY = panY; - setCursor('grabbing'); - }); - window.addEventListener('mousemove', (e) => { - if (!isPanning) return; - const dx = e.clientX - startX; - const dy = e.clientY - startY; - panX = startPanX + dx; - panY = startPanY + dy; - applyPan(); - }); - window.addEventListener('mouseup', () => { - if (isPanning) { - isPanning = false; - setCursor(allowPan ? 'grab' : ''); - } - }); - panStage.addEventListener( - 'wheel', - (e) => { - if (!e.ctrlKey && !e.metaKey) return; - e.preventDefault(); - const rect = panStage.getBoundingClientRect(); - const cx = e.clientX - rect.left - panX; - const cy = e.clientY - rect.top - panY; - const before = _zoomLevel; - const step = Math.max(0.05, Math.min(0.5, Math.abs(e.deltaY) > 20 ? 0.2 : 0.1)); - const next = Math.max(0.05, Math.min(20, before + (e.deltaY > 0 ? -step : step))); - if (next === before) return; - const scale = next / before; - panX = panX - cx * (scale - 1); - panY = panY - cy * (scale - 1); - applyZoom(next); - }, - { passive: false } - ); - let lastTouchDist = null; - let touchStartTime = 0; - let doubleTapTimer = null; - panStage.addEventListener( - 'touchstart', - (e) => { - if (e.touches.length === 1) { - const t = e.touches[0]; - isPanning = true; - startX = t.clientX; - startY = t.clientY; - startPanX = panX; - startPanY = panY; - setCursor('grabbing'); - const now = Date.now(); - if (now - touchStartTime < 300) { - // double tap -> toggle 100%/fit - const z = Math.abs(_zoomLevel - 1) < 0.01 ? computeFitZoom() : 1; - applyZoom(z); - centerInView(); - if (doubleTapTimer) clearTimeout(doubleTapTimer); - } else { - touchStartTime = now; - doubleTapTimer = setTimeout(() => { - doubleTapTimer = null; - }, 320); - } - } else if (e.touches.length === 2) { - // Pinch start - const [a, b] = e.touches; - lastTouchDist = Math.hypot(b.clientX - a.clientX, b.clientY - a.clientY); - } - }, - { passive: true } - ); - panStage.addEventListener( - 'touchmove', - (e) => { - if (e.touches.length === 1 && isPanning) { - const t = e.touches[0]; - const dx = t.clientX - startX; - const dy = t.clientY - startY; - panX = startPanX + dx; - panY = startPanY + dy; - applyPan(); - } else if (e.touches.length === 2 && lastTouchDist != null) { - e.preventDefault(); - const [a, b] = e.touches; - const dist = Math.hypot(b.clientX - a.clientX, b.clientY - a.clientY); - const rect = panStage.getBoundingClientRect(); - const centerX = (a.clientX + b.clientX) / 2 - rect.left - panX; - const centerY = (a.clientY + b.clientY) / 2 - rect.top - panY; - const before = _zoomLevel; - const scale = dist / (lastTouchDist || dist); - const next = Math.max(0.05, Math.min(20, before * scale)); - if (next !== before) { - panX = panX - centerX * (next / before - 1); - panY = panY - centerY * (next / before - 1); - applyZoom(next); - } - lastTouchDist = dist; - } - }, - { passive: false } - ); - panStage.addEventListener('touchend', () => { - isPanning = false; - lastTouchDist = null; - setCursor(panMode || allowPan ? 'grab' : ''); - }); - } - const schedulePreview = () => { - if (_previewTimer) clearTimeout(_previewTimer); - const run = () => { - _previewTimer = null; - _updateResizePreview(); - }; - if (window.requestIdleCallback) { - _previewTimer = setTimeout(() => requestIdleCallback(run, { timeout: 150 }), 50); - } else { - _previewTimer = setTimeout(() => requestAnimationFrame(run), 50); - } - }; - // Track dragging to reduce work and skip dithering during drag - const markDragStart = () => { - _isDraggingSize = true; - }; - const markDragEnd = () => { - _isDraggingSize = false; - schedulePreview(); - }; - widthSlider.addEventListener('pointerdown', markDragStart); - heightSlider.addEventListener('pointerdown', markDragStart); - widthSlider.addEventListener('pointerup', markDragEnd); - heightSlider.addEventListener('pointerup', markDragEnd); - widthSlider.addEventListener('input', () => { - onWidthInput(); - schedulePreview(); - }); - heightSlider.addEventListener('input', () => { - onHeightInput(); - schedulePreview(); - }); - - // Mask painting UX: brush size, modes, row/column fills, and precise coords - let draggingMask = false; - let lastPaintX = -1, - lastPaintY = -1; - let brushSize = 1; - let rowColSize = 1; - let maskMode = 'ignore'; // 'ignore' | 'unignore' | 'toggle' - const brushEl = resizeContainer.querySelector('#maskBrushSize'); - const brushValEl = resizeContainer.querySelector('#maskBrushSizeValue'); - const btnIgnore = resizeContainer.querySelector('#maskModeIgnore'); - const btnUnignore = resizeContainer.querySelector('#maskModeUnignore'); - const btnToggle = resizeContainer.querySelector('#maskModeToggle'); - const clearIgnoredBtnEl = resizeContainer.querySelector('#clearIgnoredBtn'); - const invertMaskBtn = resizeContainer.querySelector('#invertMaskBtn'); - const rowColSizeEl = resizeContainer.querySelector('#rowColSize'); - const rowColSizeValEl = resizeContainer.querySelector('#rowColSizeValue'); - - const updateModeButtons = () => { - const map = [ - [btnIgnore, 'ignore'], - [btnUnignore, 'unignore'], - [btnToggle, 'toggle'], - ]; - for (const [el, m] of map) { - if (!el) continue; - const active = maskMode === m; - el.classList.toggle('active', active); - el.setAttribute('aria-pressed', active ? 'true' : 'false'); - } - }; - const setMode = (mode) => { - maskMode = mode; - updateModeButtons(); - }; - if (brushEl && brushValEl) { - brushEl.addEventListener('input', () => { - brushSize = parseInt(brushEl.value, 10) || 1; - brushValEl.textContent = brushSize; - }); - brushValEl.textContent = brushEl.value; - brushSize = parseInt(brushEl.value, 10) || 1; - } - if (rowColSizeEl && rowColSizeValEl) { - rowColSizeEl.addEventListener('input', () => { - rowColSize = parseInt(rowColSizeEl.value, 10) || 1; - rowColSizeValEl.textContent = rowColSize; - }); - rowColSizeValEl.textContent = rowColSizeEl.value; - rowColSize = parseInt(rowColSizeEl.value, 10) || 1; - } - if (btnIgnore) btnIgnore.addEventListener('click', () => setMode('ignore')); - if (btnUnignore) btnUnignore.addEventListener('click', () => setMode('unignore')); - if (btnToggle) btnToggle.addEventListener('click', () => setMode('toggle')); - // Initialize button state (default to toggle mode) - updateModeButtons(); - - const mapClientToPixel = (clientX, clientY) => { - // Compute without rounding until final step to avoid drift at higher zoom - const rect = baseCanvas.getBoundingClientRect(); - const scaleX = rect.width / baseCanvas.width; - const scaleY = rect.height / baseCanvas.height; - const dx = (clientX - rect.left) / scaleX; - const dy = (clientY - rect.top) / scaleY; - const x = Math.floor(dx); - const y = Math.floor(dy); - return { x, y }; - }; - - const ensureMask = (w, h) => { - if (!state.resizeIgnoreMask || state.resizeIgnoreMask.length !== w * h) { - state.resizeIgnoreMask = new Uint8Array(w * h); - } - }; - - const paintCircle = (cx, cy, radius, value) => { - const w = baseCanvas.width, - h = baseCanvas.height; - ensureMask(w, h); - const r2 = radius * radius; - for (let yy = cy - radius; yy <= cy + radius; yy++) { - if (yy < 0 || yy >= h) continue; - for (let xx = cx - radius; xx <= cx + radius; xx++) { - if (xx < 0 || xx >= w) continue; - const dx = xx - cx, - dy = yy - cy; - if (dx * dx + dy * dy <= r2) { - const idx = yy * w + xx; - let val = state.resizeIgnoreMask[idx]; - if (maskMode === 'toggle') { - val = val ? 0 : 1; - } else if (maskMode === 'ignore') { - val = 1; - } else { - val = 0; - } - state.resizeIgnoreMask[idx] = val; - if (_maskData) { - const p = idx * 4; - if (val) { - _maskData[p] = 255; - _maskData[p + 1] = 0; - _maskData[p + 2] = 0; - _maskData[p + 3] = 150; - } else { - _maskData[p] = 0; - _maskData[p + 1] = 0; - _maskData[p + 2] = 0; - _maskData[p + 3] = 0; - } - _markDirty(xx, yy); - } - } - } - } - }; - - const paintRow = (y, value) => { - const w = baseCanvas.width, - h = baseCanvas.height; - ensureMask(w, h); - if (y < 0 || y >= h) return; - - // Paint multiple rows based on rowColSize - const halfSize = Math.floor(rowColSize / 2); - const startY = Math.max(0, y - halfSize); - const endY = Math.min(h - 1, y + halfSize); - - for (let rowY = startY; rowY <= endY; rowY++) { - for (let x = 0; x < w; x++) { - const idx = rowY * w + x; - let val = state.resizeIgnoreMask[idx]; - if (maskMode === 'toggle') { - val = val ? 0 : 1; - } else if (maskMode === 'ignore') { - val = 1; - } else { - val = 0; - } - state.resizeIgnoreMask[idx] = val; - if (_maskData) { - const p = idx * 4; - if (val) { - _maskData[p] = 255; - _maskData[p + 1] = 0; - _maskData[p + 2] = 0; - _maskData[p + 3] = 150; - } else { - _maskData[p] = 0; - _maskData[p + 1] = 0; - _maskData[p + 2] = 0; - _maskData[p + 3] = 0; - } - } - } - if (_maskData) { - _markDirty(0, rowY); - _markDirty(w - 1, rowY); - } - } - }; - - const paintColumn = (x, value) => { - const w = baseCanvas.width, - h = baseCanvas.height; - ensureMask(w, h); - if (x < 0 || x >= w) return; - - // Paint multiple columns based on rowColSize - const halfSize = Math.floor(rowColSize / 2); - const startX = Math.max(0, x - halfSize); - const endX = Math.min(w - 1, x + halfSize); - - for (let colX = startX; colX <= endX; colX++) { - for (let y = 0; y < h; y++) { - const idx = y * w + colX; - let val = state.resizeIgnoreMask[idx]; - if (maskMode === 'toggle') { - val = val ? 0 : 1; - } else if (maskMode === 'ignore') { - val = 1; - } else { - val = 0; - } - state.resizeIgnoreMask[idx] = val; - if (_maskData) { - const p = idx * 4; - if (val) { - _maskData[p] = 255; - _maskData[p + 1] = 0; - _maskData[p + 2] = 0; - _maskData[p + 3] = 150; - } else { - _maskData[p] = 0; - _maskData[p + 1] = 0; - _maskData[p + 2] = 0; - _maskData[p + 3] = 0; - } - } - } - if (_maskData) { - _markDirty(colX, 0); - _markDirty(colX, h - 1); - } - } - }; - - const redrawMaskOverlay = () => { - // Only flush the dirty region; full rebuild happens on size change - _flushDirty(); - }; - - const handlePaint = (e) => { - // Suppress painting while panning - if ((e.buttons & 4) === 4 || (e.buttons & 2) === 2 || allowPan) return; - const { x, y } = mapClientToPixel(e.clientX, e.clientY); - const w = baseCanvas.width, - h = baseCanvas.height; - if (x < 0 || y < 0 || x >= w || y >= h) return; - const radius = Math.max(1, Math.floor(brushSize / 2)); - if (e.shiftKey) { - paintRow(y); - } else if (e.altKey) { - paintColumn(x); - } else { - paintCircle(x, y, radius); - } - lastPaintX = x; - lastPaintY = y; - redrawMaskOverlay(); - }; - - maskCanvas.addEventListener('mousedown', (e) => { - if (e.button === 1 || e.button === 2 || allowPan) return; // let pan handler manage - draggingMask = true; - handlePaint(e); - }); - // Avoid hijacking touch gestures for panning/zooming - maskCanvas.addEventListener( - 'touchstart', - (e) => { - /* let panStage handle */ - }, - { passive: true } - ); - maskCanvas.addEventListener( - 'touchmove', - (e) => { - /* let panStage handle */ - }, - { passive: true } - ); - maskCanvas.addEventListener( - 'touchend', - (e) => { - /* let panStage handle */ - }, - { passive: true } - ); - window.addEventListener('mousemove', (e) => { - if (draggingMask) handlePaint(e); - }); - window.addEventListener('mouseup', () => { - if (draggingMask) { - draggingMask = false; - saveBotSettings(); - } - }); - - if (clearIgnoredBtnEl) - clearIgnoredBtnEl.addEventListener('click', () => { - const w = baseCanvas.width, - h = baseCanvas.height; - if (state.resizeIgnoreMask) state.resizeIgnoreMask.fill(0); - _ensureMaskOverlayBuffers(w, h, true); - _updateResizePreview(); - saveBotSettings(); - }); - - if (invertMaskBtn) - invertMaskBtn.addEventListener('click', () => { - if (!state.resizeIgnoreMask) return; - for (let i = 0; i < state.resizeIgnoreMask.length; i++) - state.resizeIgnoreMask[i] = state.resizeIgnoreMask[i] ? 0 : 1; - const w = baseCanvas.width, - h = baseCanvas.height; - _ensureMaskOverlayBuffers(w, h, true); - _updateResizePreview(); - saveBotSettings(); - }); - - confirmResize.onclick = async () => { - const newWidth = parseInt(widthSlider.value, 10); - const newHeight = parseInt(heightSlider.value, 10); - - // Generate the final paletted image data - const tempCanvas = document.createElement('canvas'); - const tempCtx = tempCanvas.getContext('2d'); - tempCanvas.width = newWidth; - tempCanvas.height = newHeight; - tempCtx.imageSmoothingEnabled = false; - if (baseProcessor !== processor && (!baseProcessor.img || !baseProcessor.canvas)) { - await baseProcessor.load(); - } - tempCtx.drawImage(baseProcessor.img, 0, 0, newWidth, newHeight); - const imgData = tempCtx.getImageData(0, 0, newWidth, newHeight); - const data = imgData.data; - const tThresh2 = state.customTransparencyThreshold || CONFIG.TRANSPARENCY_THRESHOLD; - let totalValidPixels = 0; - const mask = - state.resizeIgnoreMask && state.resizeIgnoreMask.length === newWidth * newHeight - ? state.resizeIgnoreMask - : null; - - const applyFSDitherFinal = async () => { - const w = newWidth, - h = newHeight; - const n = w * h; - const { work, eligible } = ensureDitherBuffers(n); - for (let y = 0; y < h; y++) { - for (let x = 0; x < w; x++) { - const idx = y * w + x; - const i4 = idx * 4; - const r = data[i4], - g = data[i4 + 1], - b = data[i4 + 2], - a = data[i4 + 3]; - const masked = mask && mask[idx]; - const isEligible = - !masked && - (state.paintTransparentPixels || a >= tThresh2) && - (state.paintWhitePixels || !Utils.isWhitePixel(r, g, b)); - eligible[idx] = isEligible ? 1 : 0; - work[idx * 3] = r; - work[idx * 3 + 1] = g; - work[idx * 3 + 2] = b; - if (!isEligible) { - data[i4 + 3] = 0; - } - } - // Yield to keep UI responsive - if ((y & 15) === 0) await Promise.resolve(); - } - - const diffuse = (nx, ny, er, eg, eb, factor) => { - if (nx < 0 || nx >= w || ny < 0 || ny >= h) return; - const nidx = ny * w + nx; - if (!eligible[nidx]) return; - const base = nidx * 3; - work[base] = Math.min(255, Math.max(0, work[base] + er * factor)); - work[base + 1] = Math.min(255, Math.max(0, work[base + 1] + eg * factor)); - work[base + 2] = Math.min(255, Math.max(0, work[base + 2] + eb * factor)); - }; - - for (let y = 0; y < h; y++) { - for (let x = 0; x < w; x++) { - const idx = y * w + x; - if (!eligible[idx]) continue; - const base = idx * 3; - const r0 = work[base], - g0 = work[base + 1], - b0 = work[base + 2]; - const [nr, ng, nb] = Utils.findClosestPaletteColor( - r0, - g0, - b0, - state.activeColorPalette - ); - const i4 = idx * 4; - data[i4] = nr; - data[i4 + 1] = ng; - data[i4 + 2] = nb; - data[i4 + 3] = 255; - totalValidPixels++; - - const er = r0 - nr; - const eg = g0 - ng; - const eb = b0 - nb; - - diffuse(x + 1, y, er, eg, eb, 7 / 16); - diffuse(x - 1, y + 1, er, eg, eb, 3 / 16); - diffuse(x, y + 1, er, eg, eb, 5 / 16); - diffuse(x + 1, y + 1, er, eg, eb, 1 / 16); - } - // Yield every row to reduce jank - await Promise.resolve(); - } - }; - - if (state.ditheringEnabled) { - await applyFSDitherFinal(); - } else { - for (let i = 0; i < data.length; i += 4) { - const r = data[i], - g = data[i + 1], - b = data[i + 2], - a = data[i + 3]; - const masked = mask && mask[i >> 2]; - const isTransparent = (!state.paintTransparentPixels && a < tThresh2) || masked; - const isWhiteAndSkipped = !state.paintWhitePixels && Utils.isWhitePixel(r, g, b); - if (isTransparent || isWhiteAndSkipped) { - data[i + 3] = 0; // overlay transparency - continue; - } - totalValidPixels++; - const [nr, ng, nb] = Utils.findClosestPaletteColor(r, g, b, state.activeColorPalette); - data[i] = nr; - data[i + 1] = ng; - data[i + 2] = nb; - data[i + 3] = 255; - } - } - tempCtx.putImageData(imgData, 0, 0); - - // Save the final pixel data for painting - // Persist the paletted (and possibly dithered) pixels so painting uses the same output seen in overlay - const palettedPixels = new Uint8ClampedArray(imgData.data); - state.imageData.pixels = palettedPixels; - state.imageData.width = newWidth; - state.imageData.height = newHeight; - state.imageData.totalPixels = totalValidPixels; - state.totalPixels = totalValidPixels; - state.paintedPixels = 0; - - state.resizeSettings = { - baseWidth: width, - baseHeight: height, - width: newWidth, - height: newHeight, - }; - saveBotSettings(); - - const finalImageBitmap = await createImageBitmap(tempCanvas); - await overlayManager.setImage(finalImageBitmap); - overlayManager.enable(); - toggleOverlayBtn.classList.add('active'); - toggleOverlayBtn.setAttribute('aria-pressed', 'true'); - - // Keep state.imageData.processor as the original-based source; painting uses paletted pixels already stored - - await updateStats(); - updateUI('resizeSuccess', 'success', { - width: newWidth, - height: newHeight, - }); - closeResizeDialog(); - }; - - downloadPreviewBtn.onclick = () => { - try { - const w = baseCanvas.width, - h = baseCanvas.height; - const out = document.createElement('canvas'); - out.width = w; - out.height = h; - const octx = out.getContext('2d'); - octx.imageSmoothingEnabled = false; - octx.drawImage(baseCanvas, 0, 0); - octx.drawImage(maskCanvas, 0, 0); - const link = document.createElement('a'); - link.download = 'wplace-preview.png'; - link.href = out.toDataURL(); - link.click(); - } catch (e) { - console.warn('Failed to download preview:', e); - } - }; - - cancelResize.onclick = closeResizeDialog; - - editImageBtn.onclick = () => { - showEditPanel(); - }; - - resizeOverlay.style.display = 'block'; - resizeContainer.style.display = 'block'; - - // Reinitialize color palette with current available colors - initializeColorPalette(resizeContainer); - - _updateResizePreview(); - _resizeDialogCleanup = () => { - try { - zoomSlider.replaceWith(zoomSlider.cloneNode(true)); - } catch { } - try { - if (zoomInBtn) zoomInBtn.replaceWith(zoomInBtn.cloneNode(true)); - } catch { } - try { - if (zoomOutBtn) zoomOutBtn.replaceWith(zoomOutBtn.cloneNode(true)); - } catch { } - }; - setTimeout(() => { - if (typeof computeFitZoom === 'function') { - const z = computeFitZoom(); - if (!isNaN(z) && isFinite(z)) { - applyZoom(z); - centerInView(); - } - } else { - centerInView(); - } - }, 0); - } - - function closeResizeDialog() { - try { - if (typeof _resizeDialogCleanup === 'function') { - _resizeDialogCleanup(); - } - } catch { } - resizeOverlay.style.display = 'none'; - resizeContainer.style.display = 'none'; - _updateResizePreview = () => { }; - try { - if (typeof cancelAnimationFrame === 'function' && _panRaf) { - cancelAnimationFrame(_panRaf); - } - } catch { } - try { - if (_previewTimer) { - clearTimeout(_previewTimer); - _previewTimer = null; - } - } catch { } - _maskImageData = null; - _maskData = null; - _dirty = null; - _ditherWorkBuf = null; - _ditherEligibleBuf = null; - _resizeDialogCleanup = null; - } - - function showEditPanel() { - try { - // Validate that we have a valid base canvas - if (!baseCanvas || !baseCanvas.width || !baseCanvas.height) { - Utils.showAlert('No image available for editing. Please upload an image first.', 'error'); - return; - } - - // Hide resize panel - resizeContainer.style.display = 'none'; - - // Create edit panel if it doesn't exist - let editOverlay = document.getElementById('editOverlay'); - if (!editOverlay) { - createEditPanel(); - editOverlay = document.getElementById('editOverlay'); - } - - // Get current image data from baseCanvas - const imageData = baseCanvas.toDataURL(); - - // Initialize edit panel with current image - initializeEditPanel(imageData); - - // Show edit panel - editOverlay.style.display = 'block'; - - console.log('✨ Pixel Art Editor opened successfully'); - } catch (error) { - console.error('Error opening pixel art editor:', error); - Utils.showAlert('Failed to open pixel art editor. Please try again.', 'error'); - } - } - - function createEditPanel() { - const editOverlay = document.createElement('div'); - editOverlay.id = 'editOverlay'; - editOverlay.className = 'edit-overlay'; - - editOverlay.innerHTML = ` -
-
-
-

Manual Pixel Art Editor

-
- 🖱️ Left click: Draw | Right click/Ctrl+drag: Pan | Mouse wheel: Zoom -
-
-
-
- - - - - -
-
- - -
-
-
- -
-
-
-
- - - - - -
- -
- - - 1 - -
- -
- - - -
-
- -
-
-
- -
-
-
Navigator
-
- -
-
-
-
-
-
- -
-
- Position: (0, 0) | Color: #000000 | Zoom: 100% -
-
- -
-
-
- -
- -
-
-
-
-
- - `; - - document.body.appendChild(editOverlay); - - // Set up event handlers - setupEditPanelEvents(); - } - - function setupEditPanelEvents() { - const editBackBtn = document.getElementById('editBackBtn'); - const editApplyBtn = document.getElementById('editApplyBtn'); - const paintBrush = document.getElementById('paintBrush'); - const eraseTool = document.getElementById('eraseTool'); - const eyedropperTool = document.getElementById('eyedropperTool'); - const fillTool = document.getElementById('fillTool'); - const brushSize = document.getElementById('brushSize'); - const brushSizeValue = document.getElementById('brushSizeValue'); - const showGrid = document.getElementById('showGrid'); - const undoBtn = document.getElementById('undoBtn'); - const redoBtn = document.getElementById('redoBtn'); - const resetViewBtn = document.getElementById('resetViewBtn'); - const editZoomIn = document.getElementById('editZoomIn'); - const editZoomOut = document.getElementById('editZoomOut'); - const zoomSelect = document.getElementById('zoomSelect'); - const zoomFit = document.getElementById('zoomFit'); - const zoom100 = document.getElementById('zoom100'); - const minimapCanvas = document.getElementById('minimapCanvas'); - - // Back to resize panel - editBackBtn.onclick = () => { - document.getElementById('editOverlay').style.display = 'none'; - resizeContainer.style.display = 'block'; - }; - - // Apply changes - editApplyBtn.onclick = () => { - applyEditChanges(); - document.getElementById('editOverlay').style.display = 'none'; - resizeContainer.style.display = 'block'; - }; - - // Tool selection - paintBrush.onclick = () => { - selectTool('paint'); - }; - - eraseTool.onclick = () => { - selectTool('erase'); - }; - - eyedropperTool.onclick = () => { - selectTool('eyedropper'); - }; - - fillTool.onclick = () => { - selectTool('fill'); - }; - - // Grid toggle - showGrid.onclick = () => { - editState.showGrid = !editState.showGrid; - showGrid.classList.toggle('active', editState.showGrid); - redrawCanvas(); - }; - - // Brush size - brushSize.oninput = () => { - brushSizeValue.textContent = brushSize.value; - updateBrushSize(parseInt(brushSize.value)); - }; - - // Undo/Redo - undoBtn.onclick = () => undoEdit(); - redoBtn.onclick = () => redoEdit(); - - // Reset view - resetViewBtn.onclick = () => resetEditView(); - - // Enhanced zoom controls - editZoomIn.onclick = () => zoomIn(); - editZoomOut.onclick = () => zoomOut(); - - zoomSelect.onchange = () => { - const newZoom = parseFloat(zoomSelect.value); - setZoom(newZoom); - }; - - zoomFit.onclick = () => fitToWindow(); - zoom100.onclick = () => setZoom(1); - - // Minimap navigation - if (minimapCanvas) { - minimapCanvas.onclick = (e) => navigateToMinimapPosition(e); - } - } - - const ZOOM_LEVELS = [0.1, 0.25, 0.5, 0.75, 1, 1.5, 2, 3, 4, 6, 8, 12, 16, 24, 32]; - - let editState = { - currentTool: 'paint', - currentColor: '#000000', - currentColorId: null, - brushSize: 1, - zoom: 1, - panX: 0, - panY: 0, - undoStack: [], - redoStack: [], - isDrawing: false, - isPanning: false, - showGrid: false, - - mouseX: 0, - mouseY: 0, - canvasWidth: 0, - canvasHeight: 0, - updatePending: false, - lastPaintPos: null, - touchStart: null, - lastTouchDistance: 0, - lastTouchCenter: { x: 0, y: 0 } - }; - - function calculateOptimalPanelSize(imageWidth, imageHeight) { - const viewportWidth = window.innerWidth; - const viewportHeight = window.innerHeight; - - // Reserve minimal space for UI elements for almost fullscreen experience - const uiReserved = { - header: 80, - toolbar: 60, - bottomBar: 120, - padding: 40 - }; - - const maxCanvasWidth = viewportWidth - uiReserved.padding; - const maxCanvasHeight = viewportHeight - uiReserved.header - uiReserved.toolbar - uiReserved.bottomBar - uiReserved.padding; - - // Calculate optimal initial zoom to fit image - const scaleX = maxCanvasWidth / imageWidth; - const scaleY = maxCanvasHeight / imageHeight; - const initialZoom = Math.min(scaleX, scaleY, 1); // Don't zoom in initially - - return { - panelWidth: Math.min(viewportWidth * 0.95, imageWidth * initialZoom + uiReserved.colorPanel + uiReserved.padding), - panelHeight: Math.min(viewportHeight * 0.95, imageHeight * initialZoom + uiReserved.toolbar + uiReserved.statusBar + uiReserved.canvasToolbar + uiReserved.padding), - canvasWidth: Math.min(maxCanvasWidth, imageWidth * initialZoom), - canvasHeight: Math.min(maxCanvasHeight, imageHeight * initialZoom), - initialZoom: Math.max(0.1, initialZoom) - }; - } - - function initializeEditPanel(imageData) { - const editCanvas = document.getElementById('editCanvas'); - const ctx = editCanvas.getContext('2d'); - - // Reset edit state - editState.zoom = 1; - editState.panX = 0; - editState.panY = 0; - editState.undoStack = []; - editState.redoStack = []; - editState.currentTool = 'paint'; - editState.brushSize = 1; - editState.showGrid = false; - editState.isPanning = false; - editState.isDrawing = false; - editState.lastPaintPos = null; - - // Set canvas size to match baseCanvas - editCanvas.width = baseCanvas.width; - editCanvas.height = baseCanvas.height; - editState.canvasWidth = editCanvas.width; - editState.canvasHeight = editCanvas.height; - - // Configure canvas context for pixel art - ctx.imageSmoothingEnabled = false; - ctx.webkitImageSmoothingEnabled = false; - ctx.mozImageSmoothingEnabled = false; - ctx.msImageSmoothingEnabled = false; - - // Calculate optimal panel size - const panelSize = calculateOptimalPanelSize(editCanvas.width, editCanvas.height); - const editContainer = document.querySelector('.edit-container'); - if (editContainer) { - editContainer.style.width = panelSize.panelWidth + 'px'; - editContainer.style.height = panelSize.panelHeight + 'px'; - } - - // Setup canvas container - setupCanvasContainer(); - - // Add CSS checkerboard background to the wrapper element (not the canvas) - // This allows users to see transparency without saving it to the template - const canvasWrapper = document.getElementById('editCanvasWrapper'); - if (canvasWrapper) { - canvasWrapper.style.background = ` - repeating-conic-gradient(#f0f0f0 0% 25%, #e0e0e0 0% 50%) - 50% / 16px 16px - `; - } - - // Load image onto canvas - const img = new Image(); - img.onload = () => { - ctx.clearRect(0, 0, editCanvas.width, editCanvas.height); - // REMOVED: drawCheckerboardBackground - prevents checkerboard from being saved with template - ctx.drawImage(img, 0, 0); - - // Always fit artwork to the visible area on start and center - fitToWindow(); - centerCanvas(); - - // Setup minimap - setupMinimap(); - - // Save initial state for undo - saveEditState(); - - // Set up canvas drawing events - setupCanvasDrawing(); - - // Initialize color palette - initializeEditColorPalette(); - - // Setup keyboard shortcuts - setupKeyboardShortcuts(); - - // Setup touch support - setupTouchSupport(); - - // Set initial tool - selectTool('paint'); - - // Update status bar - updateStatusBar(0, 0); - - // Center canvas initially - centerCanvas(); - }; - img.src = imageData; - } - - // mapClientToCanvas function for coordinate mapping - function mapClientToCanvas(clientX, clientY) { - const canvas = document.getElementById('editCanvas'); - const wrapper = document.getElementById('editCanvasWrapper'); - const container = document.getElementById('editCanvasContainer'); - - if (!canvas || !wrapper || !container) return null; - - // Get the actual canvas element bounds (after CSS transform) - const canvasRect = canvas.getBoundingClientRect(); - - // Calculate relative position within the actual canvas bounds - const relativeX = (clientX - canvasRect.left) / canvasRect.width * canvas.width; - const relativeY = (clientY - canvasRect.top) / canvasRect.height * canvas.height; - - // Convert to canvas coordinates - const canvasX = Math.floor(relativeX); - const canvasY = Math.floor(relativeY); - - // Bounds checking - if (canvasX < 0 || canvasX >= canvas.width || canvasY < 0 || canvasY >= canvas.height) { - return null; - } - - return { x: canvasX, y: canvasY }; - } - - function setupCanvasDrawing() { - const editCanvas = document.getElementById('editCanvas'); - const ctx = editCanvas.getContext('2d'); - - let isDrawing = false; - let isPanning = false; - let lastX = 0; - let lastY = 0; - let panStartX = 0; - let panStartY = 0; - - const getMousePos = (e) => { - return mapClientToCanvas(e.clientX, e.clientY); - }; - - editCanvas.onmousedown = (e) => { - if (e.button === 2 || e.ctrlKey) { // Right click or Ctrl+click for panning - editState.isPanning = true; - panStartX = e.clientX - editState.panX; - panStartY = e.clientY - editState.panY; - editCanvas.style.cursor = 'move'; - e.preventDefault(); - return; - } - - const pos = getMousePos(e); - if (!pos) return; - - if (editState.currentTool === 'eyedropper') { - handleEyedropper(pos.x, pos.y); - return; - } - - if (editState.currentTool === 'fill') { - floodFill(pos.x, pos.y, editState.currentColor); - saveEditState(); - return; - } - - editState.isDrawing = true; - editState.lastPaintPos = { x: pos.x, y: pos.y }; - lastX = pos.x; - lastY = pos.y; - - paintAtPosition(pos.x, pos.y, true); - }; - - editCanvas.onmousemove = (e) => { - const pos = getMousePos(e); - - if (pos) { - editState.mouseX = pos.x; - editState.mouseY = pos.y; - updateStatusBar(pos.x, pos.y); - } - - if (editState.isPanning) { - editState.panX = e.clientX - panStartX; - editState.panY = e.clientY - panStartY; - constrainPan(); - updateCanvasTransform(); - updateMinimap(); - return; - } - - if (!editState.isDrawing) { - return; - } - - if (pos && editState.lastPaintPos) { - paintAtPosition(pos.x, pos.y, true); - editState.lastPaintPos = { x: pos.x, y: pos.y }; - } - }; - - editCanvas.onmouseup = (e) => { - if (editState.isPanning) { - editState.isPanning = false; - selectTool(editState.currentTool); // Restore cursor - return; - } - - if (editState.isDrawing) { - editState.isDrawing = false; - editState.lastPaintPos = null; - saveEditState(); - } - }; - - editCanvas.onmouseleave = () => { - if (editState.isPanning) { - editState.isPanning = false; - selectTool(editState.currentTool); // Restore cursor - } - - if (editState.isDrawing) { - editState.isDrawing = false; - editState.lastPaintPos = null; - saveEditState(); - } - }; - - // Prevent context menu on right click - editCanvas.oncontextmenu = (e) => { - e.preventDefault(); - return false; - }; - - // Enhanced zoom with mouse wheel (zoom to cursor) - editCanvas.onwheel = (e) => { - e.preventDefault(); - const zoomFactor = e.deltaY < 0 ? 1.2 : 1 / 1.2; - zoomToPoint(editState.zoom * zoomFactor, e.clientX, e.clientY); - }; - } - - function paintAtPosition(x, y, isMouseDown = true) { - if (!isMouseDown || !editState.currentColor) return; - - const canvas = document.getElementById('editCanvas'); - const ctx = canvas.getContext('2d'); - - if (editState.lastPaintPos && editState.currentTool === 'paint') { - // Draw line from last position to current position - drawLine(ctx, editState.lastPaintPos.x, editState.lastPaintPos.y, x, y, editState.currentTool); - } else { - // Single brush stroke - drawBrush(ctx, x, y, editState.currentTool); - } - - editState.lastPaintPos = { x, y }; - - // Update minimap thumbnail - if (!editState.updatePending) { - editState.updatePending = true; - requestAnimationFrame(() => { - setupMinimap(); - editState.updatePending = false; - }); - } - } - - function drawBrush(ctx, x, y, tool) { - const size = editState.brushSize; - const halfSize = Math.floor(size / 2); - - if (tool === 'paint') { - ctx.fillStyle = editState.currentColor; - for (let dx = 0; dx < size; dx++) { - for (let dy = 0; dy < size; dy++) { - // Center the brush at the cursor position - const px = x - halfSize + dx; - const py = y - halfSize + dy; - - if (px >= 0 && px < ctx.canvas.width && py >= 0 && py < ctx.canvas.height) { - ctx.fillRect(px, py, 1, 1); - } - } - } - } else if (tool === 'erase') { - for (let dx = 0; dx < size; dx++) { - for (let dy = 0; dy < size; dy++) { - // Center the brush at the cursor position - const px = x - halfSize + dx; - const py = y - halfSize + dy; - - if (px >= 0 && px < ctx.canvas.width && py >= 0 && py < ctx.canvas.height) { - ctx.clearRect(px, py, 1, 1); - } - } - } - } - } - - function drawPixel(x, y) { - drawBrush(document.getElementById('editCanvas').getContext('2d'), x, y, 'paint'); - } - - function erasePixel(x, y) { - const editCanvas = document.getElementById('editCanvas'); - const ctx = editCanvas.getContext('2d'); - - const size = editState.brushSize; - const halfSize = Math.floor(size / 2); - - for (let dx = 0; dx < size; dx++) { - for (let dy = 0; dy < size; dy++) { - // Center the brush at the cursor position (consistent with drawBrush) - const px = x - halfSize + dx; - const py = y - halfSize + dy; - - if (px >= 0 && px < editCanvas.width && py >= 0 && py < editCanvas.height) { - ctx.clearRect(px, py, 1, 1); - } - } - } - } - - function drawLine(ctx, x1, y1, x2, y2, tool) { - // Bresenham's line algorithm with tool support - const dx = Math.abs(x2 - x1); - const dy = Math.abs(y2 - y1); - const sx = x1 < x2 ? 1 : -1; - const sy = y1 < y2 ? 1 : -1; - let err = dx - dy; - - let x = x1; - let y = y1; - - while (true) { - drawBrush(ctx, x, y, tool); - - if (x === x2 && y === y2) break; - - const e2 = 2 * err; - if (e2 > -dy) { - err -= dy; - x += sx; - } - if (e2 < dx) { - err += dx; - y += sy; - } - } - } - - function eraseLine(x1, y1, x2, y2) { - // Same as drawLine but with erase - const dx = Math.abs(x2 - x1); - const dy = Math.abs(y2 - y1); - const sx = x1 < x2 ? 1 : -1; - const sy = y1 < y2 ? 1 : -1; - let err = dx - dy; - - let x = x1; - let y = y1; - - while (true) { - erasePixel(x, y); - - if (x === x2 && y === y2) break; - - const e2 = 2 * err; - if (e2 > -dy) { - err -= dy; - x += sx; - } - if (e2 < dx) { - err += dx; - y += sy; - } - } - } - - function selectTool(tool) { - editState.currentTool = tool; - - document.querySelectorAll('.edit-tool').forEach(btn => { - btn.classList.remove('active'); - }); - - const editCanvas = document.getElementById('editCanvas'); - - switch (tool) { - case 'paint': - document.getElementById('paintBrush').classList.add('active'); - editCanvas.style.cursor = 'crosshair'; - break; - case 'erase': - document.getElementById('eraseTool').classList.add('active'); - editCanvas.style.cursor = 'not-allowed'; - break; - case 'eyedropper': - document.getElementById('eyedropperTool').classList.add('active'); - editCanvas.style.cursor = 'copy'; - break; - case 'fill': - document.getElementById('fillTool').classList.add('active'); - editCanvas.style.cursor = 'pointer'; - break; - } - } - - function updateBrushSize(size) { - editState.brushSize = size; - } - - function initializeEditColorPalette() { - const colorGrid = document.getElementById('editColorGrid'); - const currentColorDisplay = document.getElementById('currentColorDisplay'); - - colorGrid.innerHTML = ''; - - let availableColors = []; - - // Try to get colors from state first - if (state && state.availableColors && state.availableColors.length > 0) { - availableColors = state.availableColors.map(color => ({ - id: color.id, - name: color.name, - rgb: color.rgb, - hex: `#${color.rgb[0].toString(16).padStart(2, '0')}${color.rgb[1].toString(16).padStart(2, '0')}${color.rgb[2].toString(16).padStart(2, '0')}` - })); - } else { - // Fallback to CONFIG.COLOR_MAP - availableColors = Object.values(CONFIG.COLOR_MAP) - .filter(color => color.rgb !== null) - .map(color => ({ - id: color.id, - name: color.name, - rgb: [color.rgb.r, color.rgb.g, color.rgb.b], - hex: `#${color.rgb.r.toString(16).padStart(2, '0')}${color.rgb.g.toString(16).padStart(2, '0')}${color.rgb.b.toString(16).padStart(2, '0')}` - })); - } - - // Fallback to basic colors if nothing available - if (availableColors.length === 0) { - availableColors = [ - { id: 0, name: 'Black', rgb: [0, 0, 0], hex: '#000000' }, - { id: 1, name: 'White', rgb: [255, 255, 255], hex: '#ffffff' }, - { id: 2, name: 'Red', rgb: [255, 0, 0], hex: '#ff0000' }, - { id: 3, name: 'Green', rgb: [0, 255, 0], hex: '#00ff00' }, - { id: 4, name: 'Blue', rgb: [0, 0, 255], hex: '#0000ff' } - ]; - } - - // Update color count - const colorCount = document.getElementById('editColorCount'); - if (colorCount) { - colorCount.textContent = availableColors.length; - } - - availableColors.forEach(color => { - const colorBtn = document.createElement('button'); - colorBtn.className = 'color-btn'; - colorBtn.style.backgroundColor = color.hex; - colorBtn.title = `${color.name} (${color.hex})`; - colorBtn.dataset.colorId = color.id; - colorBtn.dataset.colorHex = color.hex; - - colorBtn.onclick = () => { - editState.currentColor = color.hex; - editState.currentColorId = color.id; - currentColorDisplay.style.backgroundColor = color.hex; - - // Update active color - document.querySelectorAll('.color-btn').forEach(btn => { - btn.classList.remove('selected'); - }); - colorBtn.classList.add('selected'); - - updateStatusBar(editState.mouseX, editState.mouseY); - }; - - colorGrid.appendChild(colorBtn); - }); - - // Set first color as default - if (availableColors.length > 0) { - editState.currentColor = availableColors[0].hex; - editState.currentColorId = availableColors[0].id; - currentColorDisplay.style.backgroundColor = availableColors[0].hex; - colorGrid.firstChild.classList.add('selected'); - } - } - - function saveEditState() { - const editCanvas = document.getElementById('editCanvas'); - const imageData = editCanvas.toDataURL(); - - editState.undoStack.push(imageData); - - // Limit undo stack size - if (editState.undoStack.length > 50) { - editState.undoStack.shift(); - } - - // Clear redo stack when new action is performed - editState.redoStack = []; - - updateUndoRedoButtons(); - } - - function undoEdit() { - if (editState.undoStack.length <= 1) return; - - const currentState = editState.undoStack.pop(); - editState.redoStack.push(currentState); - - const previousState = editState.undoStack[editState.undoStack.length - 1]; - loadEditState(previousState); - - updateUndoRedoButtons(); - } - - function redoEdit() { - if (editState.redoStack.length === 0) return; - - const nextState = editState.redoStack.pop(); - editState.undoStack.push(nextState); - - loadEditState(nextState); - - updateUndoRedoButtons(); - } - - function loadEditState(imageData) { - const editCanvas = document.getElementById('editCanvas'); - const ctx = editCanvas.getContext('2d'); - - const img = new Image(); - img.onload = () => { - ctx.clearRect(0, 0, editCanvas.width, editCanvas.height); - // REMOVED: drawCheckerboardBackground - prevents checkerboard from being saved with template - ctx.drawImage(img, 0, 0); - }; - img.src = imageData; - } - - function updateUndoRedoButtons() { - const undoBtn = document.getElementById('undoBtn'); - const redoBtn = document.getElementById('redoBtn'); - - if (undoBtn) { - undoBtn.disabled = editState.undoStack.length <= 1; - } - - if (redoBtn) { - redoBtn.disabled = editState.redoStack.length === 0; - } - } - - function updateCanvasTransform() { - const wrapper = document.getElementById('editCanvasWrapper'); - if (wrapper) { - wrapper.style.transform = `translate(${editState.panX}px, ${editState.panY}px) scale(${editState.zoom})`; - } - - // Update zoom select - const zoomSelect = document.getElementById('zoomSelect'); - if (zoomSelect) { - // If exact value exists, use it, otherwise show nearest but update displayed text - const exact = ZOOM_LEVELS.find(z => z === editState.zoom); - zoomSelect.value = exact != null ? exact : ZOOM_LEVELS.reduce((prev, curr) => Math.abs(curr - editState.zoom) < Math.abs(prev - editState.zoom) ? curr : prev); - // Also update status bar zoom text - updateStatusBar(editState.mouseX || 0, editState.mouseY || 0); - } - } - - function setupCanvasContainer() { - const canvasContainer = document.getElementById('editCanvasContainer'); - const editCanvas = document.getElementById('editCanvas'); - const wrapper = document.getElementById('editCanvasWrapper'); - - if (!canvasContainer || !wrapper) return; - - // Setup container styles - canvasContainer.style.cssText = ` - position: relative; - overflow: hidden; - display: flex; - align-items: center; - justify-content: center; - width: 100%; - height: 100%; - background: - linear-gradient(45deg, #f0f0f0 25%, transparent 25%), - linear-gradient(-45deg, #f0f0f0 25%, transparent 25%), - linear-gradient(45deg, transparent 75%, #f0f0f0 75%), - linear-gradient(-45deg, transparent 75%, #f0f0f0 75%); - background-size: 20px 20px; - background-position: 0 0, 0 10px, 10px -10px, -10px 0px; - background-color: #ddd; - `; - - // Setup wrapper styles - wrapper.style.cssText = ` - position: relative; - transform-origin: center center; - display: inline-block; - box-shadow: 0 4px 20px rgba(0,0,0,0.3); - border: 2px solid #333; - background: white; - `; - } - - function zoomToPoint(newZoom, clientX, clientY) { - const container = document.getElementById('editCanvasContainer'); - const wrapper = document.getElementById('editCanvasWrapper'); - - if (!container || !wrapper) return; - - newZoom = Math.max(0.1, Math.min(32, newZoom)); - - if (clientX !== undefined && clientY !== undefined) { - // Get current transform values - const oldZoom = editState.zoom; - const rect = container.getBoundingClientRect(); - - // Calculate zoom center point relative to container - const containerCenterX = rect.left + rect.width / 2; - const containerCenterY = rect.top + rect.height / 2; - - // Calculate offset to keep zoom point centered - const offsetX = (clientX - containerCenterX) * (1 - newZoom / oldZoom); - const offsetY = (clientY - containerCenterY) * (1 - newZoom / oldZoom); - - editState.panX += offsetX; - editState.panY += offsetY; - } - - editState.zoom = newZoom; - constrainPan(); - updateCanvasTransform(); - updateMinimap(); - updateStatusBar(editState.mouseX || 0, editState.mouseY || 0); - } - - function constrainPan() { - const container = document.getElementById('editCanvasContainer'); - const canvas = document.getElementById('editCanvas'); - - if (!container || !canvas) return; - - const containerRect = container.getBoundingClientRect(); - const scaledWidth = canvas.width * editState.zoom; - const scaledHeight = canvas.height * editState.zoom; - - // Small padding around edges to avoid snapping against borders - const padding = 10; - - // Calculate limits to keep canvas somewhat visible - const maxPanX = Math.max(0, (scaledWidth - containerRect.width) / 2 + padding); - const maxPanY = Math.max(0, (scaledHeight - containerRect.height) / 2 + padding); - - editState.panX = Math.max(-maxPanX, Math.min(maxPanX, editState.panX)); - editState.panY = Math.max(-maxPanY, Math.min(maxPanY, editState.panY)); - } - - function setZoom(zoom) { - zoomToPoint(zoom); - } - - function zoomIn() { - const currentIndex = ZOOM_LEVELS.findIndex(z => z >= editState.zoom); - const nextIndex = Math.min(currentIndex + 1, ZOOM_LEVELS.length - 1); - setZoom(ZOOM_LEVELS[nextIndex]); - } - - function zoomOut() { - const currentIndex = ZOOM_LEVELS.findIndex(z => z >= editState.zoom); - const prevIndex = Math.max(currentIndex - 1, 0); - setZoom(ZOOM_LEVELS[prevIndex]); - } - - function fitToWindow() { - const container = document.getElementById('editCanvasContainer'); - const canvas = document.getElementById('editCanvas'); - - if (!container || !canvas) return; - - const containerRect = container.getBoundingClientRect(); - const padding = 40; - - const scaleX = (containerRect.width - padding) / canvas.width; - const scaleY = (containerRect.height - padding) / canvas.height; - const fitZoom = Math.max(0.1, Math.min(scaleX, scaleY)); - - // Center the canvas - editState.zoom = fitZoom; - editState.panX = 0; - editState.panY = 0; - updateCanvasTransform(); - updateMinimap(); - updateStatusBar(editState.mouseX || 0, editState.mouseY || 0); - } - - function centerCanvas() { - editState.panX = 0; - editState.panY = 0; - updateCanvasTransform(); - updateMinimap(); - } - - function resetEditView() { - editState.zoom = 1; - editState.panX = 0; - editState.panY = 0; - - updateCanvasTransform(); - updateMinimap(); - } - - function setupMinimap() { - const minimapCanvas = document.getElementById('minimapCanvas'); - const editCanvas = document.getElementById('editCanvas'); - const minimapContainer = document.getElementById('minimapContainer'); - - if (!minimapCanvas || !editCanvas) return; - - // Show minimap only for larger images - if (editCanvas.width < 100 || editCanvas.height < 100) { - if (minimapContainer) minimapContainer.style.display = 'none'; - return; - } - - // Calculate minimap size - const maxSize = 150; - const scale = Math.min(maxSize / editCanvas.width, maxSize / editCanvas.height); - - minimapCanvas.width = editCanvas.width * scale; - minimapCanvas.height = editCanvas.height * scale; - - // Draw thumbnail - const minimapCtx = minimapCanvas.getContext('2d'); - minimapCtx.imageSmoothingEnabled = false; - // REMOVED: drawCheckerboardBackground - prevents checkerboard from being saved with template - minimapCtx.drawImage(editCanvas, 0, 0, minimapCanvas.width, minimapCanvas.height); - - // Update viewport indicator - updateMinimap(); - } - - function updateMinimap() { - const viewport = document.getElementById('minimapViewport'); - const minimapCanvas = document.getElementById('minimapCanvas'); - const container = document.getElementById('editCanvasContainer'); - const editCanvas = document.getElementById('editCanvas'); - - if (!viewport || !minimapCanvas || !container || !editCanvas) return; - - const containerRect = container.getBoundingClientRect(); - const scale = minimapCanvas.width / editCanvas.width; - - // Calculate visible area in minimap coordinates - const visibleWidth = Math.min(containerRect.width / editState.zoom * scale, minimapCanvas.width); - const visibleHeight = Math.min(containerRect.height / editState.zoom * scale, minimapCanvas.height); - - const viewportX = (minimapCanvas.width / 2) - (editState.panX / editState.zoom * scale) - (visibleWidth / 2); - const viewportY = (minimapCanvas.height / 2) - (editState.panY / editState.zoom * scale) - (visibleHeight / 2); - - viewport.style.width = `${visibleWidth}px`; - viewport.style.height = `${visibleHeight}px`; - viewport.style.left = `${Math.max(0, Math.min(minimapCanvas.width - visibleWidth, viewportX))}px`; - viewport.style.top = `${Math.max(0, Math.min(minimapCanvas.height - visibleHeight, viewportY))}px`; - } - - function navigateToMinimapPosition(e) { - const minimapCanvas = document.getElementById('minimapCanvas'); - const editCanvas = document.getElementById('editCanvas'); - - if (!minimapCanvas || !editCanvas) return; - - const rect = minimapCanvas.getBoundingClientRect(); - const x = (e.clientX - rect.left) / minimapCanvas.width; - const y = (e.clientY - rect.top) / minimapCanvas.height; - - // Convert to canvas coordinates and center - const targetX = (x - 0.5) * editCanvas.width * editState.zoom; - const targetY = (y - 0.5) * editCanvas.height * editState.zoom; - - editState.panX = -targetX; - editState.panY = -targetY; - - constrainPan(); - updateCanvasTransform(); - updateMinimap(); - } - - function updateStatusBar(x, y) { - const statusBar = document.getElementById('editStatusBar'); - if (statusBar) { - const zoomPercent = Math.round(editState.zoom * 100); - statusBar.textContent = `Position: (${x}, ${y}) | Color: ${editState.currentColor || '#000000'} | Zoom: ${zoomPercent}%`; - } - } - - function handleEyedropper(x, y) { - const editCanvas = document.getElementById('editCanvas'); - const ctx = editCanvas.getContext('2d'); - - if (x >= 0 && x < editCanvas.width && y >= 0 && y < editCanvas.height) { - const imageData = ctx.getImageData(x, y, 1, 1); - const [r, g, b] = imageData.data; - const pickedColor = `#${r.toString(16).padStart(2, '0')}${g.toString(16).padStart(2, '0')}${b.toString(16).padStart(2, '0')}`; - - editState.currentColor = pickedColor; - const currentColorDisplay = document.getElementById('currentColorDisplay'); - if (currentColorDisplay) { - currentColorDisplay.style.backgroundColor = pickedColor; - } - - // Try to find matching color in palette - document.querySelectorAll('.edit-color-btn').forEach(btn => { - btn.classList.remove('active'); - if (btn.dataset.colorHex === pickedColor) { - btn.classList.add('active'); - editState.currentColorId = parseInt(btn.dataset.colorId); - } - }); - - updateStatusBar(x, y); - } - } - - function floodFill(startX, startY, fillColor) { - const editCanvas = document.getElementById('editCanvas'); - const ctx = editCanvas.getContext('2d'); - const imageData = ctx.getImageData(0, 0, editCanvas.width, editCanvas.height); - const data = imageData.data; - - if (startX < 0 || startX >= editCanvas.width || startY < 0 || startY >= editCanvas.height) return; - - const startIndex = (startY * editCanvas.width + startX) * 4; - const startR = data[startIndex]; - const startG = data[startIndex + 1]; - const startB = data[startIndex + 2]; - const startA = data[startIndex + 3]; - - // Convert fill color to RGB - const fillR = parseInt(fillColor.slice(1, 3), 16); - const fillG = parseInt(fillColor.slice(3, 5), 16); - const fillB = parseInt(fillColor.slice(5, 7), 16); - - // Don't fill if the color is already the same - if (startR === fillR && startG === fillG && startB === fillB) return; - - const pixelsToCheck = [{ x: startX, y: startY }]; - const checkedPixels = new Set(); - - while (pixelsToCheck.length > 0) { - const { x, y } = pixelsToCheck.pop(); - const key = `${x},${y}`; - - if (checkedPixels.has(key)) continue; - checkedPixels.add(key); - - if (x < 0 || x >= editCanvas.width || y < 0 || y >= editCanvas.height) continue; - - const index = (y * editCanvas.width + x) * 4; - const r = data[index]; - const g = data[index + 1]; - const b = data[index + 2]; - const a = data[index + 3]; - - if (r === startR && g === startG && b === startB && a === startA) { - data[index] = fillR; - data[index + 1] = fillG; - data[index + 2] = fillB; - data[index + 3] = 255; - - pixelsToCheck.push({ x: x + 1, y }); - pixelsToCheck.push({ x: x - 1, y }); - pixelsToCheck.push({ x, y: y + 1 }); - pixelsToCheck.push({ x, y: y - 1 }); - } - } - - ctx.putImageData(imageData, 0, 0); - } - - // Brush preview removed per user request - function hideBrushPreview() { - // No-op - brush preview disabled - } - - function drawGrid(ctx, width, height) { - if (!editState.showGrid || editState.zoom < 4) return; - - ctx.save(); - ctx.strokeStyle = 'rgba(128, 128, 128, 0.3)'; - ctx.lineWidth = 1 / editState.zoom; - - for (let x = 0; x <= width; x++) { - ctx.beginPath(); - ctx.moveTo(x, 0); - ctx.lineTo(x, height); - ctx.stroke(); - } - - for (let y = 0; y <= height; y++) { - ctx.beginPath(); - ctx.moveTo(0, y); - ctx.lineTo(width, y); - ctx.stroke(); - } - - ctx.restore(); - } - - function drawCheckerboardBackground(ctx, width, height) { - const checkerSize = 8; // Size of each checker square - ctx.fillStyle = '#f0f0f0'; // Light gray - ctx.fillRect(0, 0, width, height); - - ctx.fillStyle = '#e0e0e0'; // Slightly darker gray - for (let x = 0; x < width; x += checkerSize) { - for (let y = 0; y < height; y += checkerSize) { - if ((Math.floor(x / checkerSize) + Math.floor(y / checkerSize)) % 2 === 1) { - ctx.fillRect(x, y, checkerSize, checkerSize); - } - } - } - } - - function redrawCanvas() { - if (editState.undoStack.length === 0) return; - - const currentState = editState.undoStack[editState.undoStack.length - 1]; - const editCanvas = document.getElementById('editCanvas'); - const ctx = editCanvas.getContext('2d'); - - const img = new Image(); - img.onload = () => { - ctx.clearRect(0, 0, editCanvas.width, editCanvas.height); - // REMOVED: drawCheckerboardBackground - prevents checkerboard from being saved with template - ctx.drawImage(img, 0, 0); - drawGrid(ctx, editCanvas.width, editCanvas.height); - }; - img.src = currentState; - } - - function setupTouchSupport() { - const canvas = document.getElementById('editCanvas'); - if (!canvas) return; - - let touchStartTime = 0; - - canvas.addEventListener('touchstart', handleTouchStart, { passive: false }); - canvas.addEventListener('touchmove', handleTouchMove, { passive: false }); - canvas.addEventListener('touchend', handleTouchEnd, { passive: false }); - - function handleTouchStart(e) { - e.preventDefault(); - touchStartTime = Date.now(); - - if (e.touches.length === 1) { - // Single touch - start painting - const touch = e.touches[0]; - const coords = mapClientToCanvas(touch.clientX, touch.clientY); - if (coords) { - editState.isDrawing = true; - editState.lastPaintPos = { x: coords.x, y: coords.y }; - paintAtPosition(coords.x, coords.y, true); - } - } else if (e.touches.length === 2) { - // Two finger - prepare for zoom/pan - const touch1 = e.touches[0]; - const touch2 = e.touches[1]; - - editState.lastTouchDistance = Math.hypot( - touch2.clientX - touch1.clientX, - touch2.clientY - touch1.clientY - ); - - editState.lastTouchCenter = { - x: (touch1.clientX + touch2.clientX) / 2, - y: (touch1.clientY + touch2.clientY) / 2 - }; - } - } - - function handleTouchMove(e) { - e.preventDefault(); - - if (e.touches.length === 1 && editState.isDrawing) { - // Continue painting - const touch = e.touches[0]; - const coords = mapClientToCanvas(touch.clientX, touch.clientY); - if (coords) { - paintAtPosition(coords.x, coords.y, true); - } - } else if (e.touches.length === 2) { - // Pinch zoom and pan - const touch1 = e.touches[0]; - const touch2 = e.touches[1]; - - const currentDistance = Math.hypot( - touch2.clientX - touch1.clientX, - touch2.clientY - touch1.clientY - ); - - const currentCenter = { - x: (touch1.clientX + touch2.clientX) / 2, - y: (touch1.clientY + touch2.clientY) / 2 - }; - - // Zoom based on distance change - if (editState.lastTouchDistance > 0) { - const zoomFactor = currentDistance / editState.lastTouchDistance; - const newZoom = Math.max(0.1, Math.min(32, editState.zoom * zoomFactor)); - zoomToPoint(newZoom, currentCenter.x, currentCenter.y); - } - - // Pan based on center movement - editState.panX += currentCenter.x - editState.lastTouchCenter.x; - editState.panY += currentCenter.y - editState.lastTouchCenter.y; - - editState.lastTouchDistance = currentDistance; - editState.lastTouchCenter = currentCenter; - - constrainPan(); - updateCanvasTransform(); - updateMinimap(); - } - } - - function handleTouchEnd(e) { - e.preventDefault(); - - if (editState.isDrawing) { - editState.isDrawing = false; - editState.lastPaintPos = null; - saveEditState(); - } - - if (e.touches.length === 0) { - editState.lastTouchDistance = 0; - } - } - } - - function setupKeyboardShortcuts() { - document.addEventListener('keydown', (e) => { - // Only handle shortcuts when edit panel is visible - const editOverlay = document.getElementById('editOverlay'); - if (!editOverlay || editOverlay.style.display === 'none') return; - - // Prevent default for handled keys - const handledKeys = ['b', 'e', 'i', 'f', 'g', 'z', '[', ']']; - if (handledKeys.includes(e.key.toLowerCase()) || (e.ctrlKey && e.key.toLowerCase() === 'z')) { - e.preventDefault(); - } - - switch (e.key.toLowerCase()) { - case 'b': // Brush - selectTool('paint'); - break; - case 'e': // Erase - selectTool('erase'); - break; - case 'i': // Eyedropper - selectTool('eyedropper'); - break; - case 'f': // Fill - selectTool('fill'); - break; - case 'g': // Grid - const showGrid = document.getElementById('showGrid'); - if (showGrid) showGrid.click(); - break; - case 'z': - if (e.ctrlKey || e.metaKey) { - if (e.shiftKey) { - redoEdit(); - } else { - undoEdit(); - } - } - break; - case '=': - case '+': - if (e.ctrlKey || e.metaKey) { - zoomIn(); - } - break; - case '-': - if (e.ctrlKey || e.metaKey) { - zoomOut(); - } - break; - case '0': - if (e.ctrlKey || e.metaKey) { - fitToWindow(); - } - break; - case '1': - if (e.ctrlKey || e.metaKey) { - setZoom(1); - } - break; - case '[': // Decrease brush size - const brushSize = document.getElementById('brushSize'); - const currentSize = parseInt(brushSize.value); - if (currentSize > 1) { - brushSize.value = currentSize - 1; - document.getElementById('brushSizeValue').textContent = currentSize - 1; - updateBrushSize(currentSize - 1); - } - break; - case ']': // Increase brush size - const brushSize2 = document.getElementById('brushSize'); - const currentSize2 = parseInt(brushSize2.value); - if (currentSize2 < 20) { - brushSize2.value = currentSize2 + 1; - document.getElementById('brushSizeValue').textContent = currentSize2 + 1; - updateBrushSize(currentSize2 + 1); - } - break; - } - }); - } - - function applyEditChanges() { - try { - const editCanvas = document.getElementById('editCanvas'); - if (!editCanvas) { - throw new Error('Edit canvas not found'); - } - - // Find the resize canvas in the resize panel - const resizeCanvas = document.getElementById('resizeCanvas'); - if (!resizeCanvas) { - throw new Error('Resize canvas not found'); - } - - const baseCtx = resizeCanvas.getContext('2d'); - if (!baseCtx) { - throw new Error('Resize canvas context not available'); - } - - // Make sure the resize canvas has the same dimensions as the edit canvas - if (resizeCanvas.width !== editCanvas.width || resizeCanvas.height !== editCanvas.height) { - resizeCanvas.width = editCanvas.width; - resizeCanvas.height = editCanvas.height; - } - - // Clear resize canvas - baseCtx.clearRect(0, 0, resizeCanvas.width, resizeCanvas.height); - - // Copy edited image to resize canvas (no checkerboard since we removed it entirely) - baseCtx.imageSmoothingEnabled = false; - baseCtx.drawImage(editCanvas, 0, 0); - - // CRITICAL: Completely replace the template system with edited artwork - const editedImageData = editCanvas.toDataURL(); - - // Create new processor with edited image as the template - if (window.WPlaceImageProcessor) { - const newProcessor = new window.WPlaceImageProcessor(editedImageData); - newProcessor.load().then(() => { - // Get the processed pixel data from the edited canvas - const editCtx = editCanvas.getContext('2d'); - const editImageData = editCtx.getImageData(0, 0, editCanvas.width, editCanvas.height); - const pixels = editImageData.data; - - // Count valid pixels in the edited image - let totalValidPixels = 0; - for (let i = 0; i < pixels.length; i += 4) { - const a = pixels[i + 3]; - const r = pixels[i]; - const g = pixels[i + 1]; - const b = pixels[i + 2]; - - const isTransparent = !state.paintTransparentPixels && a < state.customTransparencyThreshold; - const isWhiteAndSkipped = !state.paintWhitePixels && Utils.isWhitePixel(r, g, b); - - if (!isTransparent && !isWhiteAndSkipped) { - totalValidPixels++; - } - } - - // COMPLETELY REBUILD state.imageData with the edited artwork - state.imageData = { - width: editCanvas.width, - height: editCanvas.height, - pixels: pixels, - totalPixels: totalValidPixels, - processor: newProcessor, - }; - - // CRITICAL: Update state.originalImage so resize panel uses edited artwork as base template - state.originalImage = { - dataUrl: editedImageData, - width: editCanvas.width, - height: editCanvas.height - }; - - // Update state with new totals - state.totalPixels = totalValidPixels; - state.paintedPixels = 0; // Reset progress since this is a new template - state.currentPaintingColor = null; // Reset color tracking - state.imageLoaded = true; - - // Update local processors - if (typeof processor !== 'undefined') { - processor = newProcessor; - console.log('🔄 Updated local processor with edited artwork'); - } - if (typeof baseProcessor !== 'undefined') { - baseProcessor = newProcessor; - console.log('🔄 Updated baseProcessor with edited artwork'); - } - - // Force regeneration of overlays by clearing cached mask data - if (typeof window._maskImageData !== 'undefined') { - delete window._maskImageData; - } - - // Update UI to reflect the new template - if (typeof updateUI === 'function') { - updateUI(); - } - - // Show loading and properly reload resize panel with new template - Utils.showAlert('Updating template... Please wait.', 'info'); - - // Give more time for template to fully update and force resize panel reload - setTimeout(() => { - // Hide edit overlay first - document.getElementById('editOverlay').style.display = 'none'; - - // Completely reload resize dialog with updated processor - setTimeout(() => { - // Clean up existing dialog - if (typeof _resizeDialogCleanup === 'function') { - _resizeDialogCleanup(); - } - - // Force complete reload of resize dialog with new processor - setTimeout(() => { - showResizeDialog(newProcessor); - - console.log('✅ Template COMPLETELY replaced with edited artwork'); - console.log(`📊 New template stats: ${editCanvas.width}x${editCanvas.height}, ${totalValidPixels} pixels`); - Utils.showAlert('Template successfully replaced with your edited artwork!', 'success'); - }, 100); - }, 300); - }, 100); - }).catch((error) => { - console.error('Failed to load new processor:', error); - Utils.showAlert('Failed to update template. Please try again.', 'error'); - }); - } else { - console.error('WPlaceImageProcessor not available'); - Utils.showAlert('Image processor not available. Please reload the page.', 'error'); - } - - console.log('✅ Edit changes applied - template replacement in progress'); - } catch (error) { - console.error('Error applying edit changes:', error); - Utils.showAlert('Failed to apply changes. Please try again.', 'error'); - } - } - - if (uploadBtn) { - uploadBtn.addEventListener('click', async () => { - const availableColors = Utils.extractAvailableColors(); - if (availableColors === null || availableColors.length < 10) { - updateUI('noColorsFound', 'error'); - Utils.showAlert(Utils.t('noColorsFound'), 'error'); - return; - } - - if (!state.colorsChecked) { - state.availableColors = availableColors; - state.colorsChecked = true; - updateUI('colorsFound', 'success', { count: availableColors.length }); - await updateStats(); - selectPosBtn.disabled = false; - // Only enable resize button if image is also loaded - if (state.imageLoaded) { - resizeBtn.disabled = false; - } - } - - try { - updateUI('loadingImage', 'default'); - const imageSrc = await Utils.createImageUploader(); - if (!imageSrc) { - updateUI('colorsFound', 'success', { - count: state.availableColors.length, - }); - return; - } - - const processor = new ImageProcessor(imageSrc); - await processor.load(); - - const { width, height } = processor.getDimensions(); - const pixels = processor.getPixelData(); - - let totalValidPixels = 0; - for (let i = 0; i < pixels.length; i += 4) { - const isTransparent = - !state.paintTransparentPixels && - pixels[i + 3] < (state.customTransparencyThreshold || CONFIG.TRANSPARENCY_THRESHOLD); - const isWhiteAndSkipped = - !state.paintWhitePixels && - Utils.isWhitePixel(pixels[i], pixels[i + 1], pixels[i + 2]); - if (!isTransparent && !isWhiteAndSkipped) { - totalValidPixels++; - } - } - - state.imageData = { - width, - height, - pixels, - totalPixels: totalValidPixels, - processor, - }; - - state.totalPixels = totalValidPixels; - state.paintedPixels = 0; - state.currentPaintingColor = null; // Reset color tracking - state.imageLoaded = true; - - // Reset session-specific flags when a new image is loaded - state.preFilteringDone = false; - state.progressResetDone = false; - console.log('🔄 Reset session flags for new image load'); - - // Keep existing lastPosition to continue from where we left off - // state.lastPosition = { x: 0, y: 0 }; // REMOVED: Don't reset position - - // Initialize painted map for tracking - Utils.initializePaintedMap(width, height); - - // New image: clear previous resize settings - state.resizeSettings = null; - // Also clear any previous ignore mask - state.resizeIgnoreMask = null; - // Save original image for this browser (dataUrl + dims) - state.originalImage = { dataUrl: imageSrc, width, height }; - saveBotSettings(); - - // Use the original image for the overlay initially - const imageBitmap = await createImageBitmap(processor.img); - await overlayManager.setImage(imageBitmap); - overlayManager.enable(); - toggleOverlayBtn.disabled = false; - toggleOverlayBtn.classList.add('active'); - toggleOverlayBtn.setAttribute('aria-pressed', 'true'); - - // Only enable resize button if colors have also been captured - if (state.colorsChecked) { - resizeBtn.disabled = false; - } - saveBtn.disabled = false; - - if (state.startPosition) { - startBtn.disabled = false; - } - - await updateStats(); - updateDataButtons(); - updateUI('imageLoaded', 'success', { count: totalValidPixels }); - } catch { - updateUI('imageError', 'error'); - } - }); - } - - // 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 = []; - } - - // Reset session-specific flags when loading extracted file data - state.preFilteringDone = false; - state.progressResetDone = false; - console.log('🔄 Reset session flags for extracted file load'); - - // 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 files, force enable all necessary flags for full functionality - state.colorsChecked = true; - state.currentPaintingColor = null; // Reset color tracking - state.imageLoaded = true; - - // Ensure image processor is available for extracted files - if (state.imageData && !state.imageData.processor) { - try { - // Create a temporary canvas to generate a data URL for the processor - const canvas = document.createElement('canvas'); - canvas.width = state.imageData.width; - canvas.height = state.imageData.height; - const ctx = canvas.getContext('2d'); - - // Create image data from pixels - const imageData = new ImageData( - new Uint8ClampedArray(state.imageData.pixels), - state.imageData.width, - state.imageData.height - ); - ctx.putImageData(imageData, 0, 0); - - // Create processor with canvas data URL using the correct class - const dataUrl = canvas.toDataURL(); - state.imageData.processor = new window.WPlaceImageProcessor(dataUrl); - - // CRITICAL: Load the processor before using it - await state.imageData.processor.load(); - console.log('🔧 Created and loaded WPlaceImageProcessor for extracted artwork'); - } catch (error) { - console.error('❌ Failed to create image processor for extracted artwork:', error); - // Fallback: use global processor if available - if (window.globalImageProcessor) { - console.log('🔄 Using global image processor as fallback'); - state.imageData.processor = window.globalImageProcessor; - } - } - } - - console.log('🎨 Force-enabled all flags for extracted JSON to access resize panel'); - - // 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 with custom message for extracted artwork - Utils.showAlert('🎨 Extracted artwork loaded! Please click on the canvas to select where to place it.', 'info'); - updateUI('ready', 'default'); // Use 'ready' instead of 'waitingPosition' to avoid translation issues - - // Enable relevant buttons - selectPosBtn.disabled = false; - // Always enable resize button for extracted files since they have image processor - resizeBtn.disabled = false; - console.log('🔧 Resize button enabled for extracted artwork'); - - // Enable move artwork button (will be fully enabled after position is set) - const moveArtworkBtn = document.getElementById('moveArtworkBtn'); - if (moveArtworkBtn) { - moveArtworkBtn.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'); - - // Enable Move Artwork button when position is set - const moveArtworkBtn = document.getElementById('moveArtworkBtn'); - if (moveArtworkBtn) { - moveArtworkBtn.disabled = false; - } - - 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', () => { - console.log('🔍 [DEBUG] Resize button clicked. State check:', { - imageLoaded: state.imageLoaded, - hasProcessor: !!(state.imageData && state.imageData.processor), - colorsChecked: state.colorsChecked, - hasAvailableColors: !!(state.availableColors && state.availableColors.length > 0) - }); - - if (state.imageLoaded && state.imageData && state.imageData.processor) { - showResizeDialog(state.imageData.processor); - } else { - let message = 'Please upload an image and check colors first.'; - if (!state.imageLoaded) message = 'Please upload an image first.'; - else if (!state.imageData || !state.imageData.processor) message = 'Image processor not available. Please reload the image.'; - Utils.showAlert(message, 'warning'); - } - }); - } - - // Move Artwork button event listener - const moveArtworkBtn = document.getElementById('moveArtworkBtn'); - if (moveArtworkBtn) { - moveArtworkBtn.addEventListener('click', () => { - if (state.imageLoaded && (state.startPosition || state.selectingPosition)) { - showMoveArtworkPanel(); - } else if (!state.imageLoaded) { - Utils.showAlert('Please upload or load an image first', 'warning'); - } else { - Utils.showAlert('Please select a position for the artwork first', 'warning'); - } - }); - } - - if (selectPosBtn) { - selectPosBtn.addEventListener('click', async () => { - if (state.selectingPosition) return; - - state.selectingPosition = true; - state.startPosition = null; - state.region = null; - startBtn.disabled = true; - - // Disable Move Artwork button when selecting new position - const moveArtworkBtn = document.getElementById('moveArtworkBtn'); - if (moveArtworkBtn) { - moveArtworkBtn.disabled = true; - } - - Utils.showAlert(Utils.t('selectPositionAlert'), 'info'); - updateUI('waitingPosition', 'default'); - - const tempFetch = async (url, options) => { - if ( - typeof url === 'string' && - url.includes('https://backend.wplace.live/s0/pixel/') && - options?.method?.toUpperCase() === 'POST' - ) { - try { - const response = await originalFetch(url, options); - const clonedResponse = response.clone(); - const data = await clonedResponse.json(); - - if (data?.painted === 1) { - const regionMatch = url.match(/\/pixel\/(\d+)\/(\d+)/); - if (regionMatch && regionMatch.length >= 3) { - state.region = { - x: Number.parseInt(regionMatch[1]), - y: Number.parseInt(regionMatch[2]), - }; - } - - const payload = JSON.parse(options.body); - if (payload?.coords && Array.isArray(payload.coords)) { - state.startPosition = { - x: payload.coords[0], - y: payload.coords[1], - }; - // Keep existing lastPosition to continue from where we left off - // state.lastPosition = { x: 0, y: 0 }; // REMOVED: Don't reset position - - // 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; - - // Enable Move Artwork button when position is set - const moveArtworkBtn = document.getElementById('moveArtworkBtn'); - if (moveArtworkBtn) { - moveArtworkBtn.disabled = false; - } - } - - window.fetch = originalFetch; - state.selectingPosition = false; - updateUI('positionSet', 'success'); - } - } - - return response; - } catch { - return originalFetch(url, options); - } - } - return originalFetch(url, options); - }; - - const originalFetch = window.fetch; - window.fetch = tempFetch; - - setTimeout(() => { - if (state.selectingPosition) { - window.fetch = originalFetch; - state.selectingPosition = false; - updateUI('positionTimeout', 'error'); - Utils.showAlert(Utils.t('positionTimeout'), 'error'); - } - }, 120000); - }); - } - - async function startPainting() { - if (!state.imageLoaded || !state.startPosition || !state.region) { - updateUI('missingRequirements', 'error'); - return; - } - await ensureToken(); - if (!getTurnstileToken()) return; - - // Only perform progressive pixel detection on first start of session - if (!state.preFilteringDone) { - // Perform progressive pixel detection from top-left to bottom-right - console.log('🔍 Starting progressive pixel detection from top-left to bottom-right...'); - await performProgressivePixelDetection(); - - // Mark pre-filtering as done to prevent duplicate scanning - state.preFilteringDone = true; - console.log('✅ Pre-filtering marked as complete - will not scan again this session'); - } else { - console.log('🔄 Continuing session - pre-filtering already done'); - } - - state.running = true; - state.stopFlag = false; - startBtn.disabled = true; - stopBtn.disabled = false; - uploadBtn.disabled = true; - selectPosBtn.disabled = true; - resizeBtn.disabled = true; - saveBtn.disabled = true; - toggleOverlayBtn.disabled = true; - - updateUI('startPaintingMsg', 'success'); - - try { - await getAccounts(); - await processImage(); - } catch (e) { - console.error('Unexpected error:', e); - updateUI('paintingError', 'error'); - } finally { - state.running = false; - stopBtn.disabled = true; - saveBtn.disabled = false; - - if (state.stopFlag) { - startBtn.disabled = false; - } else { - startBtn.disabled = true; - uploadBtn.disabled = false; - const loadExtractedBtn = document.getElementById('loadExtractedBtn'); - if (loadExtractedBtn) loadExtractedBtn.disabled = false; - selectPosBtn.disabled = false; - resizeBtn.disabled = false; - } - toggleOverlayBtn.disabled = false; - } - } - - if (startBtn) { - startBtn.addEventListener('click', startPainting); - } - - if (stopBtn) { - stopBtn.addEventListener('click', () => { - state.stopFlag = true; - state.running = false; - stopBtn.disabled = true; - updateUI('paintingStoppedByUser', 'warning'); - - if (state.imageLoaded && state.paintedPixels > 0) { - Utils.saveProgress(); - Utils.showAlert(Utils.t('autoSaved'), 'success'); - } - }); - } - - const checkSavedProgress = () => { - const savedData = Utils.loadProgress(); - if (savedData && savedData.state.paintedPixels > 0) { - const savedDate = new Date(savedData.timestamp).toLocaleString(); - const progress = Math.round( - (savedData.state.paintedPixels / savedData.state.totalPixels) * 100 - ); - - Utils.showAlert( - `${Utils.t('savedDataFound')}\n\n` + - `Saved: ${savedDate}\n` + - `Progress: ${savedData.state.paintedPixels}/${savedData.state.totalPixels} pixels (${progress}%)\n` + - `${Utils.t('clickLoadToContinue')}`, - 'info' - ); - } - }; - - setTimeout(checkSavedProgress, 1000); - - if (cooldownSlider && cooldownInput && cooldownValue && cooldownDecrease && cooldownIncrease) { - const updateCooldown = (newValue) => { - const threshold = Math.max(1, Math.min(state.maxCharges || 999, parseInt(newValue))); - state.cooldownChargeThreshold = threshold; - - // Update both controls (value shows in input, label shows unit only) - cooldownSlider.value = threshold; - cooldownInput.value = threshold; - cooldownValue.textContent = `${Utils.t('charges')}`; - - saveBotSettings(); - NotificationManager.resetEdgeTracking(); // prevent spurious notify after threshold change - }; - - // Slider event listener - cooldownSlider.addEventListener('input', (e) => { - updateCooldown(e.target.value); - }); - - // Number input event listener - cooldownInput.addEventListener('input', (e) => { - updateCooldown(e.target.value); - }); - - // Decrease button - cooldownDecrease.addEventListener('click', () => { - updateCooldown(parseInt(cooldownInput.value) - 1); - }); - - // Increase button - cooldownIncrease.addEventListener('click', () => { - updateCooldown(parseInt(cooldownInput.value) + 1); - }); - - // Add scroll-to-adjust for cooldown slider - Utils.createScrollToAdjust(cooldownSlider, updateCooldown, 1, state.maxCharges, 1); - - // Skip cooldown button event handler - const skipCooldownBtn = document.getElementById('skipCooldownBtn'); - if (skipCooldownBtn) { - skipCooldownBtn.addEventListener('click', () => { - if (state.preciseCurrentCharges < state.cooldownChargeThreshold) { - console.log(`[Auto-Image] Skip cooldown requested - resetting to account index 0`); - - // Reset to account index 0 (start new cycle) - state.currentActiveIndex = 0; - console.log(`🔄 Reset currentActiveIndex to 0 for new cycle`); - - // Reset charges to threshold to bypass cooldown - state.preciseCurrentCharges = state.cooldownChargeThreshold; - console.log(`[Auto-Image] Cooldown skipped! Charges set to threshold: ${state.cooldownChargeThreshold}`); - - // Switch to first account if we have multiple accounts - if (accountManager.getAccountCount() > 1) { - const firstAccountInfo = accountManager.getAccountByIndex(0); - if (firstAccountInfo && firstAccountInfo.token) { - console.log(`🔄 Switching to first account: index 0 (${firstAccountInfo.displayName})`); - // Get accounts array for the switch - const accounts = JSON.parse(localStorage.getItem("accounts")) || []; - if (accounts.length > 0) { - switchToSpecificAccount(firstAccountInfo.token, firstAccountInfo.displayName).then(() => { - console.log(`✅ Successfully switched to account index 0 after cooldown skip`); - // Update UI immediately - updateChargesThresholdUI(0); - // Trigger immediate check for painting - if (state.isEnabled && !state.stopFlag) { - setTimeout(() => { - checkAndPaint(); - }, 100); - } - }).catch(err => { - console.error(`❌ Failed to switch to account index 0:`, err); - }); - } else { - console.warn(`⚠️ No accounts available for switching`); - } - } else { - console.warn(`⚠️ First account info not found or missing token`); - } - } else { - console.log(`📝 Single account mode - no switching needed`); - // Update UI immediately - updateChargesThresholdUI(0); - // Trigger immediate check for painting - if (state.isEnabled && !state.stopFlag) { - setTimeout(() => { - checkAndPaint(); - }, 100); - } - } - } - }); - } - } - - loadBotSettings(); - // Ensure notification poller reflects current settings - NotificationManager.syncFromState(); - } - - function getMsToTargetCharges(current, target, cooldown, intervalMs = 0) { - const remainingCharges = target - current; - return Math.max(0, remainingCharges * cooldown - intervalMs); - } - - function updateChargesThresholdUI(intervalMs) { - if (state.stopFlag) return; - - const threshold = state.cooldownChargeThreshold; - const remainingMs = getMsToTargetCharges( - state.preciseCurrentCharges, - threshold, - state.cooldown, - intervalMs - ); - const timeText = Utils.msToTimeText(remainingMs); - - updateUI( - 'noChargesThreshold', - 'warning', - { - threshold, - current: state.displayCharges, - time: timeText, - }, - true - ); - - // Update skip cooldown button state - const skipCooldownBtn = document.getElementById('skipCooldownBtn'); - if (skipCooldownBtn) { - const isInCooldown = state.preciseCurrentCharges < threshold && remainingMs > 0; - skipCooldownBtn.disabled = !isInCooldown; - skipCooldownBtn.title = isInCooldown - ? `Skip cooldown (${timeText} remaining)` - : 'Skip cooldown (only available during cooldown)'; - } - } - - // Fast tile-based pixel detection from top-left to bottom-right - async function performProgressivePixelDetection() { - if (!state.imageLoaded || !state.imageData) { - console.log('⚠️ No image loaded, skipping pixel detection'); - return; - } - - const startTime = performance.now(); - const { width, height } = state.imageData; - const startX = state.startPosition.x; - const startY = state.startPosition.y; - const regionX = state.region.x; - const regionY = state.region.y; - - let detectedPixels = 0; - let totalChecked = 0; - - console.log(`🚀 Fast scanning ${width}x${height} image from top-left (0,0) to bottom-right (${width - 1},${height - 1})...`); - - updateUI('pixelDetection', 'info', { message: 'Fast detecting already painted pixels...' }); - - // Calculate affected tiles - const worldX1 = startX; - const worldY1 = startY; - const worldX2 = startX + width - 1; - const worldY2 = startY + height - 1; - - const startTileX = Math.floor(worldX1 / 1000); - const startTileY = Math.floor(worldY1 / 1000); - const endTileX = Math.floor(worldX2 / 1000); - const endTileY = Math.floor(worldY2 / 1000); - - console.log(`📄 Processing tiles from (${startTileX},${startTileY}) to (${endTileX},${endTileY})`); - - // Cache for downloaded tile data - const tileDataCache = new Map(); - - // Download and cache all required tiles in parallel - const tilePromises = []; - for (let tileY = startTileY; tileY <= endTileY; tileY++) { - for (let tileX = startTileX; tileX <= endTileX; tileX++) { - const absoluteTileX = regionX + tileX; - const absoluteTileY = regionY + tileY; - const tileKey = `${absoluteTileX},${absoluteTileY}`; - - tilePromises.push( - downloadTileImageData(absoluteTileX, absoluteTileY).then(imageData => { - if (imageData) { - tileDataCache.set(tileKey, imageData); - } - }).catch(e => { - console.warn(`⚠️ Failed to download tile ${absoluteTileX},${absoluteTileY}:`, e.message); - }) - ); - } - } - - // Wait for all tiles to download - await Promise.all(tilePromises); - console.log(`📦 Downloaded ${tileDataCache.size} tiles for fast pixel checking`); - - // Fast pixel detection using cached tile data - for (let y = 0; y < height; y++) { - for (let x = 0; x < width; x++) { - totalChecked++; - - // Check if pixel is eligible for painting - const targetPixelInfo = checkPixelEligibility(x, y); - if (!targetPixelInfo.eligible) { - continue; // Skip non-eligible pixels - } - - // Calculate absolute world coordinates - const absX = startX + x; - const absY = startY + y; - const adderX = Math.floor(absX / 1000); - const adderY = Math.floor(absY / 1000); - const pixelX = absX % 1000; - const pixelY = absY % 1000; - const absoluteTileX = regionX + adderX; - const absoluteTileY = regionY + adderY; - - // Check if already marked as painted in local map - if (Utils.isPixelPainted(x, y, absoluteTileX, absoluteTileY)) { - detectedPixels++; - continue; - } - - // Fast pixel color check using cached tile data - const tileKey = `${absoluteTileX},${absoluteTileY}`; - const tileImageData = tileDataCache.get(tileKey); - - if (tileImageData) { - try { - // Direct array access - much faster than canvas operations - const tileWidth = tileImageData.width; - const tileHeight = tileImageData.height; - const data = tileImageData.data; - - // Ensure pixel coordinates are within tile bounds - if (pixelX >= 0 && pixelX < tileWidth && pixelY >= 0 && pixelY < tileHeight) { - const pixelIndex = (pixelY * tileWidth + pixelX) * 4; - const r = data[pixelIndex]; - const g = data[pixelIndex + 1]; - const b = data[pixelIndex + 2]; - const a = data[pixelIndex + 3]; - - // Check alpha threshold - const alphaThresh = state.customTransparencyThreshold || CONFIG.TRANSPARENCY_THRESHOLD; - if (a >= alphaThresh) { - const existingMappedColor = Utils.resolveColor( - [r, g, b], - state.availableColors, - !state.paintUnavailablePixels - ); - const isAlreadyPainted = existingMappedColor.id === targetPixelInfo.mappedColorId; - - if (isAlreadyPainted) { - // Check if pixel is already marked as painted to avoid double counting - if (!Utils.isPixelPainted(x, y, absoluteTileX, absoluteTileY)) { - // Mark as painted in the map but DO NOT increment progress counter - // Progress counter should only reflect actual painting sequence position - Utils.markPixelPainted(x, y, absoluteTileX, absoluteTileY); - detectedPixels++; - } else { - // Pixel already tracked, just count it for detection stats - detectedPixels++; - } - } - } - } - } catch (e) { - // Skip pixels we can't check - console.warn(`⚠️ Could not check pixel (${x}, ${y}):`, e.message); - } - } - - // Update progress periodically - if (totalChecked % 2500 === 0) { - const progress = Math.round((totalChecked / (width * height)) * 100); - updateUI('pixelDetection', 'info', { - message: `Fast detecting... ${progress}% (Found: ${detectedPixels})` - }); - // Yield control briefly to prevent UI blocking - await new Promise(resolve => setTimeout(resolve, 0)); - } - } - } - - const processingTime = Math.round(performance.now() - startTime); - console.log(`🏁 Fast pixel detection complete in ${processingTime}ms:`); - console.log(` - Total pixels checked: ${totalChecked}`); - console.log(` - Already painted pixels found: ${detectedPixels}`); - console.log(` - Updated progress: ${state.paintedPixels}/${state.totalPixels}`); - console.log(` - Performance: ${Math.round(totalChecked / (processingTime / 1000))} pixels/second`); - - // Update progress display - await updateStats(); - updateUI('pixelDetectionComplete', 'success', { - message: `Found ${detectedPixels} already painted pixels in ${processingTime}ms` - }); - } - - // Fast tile download and ImageData extraction (similar to Art-Extractor approach) - async function downloadTileImageData(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}`); - } - - const blob = await response.blob(); - return await processTileBlob(blob); - } catch (error) { - console.warn(`Failed to download tile ${tileX},${tileY}:`, error.message); - return null; - } - } - - // Process tile blob into ImageData (adapted from Art-Extractor) - 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.imageSmoothingEnabled = false; - ctx.drawImage(img, 0, 0); - const imageData = ctx.getImageData(0, 0, canvas.width, canvas.height); - resolve(imageData); - }; - img.onerror = () => reject(new Error('Failed to load image')); - img.src = URL.createObjectURL(blob); - }); - } catch (error) { - console.error('Error processing tile blob:', error); - return null; - } - } - - function generateCoordinates(width, height, mode, direction, snake, blockWidth, blockHeight, startFromX = 0, startFromY = 0) { - const coords = []; - console.log( - 'Generating coordinates with \n mode:', - mode, - '\n direction:', - direction, - '\n snake:', - snake, - '\n blockWidth:', - blockWidth, - '\n blockHeight:', - blockHeight, - '\n startFromX:', - startFromX, - '\n startFromY:', - startFromY - ); - // --------- Standard 4 corners traversal ---------- - let xStart, xEnd, xStep; - let yStart, yEnd, yStep; - switch (direction) { - case 'top-left': - xStart = 0; - xEnd = width; - xStep = 1; - yStart = 0; - yEnd = height; - yStep = 1; - break; - case 'top-right': - xStart = width - 1; - xEnd = -1; - xStep = -1; - yStart = 0; - yEnd = height; - yStep = 1; - break; - case 'bottom-left': - xStart = 0; - xEnd = width; - xStep = 1; - yStart = height - 1; - yEnd = -1; - yStep = -1; - break; - case 'bottom-right': - xStart = width - 1; - xEnd = -1; - xStep = -1; - yStart = height - 1; - yEnd = -1; - yStep = -1; - break; - default: - throw new Error(`Unknown direction: ${direction}`); - } - - // --------- Traversal modes ---------- - if (mode === 'rows') { - let rowIndex = 0; - for (let y = yStart; y !== yEnd; y += yStep) { - if (snake && rowIndex % 2 !== 0) { - for (let x = xEnd - xStep; x !== xStart - xStep; x -= xStep) { - coords.push([x, y]); - } - } else { - for (let x = xStart; x !== xEnd; x += xStep) { - coords.push([x, y]); - } - } - rowIndex++; - } - } else if (mode === 'columns') { - let colIndex = 0; - for (let x = xStart; x !== xEnd; x += xStep) { - if (snake && colIndex % 2 !== 0) { - for (let y = yEnd - yStep; y !== yStart - yStep; y -= yStep) { - coords.push([x, y]); - } - } else { - for (let y = yStart; y !== yEnd; y += yStep) { - coords.push([x, y]); - } - } - colIndex++; - } - } else if (mode === 'circle-out') { - const cx = Math.floor(width / 2); - const cy = Math.floor(height / 2); - const maxRadius = Math.ceil(Math.sqrt(cx * cx + cy * cy)); - - for (let r = 0; r <= maxRadius; r++) { - for (let y = cy - r; y <= cy + r; y++) { - for (let x = cx - r; x <= cx + r; x++) { - if (x >= 0 && x < width && y >= 0 && y < height) { - const dist = Math.max(Math.abs(x - cx), Math.abs(y - cy)); - if (dist === r) coords.push([x, y]); - } - } - } - } - } else if (mode === 'circle-in') { - const cx = Math.floor(width / 2); - const cy = Math.floor(height / 2); - const maxRadius = Math.ceil(Math.sqrt(cx * cx + cy * cy)); - - for (let r = maxRadius; r >= 0; r--) { - for (let y = cy - r; y <= cy + r; y++) { - for (let x = cx - r; x <= cx + r; x++) { - if (x >= 0 && x < width && y >= 0 && y < height) { - const dist = Math.max(Math.abs(x - cx), Math.abs(y - cy)); - if (dist === r) coords.push([x, y]); - } - } - } - } - } else if (mode === 'blocks' || mode === 'shuffle-blocks') { - const blocks = []; - for (let by = 0; by < height; by += blockHeight) { - for (let bx = 0; bx < width; bx += blockWidth) { - const block = []; - for (let y = by; y < Math.min(by + blockHeight, height); y++) { - for (let x = bx; x < Math.min(bx + blockWidth, width); x++) { - block.push([x, y]); - } - } - blocks.push(block); - } - } - - if (mode === 'shuffle-blocks') { - // Simple Fisher-Yates shuffle - for (let i = blocks.length - 1; i > 0; i--) { - const j = Math.floor(Math.random() * (i + 1)); - [blocks[i], blocks[j]] = [blocks[j], blocks[i]]; - } - } - - // Append all blocks without spread/concat to avoid large allocations and argument explosion - for (const block of blocks) { - for (let i = 0; i < block.length; i++) { - coords.push(block[i]); - } - } - } else { - throw new Error(`Unknown mode: ${mode}`); - } - - // Filter coordinates to start from the specified position - 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]; - if (x === startFromX && y === startFromY) { - startIndex = i; - break; - } - } - - if (startIndex >= 0) { - // Resume from the found position (skip all previous coordinates) - const filteredCoords = coords.slice(startIndex); - console.log(`✂️ Resuming: skipped ${startIndex} coordinates, continuing with ${filteredCoords.length} remaining`); - return filteredCoords; - } else { - console.warn(`⚠️ Resume position (${startFromX}, ${startFromY}) not found in coordinate list, starting from beginning`); - } - } - - return coords; - } - - async function flushPixelBatch(pixelBatch) { - if (!pixelBatch || pixelBatch.pixels.length === 0) return true; - - const batchSize = pixelBatch.pixels.length; - console.log( - `📦 Sending batch with ${batchSize} pixels (region: ${pixelBatch.regionX},${pixelBatch.regionY})` - ); - const success = await sendBatchWithRetry( - pixelBatch.pixels, - pixelBatch.regionX, - pixelBatch.regionY - ); - if (success) { - // Only increment progress for actually painted pixels to prevent multiplication - const actuallyPaintedCount = pixelBatch.pixels.length; - state.paintedPixels += actuallyPaintedCount; - console.log(`📊 Added ${actuallyPaintedCount} painted pixels to progress (total: ${state.paintedPixels})`); - - pixelBatch.pixels.forEach((p) => { - Utils.markPixelPainted(p.x, p.y, pixelBatch.regionX, pixelBatch.regionY); - }); - - // Update last painted position to the last pixel in the successful batch - if (pixelBatch.pixels.length > 0) { - const lastPixel = pixelBatch.pixels[pixelBatch.pixels.length - 1]; - // FIXED: Use localX/localY (image-relative coordinates) instead of x/y (absolute canvas coordinates) - state.lastPaintedPosition = { x: lastPixel.localX, y: lastPixel.localY }; - } - - // 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, - }; - await updateStats(); - // Update account list with new charges - await updateCurrentAccountInList(); - // Progress tracking removed from UI to reduce visual clutter - Utils.performSmartSave(); - - if (CONFIG.PAINTING_SPEED_ENABLED && state.paintingSpeed > 0 && batchSize > 0) { - const delayPerPixel = 1000 / state.paintingSpeed; - const totalDelay = Math.max(100, delayPerPixel * batchSize); - await Utils.sleep(totalDelay); - } - } else { - console.error(`❌ Batch failed permanently after retries. Stopping painting.`); - state.stopFlag = true; - updateUI('paintingBatchFailed', 'error'); - } - - pixelBatch.pixels = []; - return success; - } - - 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!'); - state.currentPaintingColor = null; // Reset color tracking - break; - } - - if (paintingResult === 'stopped') { - console.log('⏹️ Painting stopped by user'); - break; - } - - if (paintingResult === 'charges_depleted') { - if (CONFIG.autoBuyToggle && CONFIG.autoBuy != 'none') { - console.log('Trying to buy more charges before swapping account...'); - const purchaseResult = await purchase(CONFIG.autoBuy); - if (purchaseResult == 2) { - console.log('✅ Purchase successful, continuing painting'); - await updateStats(); - await updateCurrentAccountInList(); - if (CONFIG.autoBuy == 'paint_charges') continue; - } - else if (purchaseResult == 1) { - console.log('😭 Not enough droplets to buy more charges, swapping account.'); - } - else { - console.log('🤔 Purchase failed.'); - } - } - else console.log('🤫 Auto buy is disabled, wait for cooldown or swapping account.'); - 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; - break; - } - } 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; - break; - } - } else { - // Debug current state - const totalAccounts = accountManager.getAccountCount(); - console.log(`📊 Account Status - Current index: ${accountManager.currentIndex}, Total accounts: ${totalAccounts}`); - - // Check if we're at the last account in the sequence - const isLastAccount = accountManager.currentIndex >= totalAccounts - 1; - console.log(`🔍 Is last account? ${isLastAccount} (index ${accountManager.currentIndex} of ${totalAccounts})`); - - if (!isLastAccount && totalAccounts > 1) { - // Update current account status before switching - console.log('📊 Updating current account status before switch...'); - await updateCurrentAccountInList(); - - // Switch to next account immediately (no cooldown) - only if we have multiple accounts - const nextAccount = accountManager.getNextAccount(); - console.log(`🔄 Switching to next account: ${nextAccount?.displayName} (${accountManager.currentIndex + 2}/${totalAccounts})`); - const switchResult = await switchToNextAccount(accounts); - if (!switchResult) { - console.log('❌ Account switch failed, stopping'); - state.stopFlag = true; - break; - } - // Continue painting with new account immediately - continue; - } else if (totalAccounts === 1) { - // Only one account available - use cooldown and continue with same account - console.log('ℹ️ Only one account available, entering cooldown period'); - const cooldownResult = await executeCooldownPeriod(); - if (cooldownResult === 'stopped') break; - console.log('✅ Cooldown complete, continuing with same account'); - continue; - } else { - // Last account reached - use cooldown then switch to first account - console.log('⏱️ Last account reached, entering cooldown period'); - console.log(`📊 Current account: index ${accountManager.currentIndex}, Last account: index ${totalAccounts - 1}`); - - const cooldownResult = await executeCooldownPeriod(); - if (cooldownResult === 'stopped') break; - - // After cooldown, switch to first account - console.log('🔁 Cooldown complete, switching to first account'); - console.log(`🔄 Before switch - Current index: ${accountManager.currentIndex}, Target: index 0`); - - const firstAccountInfo = accountManager.getAccountByIndex(0); - const firstAccountToken = firstAccountInfo?.token; - - if (!firstAccountToken) { - console.log('❌ First account token not found, stopping'); - state.stopFlag = true; - break; - } - - // Reset to first position - accountManager.setCurrentIndex(0); - - const switchResult = await switchToSpecificAccount(firstAccountToken, firstAccountInfo.displayName); - 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 { - await finalizePaintingProcess(); - } - } - - // Phase 1: Execute a complete painting session using all available charges - async function executePaintingSession() { - console.log('🎨 Starting painting session - using all charges until 0'); - const { width, height, pixels } = state.imageData; - const { x: startX, y: startY } = state.startPosition; - const { x: regionX, y: regionY } = state.region; - - // 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( - regionX, - regionY, - width, - height, - startX, - startY, - 10000 // timeout 10s - ); - - if (!tilesReady) { - updateUI('overlayTilesNotLoaded', 'error'); - state.stopFlag = true; - return 'stopped'; - } - } - - let pixelBatch = null; - let skippedPixels = { - transparent: 0, - white: 0, - alreadyPainted: 0, - colorUnavailable: 0, - }; - - // IMPORTANT: Check charges once at start, then paint until depleted - console.log('🔋 Checking initial charges for painting session'); - const initialChargeCheck = await WPlaceService.getCharges(); - 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'; - } - - console.log(`🔋 Starting with ${state.displayCharges} charges - painting until depleted`); - - // Paint pixels until we run out of charges or complete the image - try { - - // Generate the actual filtered coordinates for painting (resuming from last position) - const coords = await generateCoordinatesAsync( - width, - height, - state.coordinateMode, - state.coordinateDirection, - state.coordinateSnake, - state.blockWidth, - state.blockHeight, - state.lastPosition.x, - state.lastPosition.y - ); - - // OPTIMIZATION: Pre-filter already painted pixels (happens only once per session) - let eligibleCoords = []; - let alreadyPaintedCount = 0; - - 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; - let adderX = Math.floor(absX / 1000); - let adderY = Math.floor(absY / 1000); - let pixelX = absX % 1000; - let pixelY = absY % 1000; - - try { - const tilePixelRGBA = await overlayManager.getTilePixelColor( - regionX + adderX, - regionY + adderY, - pixelX, - pixelY - ); - - if (tilePixelRGBA && Array.isArray(tilePixelRGBA)) { - const mappedCanvasColor = Utils.resolveColor( - tilePixelRGBA.slice(0, 3), - state.availableColors, - !state.paintUnavailablePixels // Use same parameter as target pixel - ); - const isMatch = mappedCanvasColor.id === targetPixelInfo.mappedColorId; - if (isMatch) { - alreadyPaintedCount++; - // Mark as painted in map but DO NOT increment progress counter - // Progress should only reflect actual painting sequence position - Utils.markPixelPainted(x, y, regionX + adderX, regionY + adderY); - continue; // Skip already painted pixels - } - } - } 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 (not added to progress counter)`); - console.log('ℹ️ This detection will not happen again until a new image/save is loaded'); - console.log('📊 Progress counter only reflects actual painting sequence position'); - // No need to update stats since progress wasn't changed - } - skippedPixels.alreadyPainted = alreadyPaintedCount; - } else { - // Pre-filtering already done this session, just filter for basic eligibility - 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 - let absX = startX + x; - let absY = startY + y; - let adderX = Math.floor(absX / 1000); - let adderY = Math.floor(absY / 1000); - - if (!Utils.isPixelPainted(x, y, regionX + adderX, regionY + adderY)) { - eligibleCoords.push([x, y, targetPixelInfo]); - } - } - } - - // Group pixels by color if color-by-color mode is enabled - let pixelsToProcess = eligibleCoords; - if (state.paintingOrder === 'color-by-color') { - console.log('🎨 Color-by-color mode enabled - grouping pixels by color'); - - // Group pixels by color ID - const colorGroups = new Map(); - for (const [x, y, targetPixelInfo] of eligibleCoords) { - const colorId = targetPixelInfo.mappedColorId; - if (!colorGroups.has(colorId)) { - colorGroups.set(colorId, []); - } - colorGroups.get(colorId).push([x, y, targetPixelInfo]); - } - - // Log color groups - console.log(`📊 Found ${colorGroups.size} different colors to paint:`); - for (const [colorId, pixels] of colorGroups.entries()) { - const colorInfo = Object.values(CONFIG.COLOR_MAP).find(c => c.id === colorId); - const colorName = colorInfo ? colorInfo.name : `Color ${colorId}`; - console.log(` 🎨 ${colorName} (ID: ${colorId}): ${pixels.length} pixels`); - } - - // Process colors one by one - const sortedColorGroups = Array.from(colorGroups.entries()).sort((a, b) => a[0] - b[0]); - - // If we have a current painting color, resume from that color - let startIndex = 0; - if (state.currentPaintingColor !== null) { - startIndex = sortedColorGroups.findIndex(([colorId]) => colorId === state.currentPaintingColor); - if (startIndex === -1) startIndex = 0; - console.log(`🔄 Resuming from color ID ${state.currentPaintingColor} (index ${startIndex})`); - } - - // Flatten the groups starting from the current color - pixelsToProcess = []; - for (let i = startIndex; i < sortedColorGroups.length; i++) { - const [colorId, pixels] = sortedColorGroups[i]; - // Avoid using spread or concat with very large arrays to prevent call stack/alloc overhead - for (let j = 0; j < pixels.length; j++) { - pixelsToProcess.push(pixels[j]); - } - } - - console.log(`✅ Prepared ${pixelsToProcess.length} pixels for color-by-color painting`); - } - - // Paint eligible pixels (already pre-filtered, no duplicate checks) - outerLoop: for (const [x, y, targetPixelInfo] of pixelsToProcess) { - // Track current color being painted in color-by-color mode - if (state.paintingOrder === 'color-by-color') { - if (state.currentPaintingColor !== targetPixelInfo.mappedColorId) { - state.currentPaintingColor = targetPixelInfo.mappedColorId; - const colorInfo = Object.values(CONFIG.COLOR_MAP).find(c => c.id === state.currentPaintingColor); - const colorName = colorInfo ? colorInfo.name : `Color ${state.currentPaintingColor}`; - console.log(`🎨 Now painting: ${colorName} (ID: ${state.currentPaintingColor})`); - - // Update UI to show current color - const statusDiv = document.getElementById('statusDiv'); - if (statusDiv) { - const colorMessage = Utils.t('currentlyPaintingColor', { colorName }); - const colorIndicator = document.getElementById('currentColorIndicator'); - if (colorIndicator) { - colorIndicator.textContent = colorMessage; - } else { - const indicator = document.createElement('div'); - indicator.id = 'currentColorIndicator'; - indicator.textContent = colorMessage; - indicator.style.cssText = 'margin-top: 8px; padding: 8px; background: rgba(255,255,255,0.1); border-radius: 6px; font-weight: bold;'; - statusDiv.appendChild(indicator); - } - } - } - } - - if (state.stopFlag) { - if (pixelBatch && pixelBatch.pixels.length > 0) { - console.log(`🎯 Sending last batch before stop with ${pixelBatch.pixels.length} pixels`); - await flushPixelBatch(pixelBatch); - } - state.lastPosition = { x, y }; - // Show paused coordinates in UI with proper translation template - // Use last painted position if available, otherwise use current position - const pausedX = state.lastPaintedPosition.x || x; - const pausedY = state.lastPaintedPosition.y || y; - updateUI('paintingPaused', 'warning', { x: pausedX, y: pausedY }); - console.log(`⏸️ Painting paused after last painted coordinates (${pausedX}, ${pausedY})`); - return 'stopped'; - } - - // Check if we have charges left (local count, no API call) - if (state.displayCharges <= 0) { - // 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'; - } - // else { - // console.log(`🔋 Charges after purchase: ${state.displayCharges}, continuing painting`); - // } - } - - let absX = startX + x; - let absY = startY + y; - - let adderX = Math.floor(absX / 1000); - let adderY = Math.floor(absY / 1000); - let pixelX = absX % 1000; - let pixelY = absY % 1000; - - // CRITICAL FIX: Always check if pixel is already painted (both locally and on canvas) - if (Utils.isPixelPainted(x, y, regionX + adderX, regionY + adderY)) { - console.log(`⏭️ Skipping already painted pixel at (${x}, ${y}) - marked in local map`); - continue; // Skip already painted pixels - } - - // REAL-TIME CANVAS CHECK: Verify against actual canvas state to prevent overpainting - try { - const existingColorRGBA = await overlayManager.getTilePixelColor( - regionX + adderX, - regionY + adderY, - pixelX, - pixelY - ).catch(() => null); - - if (existingColorRGBA && Array.isArray(existingColorRGBA)) { - const [er, eg, eb] = existingColorRGBA; - const existingMappedColor = Utils.resolveColor( - [er, eg, eb], - state.availableColors, - !state.paintUnavailablePixels - ); - const isAlreadyCorrect = existingMappedColor.id === targetPixelInfo.mappedColorId; - - if (isAlreadyCorrect) { - console.log(`✅ Pixel at (${x}, ${y}) already has correct color (${existingMappedColor.id}) - marking as painted`); - // Mark it as painted in local map but DO NOT increment progress counter - // Progress should only reflect actual painting sequence position - Utils.markPixelPainted(x, y, regionX + adderX, regionY + adderY); - continue; // Skip painting this pixel - } - } - } catch (e) { - // If we can't check the canvas, proceed with painting (better to attempt than skip) - console.warn(`⚠️ Could not verify canvas state for pixel (${x}, ${y}), proceeding with paint:`, e.message); - } - - const targetMappedColorId = targetPixelInfo.mappedColorId; - - // Set up pixel batch for new region if needed - if ( - !pixelBatch || - pixelBatch.regionX !== regionX + adderX || - pixelBatch.regionY !== regionY + adderY - ) { - if (pixelBatch && pixelBatch.pixels.length > 0) { - console.log(`🌍 Sending region-change batch with ${pixelBatch.pixels.length} pixels`); - const success = await flushPixelBatch(pixelBatch); - if (!success) { - console.error(`❌ Batch failed permanently after retries. Stopping painting.`); - state.stopFlag = true; - return 'stopped'; - } - await updateStats(); - } - - pixelBatch = { - regionX: regionX + adderX, - regionY: regionY + adderY, - pixels: [], - }; - } - - // Add pixel to batch (no need to check again - already pre-filtered) - pixelBatch.pixels.push({ - x: pixelX, - y: pixelY, - color: targetMappedColorId, - localX: x, - localY: y, - }); - - // Send batch if it's full - const maxBatchSize = calculateBatchSize(); - if (pixelBatch.pixels.length >= maxBatchSize) { - console.log(`📦 Sending batch with ${pixelBatch.pixels.length} pixels`); - const success = await flushPixelBatch(pixelBatch); - if (!success) { - console.error(`❌ Batch failed permanently after retries. Stopping painting.`); - state.stopFlag = true; - return 'stopped'; - } - pixelBatch.pixels = []; - await updateStats(); - } - } - - // Send final batch if any pixels remain - if (pixelBatch && pixelBatch.pixels.length > 0 && !state.stopFlag) { - console.log(`🏁 Sending final batch with ${pixelBatch.pixels.length} pixels`); - const success = await flushPixelBatch(pixelBatch); - if (!success) { - console.warn(`⚠️ Final batch failed with ${pixelBatch.pixels.length} pixels`); - } - } - - // If we completed the entire coordinate loop, image is complete - return state.stopFlag ? 'stopped' : 'completed'; - - } finally { - // Log skip statistics for this session - console.log(`📊 Session Statistics:`); - console.log(` New pixels painted: ${state.paintedPixels - (skippedPixels.alreadyPainted || 0)}`); - console.log(` Already painted detected: ${skippedPixels.alreadyPainted}`); - console.log(` Total progress: ${state.paintedPixels}`); - console.log(` Pre-filtered - Transparent: ${skippedPixels.transparent}`); - console.log(` Pre-filtered - White: ${skippedPixels.white}`); - console.log(` Pre-filtered - Color Unavailable: ${skippedPixels.colorUnavailable}`); - } - } - - // Phase 2: Execute cooldown period - wait for target charges (NO token regeneration) - async function executeCooldownPeriod() { - console.log('⏱️ Entering cooldown period - waiting for target charges'); - console.log('🚫 NO token regeneration during cooldown (even if expired/invalid)'); - - // Check initial charges to calculate wait time - let chargeCheckCount = 0; - // const maxChargeChecks = 10; // REMOVED: No limit on API calls during cooldown - - while (!state.stopFlag) { - chargeCheckCount++; - - const { charges, cooldown } = await WPlaceService.getCharges(); - state.displayCharges = Math.floor(charges); - state.preciseCurrentCharges = charges; - state.cooldown = cooldown; - - if (state.displayCharges >= state.cooldownChargeThreshold) { - console.log(`✅ Cooldown target reached: ${state.displayCharges}/${state.cooldownChargeThreshold}`); - NotificationManager.maybeNotifyChargesReached(true); - await updateStats(); - return 'target_reached'; - } - - updateUI('noChargesThreshold', 'warning', { - time: Utils.msToTimeText(state.cooldown), - threshold: state.cooldownChargeThreshold, - current: state.displayCharges, - }); - await updateStats(); - - // 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) { - // First few checks - shorter delay - delayTime = Math.max(3000, state.cooldown); // 3 seconds minimum - } else if (estimatedWaitTime > 60000) { - // Long wait expected - check every 15 seconds - delayTime = 10000; - } else if (estimatedWaitTime > 30000) { - // Medium wait - check every 15 seconds - delayTime = 15000; - } else { - // Close to target - check every 5 seconds - delayTime = 5000; - } - - console.log(`⏱️ Cooldown check ${chargeCheckCount}: ${state.displayCharges}/${state.cooldownChargeThreshold} charges, waiting 10s before next check`); - await Utils.sleep(10000); - - // REMOVED: No limit on charge checks - bot will wait infinitely until charges are available - // if (chargeCheckCount >= maxChargeChecks) { - // console.warn('⚠️ Max charge checks reached during cooldown, continuing anyway'); - // break; - // } - } - - return 'stopped'; - } - - // 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) { - console.error('❌ Token regeneration failed:', error); - return false; - } - } - - // Finalize painting process cleanup - async function finalizePaintingProcess() { - console.log('🧹 Finalizing painting process'); - - if (window._chargesInterval) { - clearInterval(window._chargesInterval); - window._chargesInterval = null; - } - - if (state.stopFlag) { - // Save progress when stopped to preserve painted map - Utils.saveProgress(); - } else { - updateUI('paintingComplete', 'success', { count: state.paintedPixels }); - state.lastPosition = { x: 0, y: 0 }; // Only reset when truly complete - Utils.saveProgress(); // Save final complete state - overlayManager.clear(); - const toggleOverlayBtn = document.getElementById('toggleOverlayBtn'); - if (toggleOverlayBtn) { - toggleOverlayBtn.classList.remove('active'); - toggleOverlayBtn.disabled = true; - } - } - } - - // Helper function to check pixel eligibility (shared by painting functions) - function checkPixelEligibility(x, y) { - const { width, height, pixels } = state.imageData; - const transparencyThreshold = state.customTransparencyThreshold || CONFIG.TRANSPARENCY_THRESHOLD; - - // CRITICAL FIX: Check module availability before processing - if (!Utils || typeof Utils.isWhitePixel !== 'function') { - console.error('❌ Utils module not available for pixel eligibility check'); - return { - eligible: false, - reason: 'moduleUnavailable', - }; - } - - const idx = (y * width + x) * 4; - const r = pixels[idx], - g = pixels[idx + 1], - b = pixels[idx + 2], - a = pixels[idx + 3]; - - if (!state.paintTransparentPixels && a < transparencyThreshold) - return { - eligible: false, - reason: 'transparent', - }; - if (!state.paintWhitePixels && Utils.isWhitePixel(r, g, b)) - return { - eligible: false, - reason: 'white', - }; - - let targetRgb = Utils.isWhitePixel(r, g, b) - ? [255, 255, 255] - : Utils.findClosestPaletteColor(r, g, b, state.activeColorPalette); - - const mappedTargetColorId = Utils.resolveColor( - targetRgb, - state.availableColors, - !state.paintUnavailablePixels - ); - - if (!state.paintUnavailablePixels && !mappedTargetColorId.id) { - return { - eligible: false, - reason: 'colorUnavailable', - r, - g, - b, - a, - mappedColorId: mappedTargetColorId.id, - }; - } - return { eligible: true, r, g, b, a, mappedColorId: mappedTargetColorId.id }; - } - - // Helper function to skip pixel and log the reason (minimized logging) - function skipPixel(reason, id, rgb, x, y, skippedPixels) { - // Minimize logging to prevent console flooding - only log non-routine skips - if (reason !== 'transparent' && reason !== 'alreadyPainted') { - console.log(`Skipped pixel for ${reason} (id: ${id}, (${rgb.join(', ')})) at (${x}, ${y})`); - } - skippedPixels[reason]++; - } - function calculateBatchSize() { - let targetBatchSize; - - // If speed control is disabled, use all available charges - if (!CONFIG.PAINTING_SPEED_ENABLED) { - targetBatchSize = state.displayCharges; - console.log(`🚀 Speed control disabled: using all ${targetBatchSize} available charges`); - return Math.max(1, targetBatchSize); - } - - // CRITICAL FIX: When speed control is ENABLED, use actual available charges as the batch size - // This ensures bot sends exactly the number of pixels matching available charges - // Example: 75 charges → send 75 pixels, 14 charges → send 14 pixels - const availableCharges = state.displayCharges; - - if (state.batchMode === 'random') { - // Generate random batch size within the specified range - const min = Math.max(1, state.randomBatchMin); - const max = Math.max(min, state.randomBatchMax); - targetBatchSize = Math.floor(Math.random() * (max - min + 1)) + min; - console.log(`🎲 Random batch size generated: ${targetBatchSize} (range: ${min}-${max})`); - } else { - // Normal mode - use the fixed paintingSpeed value - targetBatchSize = state.paintingSpeed; - } - - // FIXED: Use actual available charges as the maximum batch size - // This matches the available pixels exactly (e.g., 75 charges = 75 pixel batch) - const finalBatchSize = Math.min(targetBatchSize, availableCharges); - - console.log(`📊 Batch size: ${finalBatchSize} (target: ${targetBatchSize}, available: ${availableCharges})`); - - return Math.max(1, finalBatchSize); - } - - // Helper function to retry batch until success with exponential backoff - async function sendBatchWithRetry(pixels, regionX, regionY, maxRetries = MAX_BATCH_RETRIES) { - let attempt = 0; - while (attempt < maxRetries && !state.stopFlag) { - attempt++; - console.log( - `🔄 Attempting to send batch (attempt ${attempt}/${maxRetries}) for region ${regionX},${regionY} with ${pixels.length} pixels` - ); - - const result = await sendPixelBatch(pixels, regionX, regionY); - - if (result === true) { - console.log(`✅ Batch succeeded on attempt ${attempt}`); - return true; - } else if (result === 'token_error') { - console.log(`🔑 Token error on attempt ${attempt} - no token available during processing`); - console.log(`❌ Stopping batch processing - tokens must be generated at startup/start button only`); - updateUI('captchaFailed', 'error'); - await Utils.sleep(2000); // Wait longer before retrying after token failure - continue; // Continue to retry until maxRetries reached - } else if (result === 'token_regenerated') { - console.log(`🔄 Token regenerated on attempt ${attempt} after 403 error - retrying batch`); - const pausedX = state.lastPaintedPosition.x; - const pausedY = state.lastPaintedPosition.y; - updateUI('paintingPaused', 'warning', { x: pausedX, y: pausedY }); - // Don't count token regeneration as a failed attempt, retry immediately - attempt--; - await Utils.sleep(500); // Brief pause before retry - continue; - } else if (result === 'token_regeneration_failed') { - console.log(`❌ Token regeneration failed on attempt ${attempt} after 403 error`); - updateUI('captchaFailed', 'error'); - return false; // Stop processing if we can't get a valid token - } else if (result === 'invalid_token_error') { - console.log(`🔑 Invalid token detected on attempt ${attempt}, regenerating...`); - updateUI('captchaSolving', 'warning'); - try { - await handleCaptcha(true); // Allow generation for invalid token cases - // Don't count token regeneration as a failed attempt - attempt--; - continue; - } catch (e) { - console.error(`❌ Token regeneration failed after invalid token on attempt ${attempt}:`, e); - updateUI('captchaFailed', 'error'); - // Wait longer before retrying after token failure - await Utils.sleep(5000); - } - } else { - console.warn(`⚠️ Batch failed on attempt ${attempt}, retrying...`); - // Exponential backoff with jitter - const baseDelay = Math.min(1000 * Math.pow(2, attempt - 1), 30000); // Max 30s - const jitter = Math.random() * 1000; // Add up to 1s random delay - await Utils.sleep(baseDelay + jitter); - } - } - - if (attempt >= maxRetries) { - console.error( - `❌ Batch failed after ${maxRetries} attempts (MAX_BATCH_RETRIES=${MAX_BATCH_RETRIES}). This will stop painting to prevent infinite loops.` - ); - updateUI('paintingError', 'error'); - return false; - } - - return false; - } - - async function sendPixelBatch(pixelBatch, regionX, regionY) { - let token = getTurnstileToken(); - - // Don't auto-generate tokens during processing - return error if no token available - if (!token) { - console.warn('⚠️ No token available and auto-generation disabled during processing'); - return 'token_error'; - } - - const coords = new Array(pixelBatch.length * 2); - const colors = new Array(pixelBatch.length); - for (let i = 0; i < pixelBatch.length; i++) { - const pixel = pixelBatch[i]; - coords[i * 2] = pixel.x; - coords[i * 2 + 1] = pixel.y; - colors[i] = pixel.color; - } - - try { - const payload = { coords, colors, t: token, fp: fpStr32 }; - var wasmtoken = await createWasmToken(regionX, regionY, payload); - const res = await fetch(`https://backend.wplace.live/s0/pixel/${regionX}/${regionY}`, { - method: 'POST', - headers: { 'Content-Type': 'text/plain;charset=UTF-8', "x-pawtect-token": wasmtoken }, - credentials: 'include', - body: JSON.stringify(payload), - }); - - if (res.status === 403) { - let data = null; - try { - 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) { - console.log('✅ Token regenerated after 403 error, returning regenerate signal'); - return 'token_regenerated'; - } else { - console.error('❌ Failed to regenerate token after 403 error'); - return 'token_regeneration_failed'; - } - } - - const data = await res.json(); - return data?.painted === pixelBatch.length; - } catch (e) { - console.error('Batch paint request failed:', e); - return false; - } - } - - function saveBotSettings() { - try { - const settings = { - paintingSpeed: state.paintingSpeed, - paintingSpeedEnabled: document.getElementById('enableSpeedToggle')?.checked, - batchMode: state.batchMode, // "normal" or "random" - paintingOrder: state.paintingOrder, // "sequential" or "color-by-color" - randomBatchMin: state.randomBatchMin, - randomBatchMax: state.randomBatchMax, - cooldownChargeThreshold: state.cooldownChargeThreshold, - tokenSource: state.tokenSource, // "generator", "hybrid", or "manual" - minimized: state.minimized, - overlayOpacity: state.overlayOpacity, - blueMarbleEnabled: document.getElementById('enableBlueMarbleToggle')?.checked, - ditheringEnabled: state.ditheringEnabled, - colorMatchingAlgorithm: state.colorMatchingAlgorithm, - enableChromaPenalty: state.enableChromaPenalty, - chromaPenaltyWeight: state.chromaPenaltyWeight, - customTransparencyThreshold: state.customTransparencyThreshold, - customWhiteThreshold: state.customWhiteThreshold, - paintWhitePixels: state.paintWhitePixels, - paintTransparentPixels: state.paintTransparentPixels, - resizeSettings: state.resizeSettings, - paintUnavailablePixels: state.paintUnavailablePixels, - coordinateMode: state.coordinateMode, - coordinateDirection: state.coordinateDirection, - coordinateSnake: state.coordinateSnake, - blockWidth: state.blockWidth, - blockHeight: state.blockHeight, // Save ignore mask (as base64) with its dimensions - resizeIgnoreMask: - state.resizeIgnoreMask && - state.resizeSettings && - state.resizeSettings.width * state.resizeSettings.height === state.resizeIgnoreMask.length - ? { - w: state.resizeSettings.width, - h: state.resizeSettings.height, - data: btoa(String.fromCharCode(...state.resizeIgnoreMask)), - } - : null, // Notifications - notificationsEnabled: state.notificationsEnabled, - notifyOnChargesReached: state.notifyOnChargesReached, - notifyOnlyWhenUnfocused: state.notifyOnlyWhenUnfocused, - notificationIntervalMinutes: state.notificationIntervalMinutes, - originalImage: state.originalImage, - }; - CONFIG.PAINTING_SPEED_ENABLED = settings.paintingSpeedEnabled; - // AUTO_CAPTCHA_ENABLED is always true - no need to save/load - - localStorage.setItem('wplace-bot-settings', JSON.stringify(settings)); - } catch (e) { - console.warn('Could not save bot settings:', e); - } - } - - function loadBotSettings() { - try { - const saved = localStorage.getItem('wplace-bot-settings'); - if (!saved) return; - const settings = JSON.parse(saved); - - state.paintingSpeed = settings.paintingSpeed || CONFIG.PAINTING_SPEED.DEFAULT; - state.batchMode = settings.batchMode || CONFIG.BATCH_MODE; // Default to "normal" - state.paintingOrder = settings.paintingOrder || CONFIG.PAINTING_ORDER; // Default to "sequential" - state.randomBatchMin = settings.randomBatchMin || CONFIG.RANDOM_BATCH_RANGE.MIN; - state.randomBatchMax = settings.randomBatchMax || CONFIG.RANDOM_BATCH_RANGE.MAX; - state.cooldownChargeThreshold = - settings.cooldownChargeThreshold || CONFIG.COOLDOWN_CHARGE_THRESHOLD; - state.tokenSource = settings.tokenSource || CONFIG.TOKEN_SOURCE; // Default to "generator" - state.minimized = settings.minimized ?? false; - CONFIG.PAINTING_SPEED_ENABLED = settings.paintingSpeedEnabled ?? false; - CONFIG.AUTO_CAPTCHA_ENABLED = settings.autoCaptchaEnabled ?? false; - state.overlayOpacity = settings.overlayOpacity ?? CONFIG.OVERLAY.OPACITY_DEFAULT; - state.blueMarbleEnabled = settings.blueMarbleEnabled ?? CONFIG.OVERLAY.BLUE_MARBLE_DEFAULT; - state.ditheringEnabled = settings.ditheringEnabled ?? false; - state.colorMatchingAlgorithm = settings.colorMatchingAlgorithm || 'lab'; - state.enableChromaPenalty = settings.enableChromaPenalty ?? true; - state.chromaPenaltyWeight = settings.chromaPenaltyWeight ?? 0.15; - state.customTransparencyThreshold = - settings.customTransparencyThreshold ?? CONFIG.TRANSPARENCY_THRESHOLD; - state.customWhiteThreshold = settings.customWhiteThreshold ?? CONFIG.WHITE_THRESHOLD; - state.paintWhitePixels = settings.paintWhitePixels ?? true; - state.paintTransparentPixels = settings.paintTransparentPixels ?? false; - state.resizeSettings = settings.resizeSettings ?? null; - state.originalImage = settings.originalImage ?? null; - state.paintUnavailablePixels = settings.paintUnavailablePixels ?? CONFIG.PAINT_UNAVAILABLE; - state.coordinateMode = settings.coordinateMode ?? CONFIG.COORDINATE_MODE; - state.coordinateDirection = settings.coordinateDirection ?? CONFIG.COORDINATE_DIRECTION; - state.coordinateSnake = settings.coordinateSnake ?? CONFIG.COORDINATE_SNAKE; - state.blockWidth = settings.blockWidth ?? CONFIG.COORDINATE_BLOCK_WIDTH; - state.blockHeight = settings.blockHeight ?? CONFIG.COORDINATE_BLOCK_HEIGHT; - // Notifications - state.notificationsEnabled = settings.notificationsEnabled ?? CONFIG.NOTIFICATIONS.ENABLED; - state.notifyOnChargesReached = - settings.notifyOnChargesReached ?? CONFIG.NOTIFICATIONS.ON_CHARGES_REACHED; - state.notifyOnlyWhenUnfocused = - settings.notifyOnlyWhenUnfocused ?? CONFIG.NOTIFICATIONS.ONLY_WHEN_UNFOCUSED; - state.notificationIntervalMinutes = - settings.notificationIntervalMinutes ?? CONFIG.NOTIFICATIONS.REPEAT_MINUTES; - // Restore ignore mask if dims match current resizeSettings - if ( - settings.resizeIgnoreMask && - settings.resizeIgnoreMask.data && - state.resizeSettings && - settings.resizeIgnoreMask.w === state.resizeSettings.width && - settings.resizeIgnoreMask.h === state.resizeSettings.height - ) { - try { - const bin = atob(settings.resizeIgnoreMask.data); - const arr = new Uint8Array(bin.length); - for (let i = 0; i < bin.length; i++) arr[i] = bin.charCodeAt(i); - state.resizeIgnoreMask = arr; - } catch { - state.resizeIgnoreMask = null; - } - } else { - state.resizeIgnoreMask = null; - } - // Initialize coordinate generation UI - const coordinateModeSelect = document.getElementById('coordinateModeSelect'); - if (coordinateModeSelect) coordinateModeSelect.value = state.coordinateMode; - - const coordinateDirectionSelect = document.getElementById('coordinateDirectionSelect'); - if (coordinateDirectionSelect) coordinateDirectionSelect.value = state.coordinateDirection; - - const coordinateSnakeToggle = document.getElementById('coordinateSnakeToggle'); - if (coordinateSnakeToggle) coordinateSnakeToggle.checked = state.coordinateSnake; - - const settingsContainer = document.getElementById('wplace-settings-container'); - const directionControls = settingsContainer.querySelector('#directionControls'); - const snakeControls = settingsContainer.querySelector('#snakeControls'); - const blockControls = settingsContainer.querySelector('#blockControls'); - Utils.updateCoordinateUI({ - mode: state.coordinateMode, - directionControls, - snakeControls, - blockControls, - }); - - const paintUnavailablePixelsToggle = document.getElementById('paintUnavailablePixelsToggle'); - if (paintUnavailablePixelsToggle) { - paintUnavailablePixelsToggle.checked = state.paintUnavailablePixels; - } - - const settingsPaintWhiteToggle = settingsContainer.querySelector('#settingsPaintWhiteToggle'); - if (settingsPaintWhiteToggle) { - settingsPaintWhiteToggle.checked = state.paintWhitePixels; - } - - const settingsPaintTransparentToggle = settingsContainer.querySelector( - '#settingsPaintTransparentToggle' - ); - if (settingsPaintTransparentToggle) { - settingsPaintTransparentToggle.checked = state.paintTransparentPixels; - } - - const speedSlider = document.getElementById('speedSlider'); - const speedInput = document.getElementById('speedInput'); - const speedValue = document.getElementById('speedValue'); - if (speedSlider) speedSlider.value = state.paintingSpeed; - if (speedInput) speedInput.value = state.paintingSpeed; - if (speedValue) speedValue.textContent = `pixels`; - - const enableSpeedToggle = document.getElementById('enableSpeedToggle'); - if (enableSpeedToggle) enableSpeedToggle.checked = CONFIG.PAINTING_SPEED_ENABLED; - - // Painting order UI initialization - const paintingOrderSelect = document.getElementById('paintingOrderSelect'); - if (paintingOrderSelect) paintingOrderSelect.value = state.paintingOrder; - - // Batch mode UI initialization - const batchModeSelect = document.getElementById('batchModeSelect'); - if (batchModeSelect) batchModeSelect.value = state.batchMode; - - const normalBatchControls = document.getElementById('normalBatchControls'); - const randomBatchControls = document.getElementById('randomBatchControls'); - - // Show/hide appropriate controls based on batch mode - if (normalBatchControls && randomBatchControls) { - if (state.batchMode === 'random') { - normalBatchControls.style.display = 'none'; - randomBatchControls.style.display = 'block'; - } else { - normalBatchControls.style.display = 'block'; - randomBatchControls.style.display = 'none'; - } - } - - const randomBatchMin = document.getElementById('randomBatchMin'); - if (randomBatchMin) randomBatchMin.value = state.randomBatchMin; - - const randomBatchMax = document.getElementById('randomBatchMax'); - if (randomBatchMax) randomBatchMax.value = state.randomBatchMax; - - // AUTO_CAPTCHA_ENABLED is always true - no toggle to set - - const cooldownSlider = document.getElementById('cooldownSlider'); - const cooldownInput = document.getElementById('cooldownInput'); - const cooldownValue = document.getElementById('cooldownValue'); - if (cooldownSlider) cooldownSlider.value = state.cooldownChargeThreshold; - if (cooldownInput) cooldownInput.value = state.cooldownChargeThreshold; - if (cooldownValue) cooldownValue.textContent = `${Utils.t('charges')}`; - - const overlayOpacitySlider = document.getElementById('overlayOpacitySlider'); - if (overlayOpacitySlider) overlayOpacitySlider.value = state.overlayOpacity; - const overlayOpacityValue = document.getElementById('overlayOpacityValue'); - if (overlayOpacityValue) - overlayOpacityValue.textContent = `${Math.round(state.overlayOpacity * 100)}%`; - const enableBlueMarbleToggle = document.getElementById('enableBlueMarbleToggle'); - if (enableBlueMarbleToggle) enableBlueMarbleToggle.checked = state.blueMarbleEnabled; - - const tokenSourceSelect = document.getElementById('tokenSourceSelect'); - if (tokenSourceSelect) tokenSourceSelect.value = state.tokenSource; - - const colorAlgorithmSelect = document.getElementById('colorAlgorithmSelect'); - if (colorAlgorithmSelect) colorAlgorithmSelect.value = state.colorMatchingAlgorithm; - const enableChromaPenaltyToggle = document.getElementById('enableChromaPenaltyToggle'); - if (enableChromaPenaltyToggle) enableChromaPenaltyToggle.checked = state.enableChromaPenalty; - const chromaPenaltyWeightSlider = document.getElementById('chromaPenaltyWeightSlider'); - if (chromaPenaltyWeightSlider) chromaPenaltyWeightSlider.value = state.chromaPenaltyWeight; - const chromaWeightValue = document.getElementById('chromaWeightValue'); - if (chromaWeightValue) chromaWeightValue.textContent = state.chromaPenaltyWeight; - const transparencyThresholdInput = document.getElementById('transparencyThresholdInput'); - if (transparencyThresholdInput) - transparencyThresholdInput.value = state.customTransparencyThreshold; - const whiteThresholdInput = document.getElementById('whiteThresholdInput'); - if (whiteThresholdInput) whiteThresholdInput.value = state.customWhiteThreshold; - // Notifications UI - const notifEnabledToggle = document.getElementById('notifEnabledToggle'); - if (notifEnabledToggle) notifEnabledToggle.checked = state.notificationsEnabled; - const notifOnChargesToggle = document.getElementById('notifOnChargesToggle'); - if (notifOnChargesToggle) notifOnChargesToggle.checked = state.notifyOnChargesReached; - const notifOnlyUnfocusedToggle = document.getElementById('notifOnlyUnfocusedToggle'); - if (notifOnlyUnfocusedToggle) - notifOnlyUnfocusedToggle.checked = state.notifyOnlyWhenUnfocused; - const notifIntervalInput = document.getElementById('notifIntervalInput'); - if (notifIntervalInput) notifIntervalInput.value = state.notificationIntervalMinutes; - NotificationManager.resetEdgeTracking(); - } catch (e) { - console.warn('Could not load bot settings:', e); - } - } - - // Initialize Turnstile generator integration - console.log('🚀 WPlace Auto-Image with Turnstile Token Generator loaded'); - console.log('🔑 Turnstile token generator: ALWAYS ENABLED (Background mode)'); - console.log('🎯 Manual pixel captcha solving: Available as fallback/alternative'); - console.log('📱 Turnstile widgets: DISABLED - pure background token generation only!'); - - // Generate fingerprint string for requests - if (!window.WPlaceTokenManager) { - console.error('❌ WPlaceTokenManager not available - dependency loading issue'); - return; - } - const fpStr32 = tokenManager._randStr(32); - console.log('🔑 Generated fingerprint string for API requests'); - - // Function to enable file operations after initial startup setup is complete - function enableFileOperations() { - state.initialSetupComplete = true; - - const loadBtn = document.querySelector('#loadBtn'); - const loadFromFileBtn = document.querySelector('#loadFromFileBtn'); - const uploadBtn = document.querySelector('#uploadBtn'); - - if (loadBtn) { - loadBtn.disabled = false; - loadBtn.title = ''; - // Add a subtle animation to indicate the button is now available - loadBtn.style.animation = 'pulse 0.6s ease-in-out'; - setTimeout(() => { - if (loadBtn) loadBtn.style.animation = ''; - }, 600); - console.log('✅ Load Progress button enabled after initial setup'); - } - - if (loadFromFileBtn) { - loadFromFileBtn.disabled = false; - loadFromFileBtn.title = ''; - // Add a subtle animation to indicate the button is now available - loadFromFileBtn.style.animation = 'pulse 0.6s ease-in-out'; - setTimeout(() => { - if (loadFromFileBtn) loadFromFileBtn.style.animation = ''; - }, 600); - console.log('✅ Load from File button enabled after initial setup'); - } - - if (uploadBtn) { - uploadBtn.disabled = false; - uploadBtn.title = ''; - // Add a subtle animation to indicate the button is now available - uploadBtn.style.animation = 'pulse 0.6s ease-in-out'; - setTimeout(() => { - if (uploadBtn) uploadBtn.style.animation = ''; - }, 600); - 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'); - } - - // Optimized token initialization with better timing and error handling - async function initializeTokenGenerator() { - // Skip if already have valid token - if (isTokenValid()) { - console.log('✅ Valid token already available, skipping initialization'); - updateUI('tokenReady', 'success'); - enableFileOperations(); // Enable file operations since initial setup is complete - return; - } - - try { - console.log('🔧 Initializing Turnstile token generator...'); - updateUI('initializingToken', 'default'); - - console.log('Attempting to load Turnstile script...'); - await Utils.loadTurnstile(); - console.log('Turnstile script loaded. Attempting to generate token...'); - - // Use TokenManager's handleCaptchaWithRetry method instead - const token = await tokenManager.handleCaptchaWithRetry(); - if (token) { - setTurnstileToken(token); - console.log('✅ Startup token generated successfully'); - updateUI('tokenReady', 'success'); - Utils.showAlert(Utils.t('tokenGeneratorReady'), 'success'); - enableFileOperations(); // Enable file operations since initial setup is complete - } else { - console.warn( - '⚠️ Startup token generation failed (no token received), will retry when needed' - ); - updateUI('tokenRetryLater', 'warning'); - // Still enable file operations even if initial token generation fails - // Users can load progress and use manual/hybrid modes - enableFileOperations(); - } - } catch (error) { - console.error('❌ Critical error during Turnstile initialization:', error); // More specific error - updateUI('tokenRetryLater', 'warning'); - // Still enable file operations even if initial setup fails - // Users can load progress and use manual/hybrid modes - enableFileOperations(); - // Don't show error alert for initialization failures, just log them - } - } - - // Load theme preference immediately on startup before creating UI - loadThemePreference(); - applyTheme(); - - var pawtect_chunk = null; - - //find module if pawtect_chunk is null - pawtect_chunk ??= await findTokenModule("pawtect_wasm_bg.wasm"); - - async function createWasmToken(regionX, regionY, payload) { - try { - // Load the Pawtect module and WASM - const mod = await import(new URL('/_app/immutable/chunks/' + pawtect_chunk, location.origin).href); - let wasm; - try { - wasm = await mod._(); - console.log('✅ WASM initialized successfully'); - } catch (wasmError) { - console.error('❌ WASM initialization failed:', wasmError); - return null; - } - try { - try { - const me = await fetch(`https://backend.wplace.live/me`, { credentials: 'include' }).then(r => r.ok ? r.json() : null); - if (me?.id) { - mod.i(me.id); - console.log('✅ user ID set:', me.id); - } - } catch { } - } catch (userIdError) { - console.log('⚠️ Error setting user ID:', userIdError.message); - } - try { - const testUrl = `https://backend.wplace.live/s0/pixel/${regionX}/${regionY}`; - if (mod.r) { - mod.r(testUrl); - console.log('✅ Request URL set:', testUrl); - } else { - console.log('⚠️ request_url function (mod.r) not available'); - } - } catch (urlError) { - console.log('⚠️ Error setting request URL:', urlError.message); - } - - // Create test payload - - console.log('📝 payload:', payload); - - // Encode payload - const enc = new TextEncoder(); - const dec = new TextDecoder(); - const bodyStr = JSON.stringify(payload); - const bytes = enc.encode(bodyStr); - console.log('📏 Payload size:', bytes.length, 'bytes'); - console.log('📄 Payload string:', bodyStr); - - // Allocate WASM memory with validation - let inPtr; - try { - if (!wasm.__wbindgen_malloc) { - console.error('❌ __wbindgen_malloc function not found'); - return null; - } - - inPtr = wasm.__wbindgen_malloc(bytes.length, 1); - console.log('✅ WASM memory allocated, pointer:', inPtr); - - // Copy data to WASM memory - const wasmBuffer = new Uint8Array(wasm.memory.buffer, inPtr, bytes.length); - wasmBuffer.set(bytes); - console.log('✅ Data copied to WASM memory'); - } catch (memError) { - console.error('❌ Memory allocation error:', memError); - return null; - } - - // Call the WASM function - console.log('🚀 Calling get_pawtected_endpoint_payload...'); - let outPtr, outLen, token; - try { - const result = wasm.get_pawtected_endpoint_payload(inPtr, bytes.length); - console.log('✅ Function called, result type:', typeof result, result); - - if (Array.isArray(result) && result.length === 2) { - [outPtr, outLen] = result; - console.log('✅ Got output pointer:', outPtr, 'length:', outLen); - - // Decode the result - const outputBuffer = new Uint8Array(wasm.memory.buffer, outPtr, outLen); - token = dec.decode(outputBuffer); - console.log('✅ Token decoded successfully'); - } else { - console.error('❌ Unexpected function result format:', result); - return null; - } - } catch (funcError) { - console.error('❌ Function call error:', funcError); - console.error('Stack trace:', funcError.stack); - return null; - } - - // Cleanup memory - try { - if (wasm.__wbindgen_free && outPtr && outLen) { - wasm.__wbindgen_free(outPtr, outLen, 1); - console.log('✅ Output memory freed'); - } - if (wasm.__wbindgen_free && inPtr) { - wasm.__wbindgen_free(inPtr, bytes.length, 1); - console.log('✅ Input memory freed'); - } - } catch (cleanupError) { - console.log('⚠️ Cleanup warning:', cleanupError.message); - } - - // Display results - console.log(''); - console.log('🎉 SUCCESS!'); - console.log('🔑 Full token:'); - console.log(token); - return token; - } catch (error) { - console.error('❌ Failed to generate fp parameter:', error); - return null; - } - } - - async function findTokenModule(str) { - console.log('🔎 Searching for wasm Module...'); - const links = Array.from(document.querySelectorAll('link[rel="modulepreload"][href$=".js"]')); - - for (const link of links) { - try { - const url = new URL(link.getAttribute("href"), location.origin).href; - const code = await fetch(url).then(r => r.text()); - if (code.includes(str)) { - console.log('✅ Found wasm Module...'); - return url.split('/').pop(); - } - } catch { } - } - console.error(`❌ Could not find Pawtect chunk: `, error); - } - - async function purchase(type) { - // loadThemePreference() - let id; - let chargeMultiplier; - if (type === "max_charges") { - id = 70; - chargeMultiplier = 5; - } else if (type === "paint_charges") { - id = 80; - chargeMultiplier = 30; - } else { - console.error("Error: Invalid purchase type provided."); - return; - } - const { droplets } = await WPlaceService.getCharges(); - console.log("There are currently : ", droplets, "droplets."); - try { - const amounts = Math.floor(droplets / 500); - if (amounts < 1) { - console.log("Not enough droplets to purchase."); - return; - } - const payload = { - "product": { - "id": id, - "amount": amounts - } - }; - const res = await fetch("https://backend.wplace.live/purchase", { - method: "POST", - headers: { - "Content-Type": "text/plain;charset=UTF-8" - }, - body: JSON.stringify(payload), - credentials: "include" - }); - // POST request completed - const { droplets: newDroplets } = await WPlaceService.getCharges(); - if (droplets != newDroplets) { - console.log("Successfully bought", amounts * chargeMultiplier, type.replace('_', ' '), "."); - return 2; - } else { - console.log("Failed to buy charges"); - return 1; - } - } catch (e) { - console.error("An error occurred during the purchase:", e); - return 0; - } - } - 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) { - console.error('❌ Failed to send setCookie message:', error); - return false; - } - } - async function getAccounts() { - return new Promise((resolve, reject) => { - console.log("Requesting accounts from extension..."); - - // Ask extension for accounts - window.postMessage({ - source: "my-userscript", - type: "getAccounts" - }, "*"); - - function handler(event) { - if (event.source !== window) return; - if (event.data.source !== "extension") return; - if (event.data.type === "accountsData") { - // Remove listener when we get the response - window.removeEventListener("message", handler); - - try { - localStorage.setItem("accounts", JSON.stringify(event.data.accounts)); - console.log("✅ Accounts saved to localStorage:", event.data.accounts); - } catch (e) { - console.error("❌ Failed to save accounts:", e); - } - resolve(event.data.accounts); - } - } - window.addEventListener("message", handler); - }); - } - - async function fetchAccount() { - const { ID, Charges, Max, Droplets } = await WPlaceService.fetchCheck(); - console.log("User's ID :", ID); - console.log("User's Charges :", Charges, "/", Max); - console.log("User's Droplets :", Droplets); - } - - async function fetchAllAccountDetails() { - if (state.isFetchingAllAccounts) { - Utils.showAlert("Already fetching account details.", "warning"); - return; - } - - state.isFetchingAllAccounts = true; - - const refreshBtn = document.getElementById('refreshAllAccountsBtn'); - if (refreshBtn) { - refreshBtn.innerHTML = ''; - refreshBtn.disabled = true; - } - - const accountsListArea = document.getElementById('accountsListArea'); - if (accountsListArea) { - accountsListArea.innerHTML = `
Loading accounts...
`; - } - - try { - // First, get accounts from the extension - console.log(`🔄 [FETCH] Requesting accounts from extension...`); - try { - await getAccounts(); - console.log(`✅ [FETCH] Successfully retrieved accounts from extension`); - } catch (error) { - console.warn(`⚠️ [FETCH] Failed to get accounts from extension:`, error); - // Continue anyway in case we have cached accounts - } - - // Load accounts using the new AccountManager - await accountManager.loadAccounts(); - - console.log(`✅ [FETCH] Loaded ${accountManager.getAccountCount()} accounts from storage`); - - // Debug: Check if we actually have accounts - if (accountManager.getAccountCount() === 0) { - console.warn(`⚠️ [FETCH] No accounts found in storage. Check localStorage 'accounts' and chrome.storage 'infoAccounts'`); - - // Check localStorage for accounts - const localStorageAccounts = JSON.parse(localStorage.getItem("accounts")) || []; - console.log(`📋 [DEBUG] localStorage accounts:`, localStorageAccounts); - - // Render empty state and return early - renderAccountsList(); - return; - } - - // Now fetch fresh data for each account - const accounts = accountManager.getAllAccounts(); - if (accounts.length > 0) { - console.log(`🔄 [FETCH] Fetching fresh data for ${accounts.length} accounts...`); - - // Remember the current account so we can switch back - const originalCurrentAccount = accounts.find(acc => acc.isCurrent); - - for (let i = 0; i < accounts.length; i++) { - const account = accounts[i]; - console.log(`📊 [FETCH] Fetching data for account ${i + 1}: ${account.displayName}`); - - try { - // Switch to this account temporarily to fetch its data - console.log(`🔄 [FETCH] Switching to ${account.displayName} to fetch fresh data...`); - await switchToSpecificAccount(account.token, account.displayName); - // await Utils.sleep(500); // Small delay to ensure switch takes effect - - // Fetch fresh account details - const accountData = await WPlaceService.getCharges(); - const accountInfo = await WPlaceService.fetchCheck(); - - // Update account with fresh data - accountManager.updateAccountData(account.token, { - ID: accountData.id || accountInfo.ID, - Charges: Math.floor(accountData.charges || 0), - Max: Math.floor(accountData.max || 0), - Droplets: Math.floor(accountData.droplets || 0), - displayName: accountInfo.Username || accountInfo.name || account.displayName - }); - - console.log(`✅ [FETCH] Updated ${account.displayName}: ⚡${Math.floor(accountData.charges)}/${Math.floor(accountData.max)} 💧${Math.floor(accountData.droplets)}`); - - } catch (error) { - console.warn(`⚠️ [FETCH] Failed to fetch data for ${account.displayName}:`, error); - } - } - - // Switch back to the original current account if there was one - if (originalCurrentAccount) { - console.log(`🔙 [FETCH] Switching back to original current account: ${originalCurrentAccount.displayName}`); - try { - await switchToSpecificAccount(originalCurrentAccount.token, originalCurrentAccount.displayName); - //await Utils.sleep(300); - - // Mark it as current again - accountManager.updateAccountData(originalCurrentAccount.token, { - isCurrent: true - }); - } catch (error) { - console.warn(`⚠️ [FETCH] Failed to switch back to original account:`, error); - } - } - - console.log(`🎯 [FETCH] Completed fetching fresh data for all accounts`); - } - - // Render the accounts list with fresh data - renderAccountsList(); - - } catch (error) { - console.error('❌ [FETCH] Error fetching account details:', error); - if (accountsListArea) { - accountsListArea.innerHTML = `
Error loading accounts.
`; - } - } finally { - state.isFetchingAllAccounts = false; - if (refreshBtn) { - refreshBtn.innerHTML = ''; - refreshBtn.disabled = false; - } - } - } - - // Function to update current account charges in the account list - async function updateCurrentAccountInList() { - if (accountManager.getAccountCount() === 0) return; - - // Find current account in the list and update its charges - const currentAccount = accountManager.getCurrentAccount(); - if (currentAccount) { - const { charges, cooldown, droplets } = await WPlaceService.getCharges(); - state.displayCharges = Math.floor(charges); - state.preciseCurrentCharges = charges; - await updateStats(); - - // Update the current account data in AccountManager - accountManager.updateAccountData(currentAccount.token, { - Charges: Math.floor(state.displayCharges || state.preciseCurrentCharges || 0), - Max: state.maxCharges, - Droplets: Math.floor(droplets) - }); - - // Re-render the account list to show updated charges - renderAccountsList(); - } - } - - // Function to update current account spotlight when switching during painting - async function updateCurrentAccountSpotlight() { - if (accountManager.getAccountCount() === 0) return; - // await Utils.sleep(500); // Wait a bit for the switch to take effect - try { - const currentAccountData = await WPlaceService.getCharges(); - console.log("Current account after switch:", currentAccountData); - console.log(`🔍 Switched to account with ID: ${currentAccountData.id}`); - - // Find the current account in AccountManager and update it - const accounts = accountManager.getAllAccounts(); - const currentAccount = accounts.find(acc => acc.ID === currentAccountData.id); - - if (currentAccount) { - const currentAccountInfo = await WPlaceService.fetchCheck(); - - // Update account data in AccountManager - accountManager.updateAccountData(currentAccount.token, { - isCurrent: true, - Charges: Math.floor(currentAccountData.charges), - Max: Math.floor(currentAccountData.max), - Droplets: Math.floor(currentAccountData.droplets), - displayName: currentAccountInfo.Username || currentAccountInfo.name || currentAccount.displayName - }); - - console.log(`🎯 Updated current account spotlight: ${currentAccount.displayName}`); - - // Check for autobuy after account is loaded - if (CONFIG.autoBuyToggle && Math.floor(currentAccountData.droplets) > 500) { - console.log(`💰 Account has ${Math.floor(currentAccountData.droplets)} droplets (>500), triggering autobuy...`); - try { - const purchaseResult = await purchase(CONFIG.autoBuy); - if (purchaseResult == 2) { - console.log('✅ Autobuy successful after account switch'); - // Update charges after purchase - const updatedCharges = await WPlaceService.getCharges(); - accountManager.updateAccountData(currentAccount.token, { - Charges: Math.floor(updatedCharges.charges), - Droplets: Math.floor(updatedCharges.droplets) - }); - // Re-render account list to show updated status after purchase - renderAccountsList(); - } else { - console.log('❌ Autobuy failed after account switch'); - } - } catch (error) { - console.error('❌ Error during autobuy after account switch:', error); - } - } else if (CONFIG.autoBuyToggle) { - console.log(`💰 Account has ${Math.floor(currentAccountData.droplets)} droplets (≤500), skipping autobuy`); - } - - // Re-render the account list to show new current account - renderAccountsList(); - - console.log(`🔒 PRESERVING currentActiveIndex: ${state.currentActiveIndex} (do not recalculate from isCurrent flag)`); - console.log(`� Account at currentActiveIndex ${state.currentActiveIndex}: ${state.originalAccountOrder[state.currentActiveIndex]?.displayName} (ID ${state.originalAccountOrder[state.currentActiveIndex]?.orderId})`); - - // Update accountIndex to match original array position - const originalArrayIndex = state.allAccountsInfo.findIndex(acc => acc.isCurrent); - if (originalArrayIndex !== -1) { - state.accountIndex = originalArrayIndex; - } - - console.log(`📊 Final state: activeIndex=${state.currentActiveIndex}, accountIndex=${state.accountIndex}, orderId=${newCurrentAccount.orderId}`); - - console.warn(`⚠️ Could not find account ID ${newCurrentAccount.orderId} in switching order`); - - console.warn(`⚠️ Could not find switched account with ID ${currentAccountData.id} in account list`); - } - - // Re-render the account list to show new current account - renderAccountsList(); - - } catch (error) { - console.warn('⚠️ Failed to update account spotlight:', error); - } - } - - function renderAccountsList() { - const accountsListArea = document.getElementById('accountsListArea'); - if (!accountsListArea) return; - - accountsListArea.innerHTML = ''; - - const accounts = accountManager.getAllAccounts(); - console.log(`🔍 [RENDER] Rendering ${accounts.length} accounts`); - - // Debug: Log account data to see what we're working with - accounts.forEach((account, index) => { - console.log(`📊 [RENDER] Account ${index + 1}: ${account.displayName} - ⚡${account.Charges}/${account.Max} 💧${account.Droplets} ${account.isCurrent ? '(CURRENT)' : ''}`); - }); - - if (accounts.length === 0) { - // Don't show any placeholder - just leave empty - console.log(`📝 [RENDER] No accounts to display`); - return; - } - - accounts.forEach((account, index) => { - const item = createAccountItem(account, index); - accountsListArea.appendChild(item); - }); - - console.log(`✅ [RENDER] Successfully rendered ${accounts.length} accounts`); - } - - function createAccountItem(account, index) { - const item = document.createElement('div'); - - // Determine if this is the current account - const isCurrentAccount = account.isCurrent; - const isNextInSequence = accountManager.currentIndex !== -1 && - ((accountManager.currentIndex + 1) % accountManager.getAccountCount()) === index; - - let itemClasses = 'wplace-account-item'; - if (isCurrentAccount) { - itemClasses += ' current'; - } else if (isNextInSequence) { - itemClasses += ' next-in-sequence'; - } - - item.className = itemClasses; - - // Create ordering number element with 1-based index for user display - const orderNumber = document.createElement('div'); - orderNumber.className = 'wplace-account-number'; - orderNumber.textContent = index + 1; // Show 1-based index for user-friendly display - - // 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)'; - } else if (isNextInSequence) { - orderNumber.style.background = '#f39c12'; // Orange color for next - orderNumber.style.color = 'white'; - orderNumber.style.boxShadow = '0 0 8px rgba(243, 156, 18, 0.6)'; - } - - // Create account details - const details = document.createElement('div'); - details.className = 'wplace-account-details'; - - const accountName = document.createElement('div'); - accountName.className = 'wplace-account-name'; - const displayName = account.displayName || account.name || `Account ${index + 1}`; - accountName.textContent = displayName; - accountName.title = displayName; - - const accountStats = document.createElement('div'); - accountStats.className = 'wplace-account-stats'; - - // Safely handle undefined values - const charges = account.Charges !== undefined ? Math.floor(account.Charges) : 0; - const max = account.Max !== undefined ? Math.floor(account.Max) : 0; - const droplets = account.Droplets !== undefined ? Math.floor(account.Droplets) : 0; - - accountStats.innerHTML = ` - ${charges}/${max} - ${droplets} - `; - - details.appendChild(accountName); - details.appendChild(accountStats); - - // Add status indicators - const status = document.createElement('div'); - status.className = 'wplace-account-status'; - if (isCurrentAccount) { - status.innerHTML = ''; - } else if (isNextInSequence) { - status.innerHTML = ''; - } - - item.appendChild(orderNumber); - item.appendChild(details); - item.appendChild(status); - - return item; - } - - // SIMPLIFIED ACCOUNT SWITCHING - async function switchToNextAccount(accounts) { - console.log(`🔄 [SWITCH] Starting account switch`); - - // Validate we have accounts - if (accountManager.getAccountCount() === 0) { - console.error('❌ No accounts available for switching'); - return false; - } - - // Get next account using simplified manager - const nextAccount = accountManager.switchToNext(); - if (!nextAccount) { - console.error('❌ Failed to get next account'); - return false; - } - - console.log(`🎯 [SWITCH] Switching to: ${nextAccount.displayName} (index ${accountManager.currentIndex})`); - - // Ensure token exists - if (!nextAccount.token) { - console.error(`❌ Missing token for account: ${nextAccount.displayName}`); - return false; - } - - console.log(`� [SWITCH] Using token: ${nextAccount.token.substring(0, 20)}...`); - - // Perform the account switch - try { - await swapAccountTrigger(nextAccount.token); - - // Update account index for backward compatibility - state.accountIndex = accountManager.currentIndex; - - console.log(`✅ [SWITCH] Successfully switched to ${nextAccount.displayName}`); - - // Wait a moment for the switch to fully complete - // await new Promise(resolve => setTimeout(resolve, 1000)); - - // Update the account status and UI after successful switch - await updateCurrentAccountInList(); - - return true; - } catch (error) { - console.error('❌ [SWITCH] Account switch failed:', error); - return false; - } - } - - // SIMPLIFIED helper function for specific account switching - async function switchToSpecificAccount(token, accountName) { - console.log(`🔄 [SPECIFIC SWITCH] Attempting to switch to account: ${accountName}`); - console.log(`🔑 [SPECIFIC SWITCH] Using token: ${token.substring(0, 20)}...`); - - await swapAccountTrigger(token); - - let maxRetries = 20; - let retryCount = 0; - let swapSuccess = false; - - while (!swapSuccess && retryCount < maxRetries) { - console.log(`⏳ [SPECIFIC SWITCH] Waiting for account swap... (Attempt ${retryCount + 1}/${maxRetries})`); - //await new Promise(resolve => setTimeout(resolve, 1000)); - - try { - // await fetchAccount(); - console.log('✅ [SPECIFIC SWITCH] Account swap confirmed.'); - swapSuccess = true; - } catch (error) { - console.warn('❌ [SPECIFIC SWITCH] Account swap not yet successful. Retrying...', error); - retryCount++; - - if (retryCount % 5 === 0) { - console.log('🔄 [SPECIFIC SWITCH] Re-triggering account swap...'); - 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; - Utils.performSmartSave(); - await updateStats(); - - // Update account data in manager - accountManager.updateAccountData({ charges, cooldown }); - - // Update the account status and UI after successful switch - await updateCurrentAccountSpotlight(); - - console.log(`✅ [SPECIFIC SWITCH] Successfully switched to ${accountName} with ${Math.floor(charges)} charges`); - return true; - } else { - console.error(`❌ [SPECIFIC SWITCH] Failed to swap to ${accountName} after multiple retries.`); - return false; - } - } - - // Wait for dependencies before initializing UI - async function waitForDependenciesAndInitialize() { - // 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, - imageProcessor: window.globalImageProcessor, - 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:', { - utilsManager: !!window.globalUtilsManager, - imageProcessor: !!window.globalImageProcessor, - overlayManager: !!window.globalOverlayManager, - tokenManager: !!window.globalTokenManager - }); - } - - return createUI(); - } - - waitForDependenciesAndInitialize().then(() => { - // Generate token automatically after UI is ready - setTimeout(initializeTokenGenerator, 1000); - - // Quick initial account load from cache - setTimeout(async () => { - console.log('🔄 Initial account load from cache...'); - try { - await accountManager.loadAccounts(); - if (accountManager.getAccountCount() > 0) { - console.log(`✅ Loaded ${accountManager.getAccountCount()} cached accounts`); - renderAccountsList(); - } else { - console.log('📭 No cached accounts found'); - } - } catch (error) { - console.warn('⚠️ Initial account load failed:', error); - } - }, 500); - - // Auto-refresh account list on startup (with extension communication) - setTimeout(() => { - console.log('🔄 Auto-refreshing account list on startup...'); - fetchAllAccountDetails(); - }, 2000); - - // Attach advanced color matching listeners (resize dialog) - const advancedInit = () => { - const chromaSlider = document.getElementById('chromaPenaltyWeightSlider'); - const chromaValue = document.getElementById('chromaWeightValue'); - const resetBtn = document.getElementById('resetAdvancedColorBtn'); - const algoSelect = document.getElementById('colorAlgorithmSelect'); - const chromaToggle = document.getElementById('enableChromaPenaltyToggle'); - const transInput = document.getElementById('transparencyThresholdInput'); - const whiteInput = document.getElementById('whiteThresholdInput'); - const ditherToggle = document.getElementById('enableDitheringToggle'); - if (algoSelect) - algoSelect.addEventListener('change', (e) => { - state.colorMatchingAlgorithm = e.target.value; - saveBotSettings(); - _updateResizePreview(); - }); - if (chromaToggle) - chromaToggle.addEventListener('change', (e) => { - state.enableChromaPenalty = e.target.checked; - saveBotSettings(); - _updateResizePreview(); - }); - if (chromaSlider && chromaValue) - chromaSlider.addEventListener('input', (e) => { - state.chromaPenaltyWeight = parseFloat(e.target.value) || 0.15; - chromaValue.textContent = state.chromaPenaltyWeight.toFixed(2); - saveBotSettings(); - _updateResizePreview(); - }); - if (transInput) - transInput.addEventListener('change', (e) => { - const v = parseInt(e.target.value, 10); - if (!isNaN(v) && v >= 0 && v <= 255) { - state.customTransparencyThreshold = v; - CONFIG.TRANSPARENCY_THRESHOLD = v; - saveBotSettings(); - _updateResizePreview(); - } - }); - if (whiteInput) - whiteInput.addEventListener('change', (e) => { - const v = parseInt(e.target.value, 10); - if (!isNaN(v) && v >= 200 && v <= 255) { - state.customWhiteThreshold = v; - CONFIG.WHITE_THRESHOLD = v; - saveBotSettings(); - _updateResizePreview(); - } - }); - if (ditherToggle) - ditherToggle.addEventListener('change', (e) => { - state.ditheringEnabled = e.target.checked; - saveBotSettings(); - _updateResizePreview(); - }); - if (resetBtn) - resetBtn.addEventListener('click', () => { - state.colorMatchingAlgorithm = 'lab'; - state.enableChromaPenalty = true; - state.chromaPenaltyWeight = 0.15; - state.customTransparencyThreshold = CONFIG.TRANSPARENCY_THRESHOLD = 100; - state.customWhiteThreshold = CONFIG.WHITE_THRESHOLD = 250; - saveBotSettings(); - const a = document.getElementById('colorAlgorithmSelect'); - if (a) a.value = 'lab'; - const ct = document.getElementById('enableChromaPenaltyToggle'); - if (ct) ct.checked = true; - if (chromaSlider) chromaSlider.value = 0.15; - if (chromaValue) chromaValue.textContent = '0.15'; - if (transInput) transInput.value = 100; - if (whiteInput) whiteInput.value = 250; - _updateResizePreview(); - Utils.showAlert(Utils.t('advancedColorSettingsReset'), 'success'); - }); - }; - // Delay to ensure resize UI built - setTimeout(advancedInit, 500); - - // Add cleanup on page unload - window.addEventListener('beforeunload', () => { - Utils.cleanupTurnstile(); - }); - }); -})(); - -// === Async coordinate generation (off-main-thread) === -async function generateCoordinatesAsync( - width, - height, - mode, - direction, - snake, - blockWidth, - blockHeight, - startFromX = 0, - startFromY = 0 -) { - try { - if (typeof Worker === 'undefined') { - // Environment does not support Web Workers – fall back - return generateCoordinates( - width, - height, - mode, - direction, - snake, - blockWidth, - blockHeight, - startFromX, - startFromY - ); - } - - const workerCode = `self.onmessage = function(e) { - const { width, height, mode, direction, snake, blockWidth, blockHeight, startFromX, startFromY } = e.data || {}; - - function generate() { - let coords = []; - - // Determine traversal direction - let xStart, xEnd, xStep; - let yStart, yEnd, yStep; - switch (direction) { - case 'top-left': - xStart = 0; xEnd = width; xStep = 1; - yStart = 0; yEnd = height; yStep = 1; - break; - case 'top-right': - xStart = width - 1; xEnd = -1; xStep = -1; - yStart = 0; yEnd = height; yStep = 1; - break; - case 'bottom-left': - xStart = 0; xEnd = width; xStep = 1; - yStart = height - 1; yEnd = -1; yStep = -1; - break; - case 'bottom-right': - xStart = width - 1; xEnd = -1; xStep = -1; - yStart = height - 1; yEnd = -1; yStep = -1; - break; - default: - throw new Error('Unknown direction: ' + direction); - } - - if (mode === 'rows') { - let rowIndex = 0; - for (let y = yStart; y !== yEnd; y += yStep) { - if (snake && rowIndex % 2 !== 0) { - for (let x = xEnd - xStep; x !== xStart - xStep; x -= xStep) { - coords.push([x, y]); - } - } else { - for (let x = xStart; x !== xEnd; x += xStep) { - coords.push([x, y]); - } - } - rowIndex++; - } - } else if (mode === 'columns') { - let colIndex = 0; - for (let x = xStart; x !== xEnd; x += xStep) { - if (snake && colIndex % 2 !== 0) { - for (let y = yEnd - yStep; y !== yStart - yStep; y -= yStep) { - coords.push([x, y]); - } - } else { - for (let y = yStart; y !== yEnd; y += yStep) { - coords.push([x, y]); - } - } - colIndex++; - } - } else if (mode === 'circle-out') { - const cx = Math.floor(width / 2); - const cy = Math.floor(height / 2); - const maxRadius = Math.ceil(Math.sqrt(cx * cx + cy * cy)); - for (let r = 0; r <= maxRadius; r++) { - for (let y = cy - r; y <= cy + r; y++) { - for (let x = cx - r; x <= cx + r; x++) { - if (x >= 0 && x < width && y >= 0 && y < height) { - const dist = Math.max(Math.abs(x - cx), Math.abs(y - cy)); - if (dist === r) coords.push([x, y]); - } - } - } - } - } else if (mode === 'circle-in') { - const cx = Math.floor(width / 2); - const cy = Math.floor(height / 2); - const maxRadius = Math.ceil(Math.sqrt(cx * cx + cy * cy)); - for (let r = maxRadius; r >= 0; r--) { - for (let y = cy - r; y <= cy + r; y++) { - for (let x = cx - r; x <= cx + r; x++) { - if (x >= 0 && x < width && y >= 0 && y < height) { - const dist = Math.max(Math.abs(x - cx), Math.abs(y - cy)); - if (dist === r) coords.push([x, y]); - } - } - } - } - } else if (mode === 'blocks' || mode === 'shuffle-blocks') { - const blocks = []; - for (let by = 0; by < height; by += blockHeight) { - for (let bx = 0; bx < width; bx += blockWidth) { - const block = []; - for (let y = by; y < Math.min(by + blockHeight, height); y++) { - for (let x = bx; x < Math.min(bx + blockWidth, width); x++) { - block.push([x, y]); - } - } - blocks.push(block); - } - } - if (mode === 'shuffle-blocks') { - for (let i = blocks.length - 1; i > 0; i--) { - const j = Math.floor(Math.random() * (i + 1)); - const tmp = blocks[i]; - blocks[i] = blocks[j]; - blocks[j] = tmp; - } - } - for (const block of blocks) { - for (let i = 0; i < block.length; i++) { - coords.push(block[i]); - } - } - } else { - throw new Error('Unknown mode: ' + mode); - } - - // Resume from specified position if provided - if (startFromX > 0 || startFromY > 0) { - let startIndex = -1; - for (let i = 0; i < coords.length; i++) { - const c = coords[i]; - if (c[0] === startFromX && c[1] === startFromY) { - startIndex = i; - break; - } - } - if (startIndex >= 0) { - coords = coords.slice(startIndex); - } - } - - return coords; - } - - try { - const coords = generate(); - self.postMessage({ ok: true, coords }); - } catch (err) { - self.postMessage({ ok: false, error: err && (err.message || String(err)) }); - } -};`; - - const blob = new Blob([workerCode], { type: 'application/javascript' }); - const url = URL.createObjectURL(blob); - - return await new Promise((resolve) => { - const worker = new Worker(url); - const cleanup = () => { - URL.revokeObjectURL(url); - try { worker.terminate(); } catch (e) { } - }; - - worker.onmessage = (e) => { - const { ok, coords, error } = e.data || {}; - cleanup(); - if (ok && Array.isArray(coords)) { - resolve(coords); - } else { - console.warn('Coordinate worker failed, falling back to sync:', error); - resolve( - generateCoordinates( - width, - height, - mode, - direction, - snake, - blockWidth, - blockHeight, - startFromX, - startFromY - ) - ); - } - }; - - worker.onerror = (err) => { - console.warn('Coordinate worker error, falling back to sync:', err && (err.message || err)); - cleanup(); - resolve( - generateCoordinates( - width, - height, - mode, - direction, - snake, - blockWidth, - blockHeight, - startFromX, - startFromY - ) - ); - }; - - worker.postMessage({ - width, - height, - mode, - direction, - snake, - blockWidth, - blockHeight, - startFromX, - startFromY, - }); - }); - } catch (e) { - console.warn('Failed to start coordinate worker, using sync generation:', e); - return generateCoordinates( - width, - height, - mode, - direction, - snake, - blockWidth, - blockHeight, - startFromX, - startFromY - ); - } -} +// ==UserScript== +// @name WPlaceBot +// @namespace http://tampermonkey.net/ +// @version 2025-08-08.3 +// @description Bot +// @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[key]) { + let text = window.loadedTranslations[key]; + if (params) { + Object.keys(params).forEach(paramKey => { + text = text.replace(new RegExp(`{{${paramKey}}}`, 'g'), params[paramKey]); + }); + } + 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]; + if (params) { + Object.keys(params).forEach(paramKey => { + text = text.replace(new RegExp(`{{${paramKey}}}`, 'g'), params[paramKey]); + }); + } + return text; + } + } catch (error) { + console.warn('Error in getText fallback:', error); + } + + return key; // Fallback to key if no translation found +} + +; (async () => { + // Prevent multiple instances of this script from running + if (window.WPLACE_AUTO_IMAGE_LOADED) { + console.log('%c⚠️ Auto-Image script already loaded, skipping duplicate execution', 'color: #ff9800; font-weight: bold;'); + return; + } + window.WPLACE_AUTO_IMAGE_LOADED = true; + + console.log('%c🚀 WPlace AutoBot Auto-Image Starting...', 'color: #00ff41; font-weight: bold; font-size: 16px;'); + + const buyTypes = [ + 'none', + 'max_charges', + 'paint_charges' + ]; + + // CONFIGURATION CONSTANTS + const CONFIG = { + COOLDOWN_DEFAULT: 31000, + TRANSPARENCY_THRESHOLD: 100, + WHITE_THRESHOLD: 250, + LOG_INTERVAL: 10, + PAINTING_SPEED: { + MIN: 1, // Minimum 1 pixel batch size + MAX: 1000, // Maximum 1000 pixels batch size + DEFAULT: 5, // Default 5 pixels batch size + }, + BATCH_MODE: 'normal', // "normal" or "random" - default to normal + RANDOM_BATCH_RANGE: { + MIN: 3, // Random range minimum + MAX: 20, // Random range maximum + }, + PAINTING_ORDER: 'sequential', // "sequential" or "color-by-color" - default to sequential + PAINTING_SPEED_ENABLED: false, // Off by default + AUTO_CAPTCHA_ENABLED: true, // Turnstile generator enabled by default + TOKEN_SOURCE: 'generator', // "generator", "manual", or "hybrid" - default to generator + COOLDOWN_CHARGE_THRESHOLD: 1, // Default wait threshold + // Desktop Notifications (defaults) + NOTIFICATIONS: { + ENABLED: false, + ON_CHARGES_REACHED: true, + ONLY_WHEN_UNFOCUSED: true, + REPEAT_MINUTES: 5, // repeat reminder while threshold condition holds + }, + OVERLAY: { + OPACITY_DEFAULT: 0.6, + BLUE_MARBLE_DEFAULT: true, // Enable blue marble effect by default + ditheringEnabled: false, // Default dithering OFF + }, // --- START: Color data from colour-converter.js --- + // New color structure with proper ID mapping + 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 }, + }, // --- END: Color data --- + // Optimized CSS Classes for reuse + CSS_CLASSES: { + BUTTON_PRIMARY: ` + background: linear-gradient(135deg, #4facfe 0%, #00f2fe 100%); + color: white; border: none; border-radius: 8px; padding: 10px 16px; + cursor: pointer; font-weight: 500; transition: all 0.3s ease; + display: flex; align-items: center; gap: 8px; + `, + BUTTON_SECONDARY: ` + background: rgba(255,255,255,0.1); color: white; + border: 1px solid rgba(255,255,255,0.2); border-radius: 8px; + padding: 8px 12px; cursor: pointer; transition: all 0.3s ease; + `, + MODERN_CARD: ` + background: rgba(255,255,255,0.1); border-radius: 12px; + padding: 18px; border: 1px solid rgba(255,255,255,0.1); + backdrop-filter: blur(5px); + `, + GRADIENT_TEXT: ` + background: linear-gradient(135deg, #4facfe 0%, #00f2fe 100%); + -webkit-background-clip: text; -webkit-text-fill-color: transparent; + background-clip: text; font-weight: bold; + `, + }, + THEMES: { + 'Classic Autobot': { + primary: '#000000', + secondary: '#111111', + accent: '#222222', + text: '#ffffff', + highlight: '#775ce3', + success: '#00ff00', + error: '#ff0000', + warning: '#ffaa00', + fontFamily: "'Segoe UI', Roboto, sans-serif", + borderRadius: '12px', + borderStyle: 'solid', + borderWidth: '1px', + boxShadow: '0 8px 32px rgba(0,0,0,0.6), 0 0 0 1px rgba(255,255,255,0.1)', + backdropFilter: 'blur(10px)', + animations: { + glow: false, + scanline: false, + 'pixel-blink': false, + }, + }, + 'Classic Light': { + primary: '#E0E0E1', + secondary: '#FBFBFB', + accent: '#F3F3F3', + text: '#203C5D', + highlight: '#203C5D', + success: '#28a745', + error: '#dc3545', + warning: '#ffc107', + fontFamily: "'Segoe UI', Roboto, sans-serif", + borderRadius: '12px', + borderStyle: 'solid', + borderWidth: '1px', + boxShadow: '0 8px 32px rgba(0,0,0,0.15), 0 0 0 1px rgba(0,0,0,0.08)', + backdropFilter: 'blur(10px)', + animations: { + glow: false, + scanline: false, + 'pixel-blink': false, + }, + }, + 'Neon Retro': { + primary: '#1a1a2e', + secondary: '#16213e', + accent: '#0f3460', + text: '#00ff41', + highlight: '#ff6b35', + success: '#39ff14', + error: '#ff073a', + warning: '#ffff00', + neon: '#00ffff', + purple: '#bf00ff', + pink: '#ff1493', + fontFamily: "'Press Start 2P', monospace", + borderRadius: '0', + borderStyle: 'solid', + borderWidth: '3px', + boxShadow: '0 0 20px rgba(0, 255, 65, 0.3), inset 0 0 20px rgba(0, 255, 65, 0.1)', + backdropFilter: 'none', + animations: { + glow: true, + scanline: true, + 'pixel-blink': true, + }, + }, + 'Neon Retro Cyan': { + primary: '#1959A1', + secondary: '#3C74AF', + accent: '#538CC0', + text: '#81DCF7', + highlight: '#EA9C00', + success: '#39ff14', + error: '#ff073a', + warning: '#ffff00', + neon: '00ffff', + purple: '#bf00ff', + pink: '#ff1493', + fontFamily: "'Press Start 2P', monospace", + borderRadius: '0', + borderStyle: 'solid', + borderWidth: '3px', + boxShadow: '0 0 20px rgba(234 156 0, 0.3), inset 0 0 20px rgba(234 156 0, 0.1)', + backdropFilter: 'none', + animations: { + glow: true, + scanline: true, + 'pixel-blink': true, + }, + }, + 'Neon Retro Light': { + primary: '#E0E0E1', + secondary: '#FBFBFB', + accent: '#F3F3F3', + text: '#203C5D', + highlight: '#203C5D', + success: '#39ff14', + error: '#ff073a', + warning: '#ffff00', + neon: '#203C5D', + purple: '#bf00ff', + pink: '#ff1493', + fontFamily: "'Press Start 2P', monospace", + borderRadius: '0', + borderStyle: 'solid', + borderWidth: '3px', + boxShadow: '0 0 20px rgba(234 156 0, 0.3), inset 0 0 20px rgba(234 156 0, 0.1)', + backdropFilter: 'none', + animations: { + glow: true, + scanline: true, + 'pixel-blink': true, + }, + }, + 'Acrylic': { + primary: '#00000080', + secondary: '#00000040', + accent: 'rgba(0,0,0,0.75)', + text: '#ffffff', + highlight: '#ffffff', + success: '#00e500', + error: '#e50000', + warning: '#e5e500', + fontFamily: "'Inter', 'Apple Color Emoji'", + borderRadius: '10px', + borderStyle: 'solid', + borderWidth: '0px', + boxShadow: 'none', + backdropFilter: 'blur(20px)', + animations: { + glow: false, + scanline: false, + 'pixel-blink': false, + }, + }, + }, + currentTheme: 'Classic Autobot', + PAINT_UNAVAILABLE: true, + COORDINATE_MODE: 'rows', + COORDINATE_DIRECTION: 'top-left', + COORDINATE_SNAKE: true, + COORDINATE_BLOCK_WIDTH: 6, + COORDINATE_BLOCK_HEIGHT: 2, + autoSwap: true, + autoBuy: 'none', // "none", "max_charges", or "paint_charges" + autoBuyToggle: false, + maxChargesStopEnable: false, + maxChargesBeforeStop: 1500, + }; + + // Expose CONFIG globally for the utils manager and other modules + window.CONFIG = CONFIG; + + const getCurrentTheme = () => CONFIG.THEMES[CONFIG.currentTheme]; + + const switchTheme = (themeName) => { + if (CONFIG.THEMES[themeName]) { + CONFIG.currentTheme = themeName; + saveThemePreference(); + + // APPLY THEME VARS/CLASS (new) + applyTheme(); + + // Recreate UI (kept for now) + createUI(); + } + }; + + // Add this helper (place it after getCurrentTheme/switchTheme definitions) + function applyTheme() { + const theme = getCurrentTheme(); + console.group('%c🎨 Applying Theme in Auto-Image Script', 'color: #8b5cf6; font-weight: bold;'); + console.log(`%c🎯 Target theme: ${CONFIG.currentTheme}`, 'color: #8b5cf6;'); + + // Toggle theme class on documentElement so CSS vars cascade to our UI + document.documentElement.classList.remove( + 'wplace-theme-classic', + 'wplace-theme-classic-light', + 'wplace-theme-acrylic', + 'wplace-theme-neon', + 'wplace-theme-neon-cyan', + 'wplace-theme-neon-light' + ); + + let themeClass = 'wplace-theme-classic'; // default + let themeFileName = 'classic'; // corresponding file name + + if (CONFIG.currentTheme === 'Neon Retro') { + themeClass = 'wplace-theme-neon'; + themeFileName = 'neon'; + } else if (CONFIG.currentTheme === 'Classic Light') { + themeClass = 'wplace-theme-classic-light'; + themeFileName = 'classic-light'; + } else if (CONFIG.currentTheme === 'Neon Retro Cyan') { + themeClass = 'wplace-theme-neon-cyan'; + themeFileName = 'neon-cyan'; + } else if (CONFIG.currentTheme === 'Neon Retro Light') { + themeClass = 'wplace-theme-neon-light'; + themeFileName = 'neon-light'; + } else if (CONFIG.currentTheme === 'Acrylic') { + themeClass = 'wplace-theme-acrylic'; + themeFileName = 'acrylic'; + } + + document.documentElement.classList.add(themeClass); + console.log(`%c✅ Applied theme class: ${themeClass}`, 'color: #8b5cf6;'); + + // Use extension's applyTheme helper if available (loads from local extension resources) + if (typeof window.applyTheme === 'function') { + console.log(`%c🔧 Using extension's applyTheme() helper for: ${themeFileName}`, 'color: #10b981; font-weight: bold;'); + const success = window.applyTheme(themeFileName); + if (success) { + console.log(`%c✅ Theme CSS loaded from extension local resources`, 'color: #10b981; font-weight: bold;'); + console.log(` 📍 Source: Extension local file (themes/${themeFileName}.css)`); + console.log(` 🚀 Performance: Instant load (no network request)`); + } else { + console.warn(`%c⚠️ Extension theme loading failed, fallback to CSS classes only`, 'color: #f59e0b;'); + console.log(` 🎨 Using CSS class: ${themeClass}`); + console.log(` 📝 Note: Theme variables may be limited without CSS file`); + } + } else { + console.log(`%c📝 Extension applyTheme() not available, using CSS class only`, 'color: #f59e0b;'); + console.log(` 🎨 Using CSS class: ${themeClass}`); + console.log(` ⚠️ Note: Full theme styling requires extension helper function`); + } + + // Also set CSS variables explicitly in case you want runtime overrides + const root = document.documentElement; + const setVar = (k, v) => { + try { + root.style.setProperty(k, v); + } catch { } + }; + + setVar('--wplace-primary', theme.primary); + setVar('--wplace-secondary', theme.secondary); + setVar('--wplace-accent', theme.accent); + setVar('--wplace-text', theme.text); + setVar('--wplace-highlight', theme.highlight); + setVar('--wplace-success', theme.success); + setVar('--wplace-error', theme.error); + setVar('--wplace-warning', theme.warning); + + // Typography + look + setVar('--wplace-font', theme.fontFamily || "'Segoe UI', Roboto, sans-serif"); + setVar('--wplace-radius', '' + (theme.borderRadius || '12px')); + setVar('--wplace-border-style', '' + (theme.borderStyle || 'solid')); + setVar('--wplace-border-width', '' + (theme.borderWidth || '1px')); + setVar('--wplace-backdrop', '' + (theme.backdropFilter || 'blur(10px)')); + setVar('--wplace-border-color', 'rgba(255,255,255,0.1)'); + + console.log(`%c🎨 Theme application complete`, 'color: #8b5cf6; font-weight: bold;'); + console.groupEnd(); + } + + const saveThemePreference = () => { + try { + localStorage.setItem('wplace-theme', CONFIG.currentTheme); + } catch (e) { + console.warn('Could not save theme preference:', e); + } + }; + + const loadThemePreference = () => { + try { + const saved = localStorage.getItem('wplace-theme'); + if (saved && CONFIG.THEMES[saved]) { + CONFIG.currentTheme = saved; + } + } catch (e) { + console.warn('Could not load theme preference:', e); + } + }; + + // Dynamically loaded translations + window.loadedTranslations = {}; + + // Available languages + const AVAILABLE_LANGUAGES = [ + 'en', + 'es', + 'ru', + 'pt', + 'vi', + 'fr', + 'id', + 'tr', + 'zh-CN', + 'zh-TW', + 'ja', + 'ko', + 'uk', + ]; + + // Function to load translations from JSON file with retry mechanism + const loadTranslations = async (language, retryCount = 0) => { + if (window.loadedTranslations[language]) { + return window.loadedTranslations[language]; + } + + console.group(`%c🌍 Loading ${language.toUpperCase()} translations`, 'color: #06b6d4; font-weight: bold;'); + + // First try: Check if extension has loaded local resources + if (window.AUTOBOT_LANGUAGES && Object.keys(window.AUTOBOT_LANGUAGES).length > 0) { + console.log(`%c🔍 Checking extension local resources...`, 'color: #06b6d4;'); + + const langFile = `${language}.json`; + const availableFiles = Object.keys(window.AUTOBOT_LANGUAGES || {}); + const regionalMatch = availableFiles.find(file => file.toLowerCase().startsWith(`${language.toLowerCase()}-`)); + const resolvedFile = window.AUTOBOT_LANGUAGES[langFile] ? langFile : regionalMatch; + + if (resolvedFile && window.AUTOBOT_LANGUAGES[resolvedFile]) { + const translations = window.AUTOBOT_LANGUAGES[resolvedFile]; + + // Validate that translations is an object with keys + if ( + typeof translations === 'object' && + translations !== null && + Object.keys(translations).length > 0 + ) { + window.loadedTranslations[language] = translations; + console.log(`%c✅ Loaded ${language} translations from EXTENSION LOCAL FILES`, 'color: #10b981; font-weight: bold;'); + console.log(` 📍 Source: Extension local storage (chrome-extension://)`); + if (resolvedFile !== langFile) { + console.log(` 🔄 Resolved locale: ${resolvedFile.replace('.json', '')}`); + } + console.log(` 📏 Keys count: ${Object.keys(translations).length}`); + console.log(` 🚀 Performance: Instant load (no network request)`); + console.groupEnd(); + return translations; + } + } else { + console.log(`%c📝 ${langFile} not found in extension resources`, 'color: #f59e0b;'); + console.log(` 📋 Available in extension: ${Object.keys(window.AUTOBOT_LANGUAGES).join(', ')}`); + } + } else { + console.log(`%c📝 No extension local resources available`, 'color: #f59e0b;'); + console.log(` 🔍 window.AUTOBOT_LANGUAGES: ${typeof window.AUTOBOT_LANGUAGES}`); + } + + // Second try: Use helper function if available + if (typeof window.getLanguage === 'function') { + console.log(`%c🔧 Trying extension getLanguage() helper...`, 'color: #06b6d4;'); + try { + const translations = window.getLanguage(language); + if ( + typeof translations === 'object' && + translations !== null && + Object.keys(translations).length > 0 + ) { + window.loadedTranslations[language] = translations; + console.log(`%c✅ Loaded ${language} translations via extension helper`, 'color: #10b981; font-weight: bold;'); + console.log(` 📍 Source: Extension getLanguage() function`); + console.log(` 📏 Keys count: ${Object.keys(translations).length}`); + console.groupEnd(); + return translations; + } + } catch (error) { + console.warn(`%c⚠️ Extension helper failed:`, 'color: #f59e0b;', error); + } + } + + // Fallback: Load from CDN (original behavior) + console.log(`%c🌐 Falling back to CDN loading...`, 'color: #8b5cf6;'); + const url = `https://wplace-autobot.github.io/WPlace-AutoBOT/main/lang/${language}.json`; + const maxRetries = 3; + const baseDelay = 1000; // 1 second + + try { + if (retryCount === 0) { + console.log(`🔄 Loading ${language} translations from CDN...`); + } else { + console.log( + `🔄 Retrying ${language} translations (attempt ${retryCount + 1}/${maxRetries + 1})...` + ); + } + + const response = await fetch(url); + if (response.ok) { + const translations = await response.json(); + + // Validate that translations is an object with keys + if ( + typeof translations === 'object' && + translations !== null && + Object.keys(translations).length > 0 + ) { + window.loadedTranslations[language] = translations; + console.log( + `%c📚 Loaded ${language} translations from CDN (${Object.keys(translations).length} keys)`, 'color: #f59e0b; font-weight: bold;' + ); + console.log(` 📍 Source: CDN (wplace-autobot.github.io)`); + console.log(` 🌐 URL: ${url}`); + console.log(` ⚠️ Performance: Network request required`); + console.groupEnd(); + return translations; + } else { + console.warn(`❌ Invalid translation format for ${language}`); + throw new Error('Invalid translation format'); + } + } else { + console.warn( + `❌ CDN returned HTTP ${response.status}: ${response.statusText} for ${language} translations` + ); + throw new Error(`HTTP ${response.status}: ${response.statusText}`); + } + } catch (error) { + console.error( + `❌ Failed to load ${language} translations from CDN (attempt ${retryCount + 1}):`, + error + ); + + // Retry with exponential backoff + if (retryCount < maxRetries) { + const delay = baseDelay * Math.pow(2, retryCount); + console.log(`⏳ Retrying in ${delay}ms...`); + await new Promise((resolve) => setTimeout(resolve, delay)); + console.groupEnd(); + return loadTranslations(language, retryCount + 1); + } + } + + console.error(`%c💥 All translation loading methods failed for ${language}`, 'color: #ef4444; font-weight: bold;'); + console.groupEnd(); + return null; + }; + + const loadLanguagePreference = async () => { + const savedLanguage = localStorage.getItem('wplace_language'); + const browserLocale = navigator.language; + const browserLanguage = browserLocale.split('-')[0]; + + let selectedLanguage = 'en'; // Default fallback + + try { + // Check if we have the saved language available + if (savedLanguage && AVAILABLE_LANGUAGES.includes(savedLanguage)) { + selectedLanguage = savedLanguage; + console.log(`🔄 Using saved language preference: ${selectedLanguage}`); + } + // Try full locale match (e.g. "zh-CN", "zh-TW" etc) + else if (AVAILABLE_LANGUAGES.includes(browserLocale)) { + selectedLanguage = browserLocale; + localStorage.setItem('wplace_language', browserLocale); + console.log(`🔄 Using browser locale: ${selectedLanguage}`); + } + // Try base language match (e.g. "en" for "en-US" or "en-GB" etc) + else if (AVAILABLE_LANGUAGES.includes(browserLanguage)) { + selectedLanguage = browserLanguage; + localStorage.setItem('wplace_language', browserLanguage); + console.log(`🔄 Using browser language: ${selectedLanguage}`); + } + // Use English as fallback + else { + console.log(`🔄 No matching language found, using English fallback`); + } + + // Set the language in state first + state.language = selectedLanguage; + + // Only load translations if not already loaded and not English (which should already be loaded) + if (selectedLanguage !== 'en' && !window.loadedTranslations[selectedLanguage]) { + const loaded = await loadTranslations(selectedLanguage); + if (!loaded) { + console.warn( + `⚠️ Failed to load ${selectedLanguage} translations, falling back to English` + ); + state.language = 'en'; + localStorage.setItem('wplace_language', 'en'); + } + } + } catch (error) { + console.error(`❌ Error in loadLanguagePreference:`, error); + state.language = 'en'; // Always ensure we have a valid language + } + }; + + // Simple user notification function for critical issues + const showTranslationWarning = (message) => { + try { + // Create a simple temporary notification banner + const warning = document.createElement('div'); + warning.style.cssText = ` + position: fixed; top: 10px; right: 10px; z-index: 10001; + background: rgba(255, 193, 7, 0.95); color: #212529; padding: 12px 16px; + border-radius: 8px; font-size: 14px; font-weight: 500; + box-shadow: 0 4px 12px rgba(0,0,0,0.3); border: 1px solid rgba(255, 193, 7, 0.8); + max-width: 300px; word-wrap: break-word; + `; + warning.textContent = message; + document.body.appendChild(warning); + + // Auto-remove after 8 seconds + setTimeout(() => { + if (warning.parentNode) { + warning.remove(); + } + }, 8000); + } catch (e) { + // If DOM manipulation fails, just log + console.warn('Failed to show translation warning UI:', e); + } + }; + + // Initialize translations function + const initializeTranslations = async () => { + try { + console.log('🌐 Initializing translation system...'); + + // Always ensure English is loaded as fallback first + if (!window.loadedTranslations['en']) { + const englishLoaded = await loadTranslations('en'); + if (!englishLoaded) { + console.warn('⚠️ Failed to load English translations from CDN, using fallback'); + showTranslationWarning('⚠️ Translation loading failed, using basic fallbacks'); + } + } + + // Then load user's language preference + await loadLanguagePreference(); + + console.log(`✅ Translation system initialized. Active language: ${state.language}`); + } catch (error) { + console.error('❌ Translation initialization failed:', error); + // Ensure state has a valid language even if loading fails + if (!state.language) { + state.language = 'en'; + } + console.warn('⚠️ Using fallback translations due to initialization failure'); + showTranslationWarning('⚠️ Translation system error, using basic English'); + } + }; + + // Emergency fallback TEXT (minimal) + const FALLBACK_TEXT = { + en: { + title: 'WPlace Auto-Image', + toggleOverlay: 'Toggle Overlay', + scanColors: 'Scan Colors', + uploadImage: 'Upload Image', + resizeImage: 'Process/Edit Image', + processImageFirst: '⚠️ Process the image first via "Process/Edit Image" before placing!', + imageNeedsProcessing: 'Image needs processing', + selectPosition: 'Select Position', + startPainting: 'Start Painting', + stopPainting: 'Stop Painting', + progress: 'Progress', + pixels: 'Pixels', + charges: 'Charges', + batchSize: 'Batch Size', + paintingOrder: 'Painting Order', + paintingOrderSequential: 'Sequential (Top to Bottom)', + paintingOrderColorByColor: 'Color by Color', + currentlyPaintingColor: 'Currently painting: {colorName}', + colorProgress: '{painted} of {total} {colorName} pixels', + cooldownSettings: 'Cooldown Settings', + waitCharges: 'Wait for Charges', + settings: 'Settings', + showStats: 'Show Statistics', + compactMode: 'Compact Mode', + minimize: 'Minimize', + tokenCapturedSuccess: 'Token captured successfully', + turnstileInstructions: 'Complete the verification', + hideTurnstileBtn: 'Hide', + notificationsNotSupported: 'Notifications not supported', + chargesReadyMessage: 'Charges are ready', + chargesReadyNotification: 'WPlace AutoBot', + initMessage: "Click 'Upload Image' to begin", + }, + }; + + // Safe translation function with multiple fallback levels + const getText = (key, replacements = {}) => { + // Try current language first + let text = window.loadedTranslations[state.language]?.[key]; + + // Fallback to English translations + if (!text && state.language !== 'en') { + text = window.loadedTranslations['en']?.[key]; + } + + // Fallback to hardcoded English + if (!text) { + text = FALLBACK_TEXT['en']?.[key]; + } + + // Last resort - return the key itself + if (!text) { + console.warn(`⚠️ Missing translation for key: ${key}`); + return key; + } + + // Handle string replacements like {count}, {time}, etc. + return Object.entries(replacements).reduce((result, [placeholder, value]) => { + return result.replace(new RegExp(`\\{${placeholder}\\}`, 'g'), value); + }, text); + }; + + // UNIFIED ACCOUNT MANAGER + class AccountManager { + constructor() { + this.accounts = []; + this.currentIndex = 0; + } + + // Load accounts from storage + async loadAccounts() { + try { + // Get tokens from localStorage + const tokens = JSON.parse(localStorage.getItem("accounts")) || []; + + // Get account info from storage + const infoAccountsResult = await new Promise((resolve) => { + if (typeof chrome !== 'undefined' && chrome.storage) { + chrome.storage.local.get('infoAccounts', (result) => { + resolve(result.infoAccounts || []); + }); + } else { + // Fallback for when chrome storage isn't available + resolve([]); + } + }); + + console.log(`📥 [ACCOUNT MANAGER] Loading ${tokens.length} accounts`); + + this.accounts = tokens.map((token, index) => { + const info = infoAccountsResult.find(acc => acc.token === token); + return { + token, + displayName: info?.displayName || info?.name || `Account ${index + 1}`, + ID: info?.ID || null, + Charges: info?.Charges || 0, + Max: info?.Max || 0, + Droplets: info?.Droplets || 0, + isCurrent: info?.isCurrent || false, + lastUsed: info?.lastUsed || null, + lastImported: info?.lastImported || null + }; + }); + + // Find current account index + this.currentIndex = this.accounts.findIndex(acc => acc.isCurrent); + if (this.currentIndex === -1) this.currentIndex = 0; + + console.log(`✅ [ACCOUNT MANAGER] Loaded ${this.accounts.length} accounts, current index: ${this.currentIndex}`); + console.log(`🎯 [ACCOUNT MANAGER] Current account: ${this.getCurrentAccount()?.displayName || 'None'}`); + + // Auto-refresh account statuses after loading + if (this.accounts.length > 0) { + console.log('🔄 [ACCOUNT MANAGER] Auto-refreshing account statuses...'); + // Use setTimeout to avoid blocking the load operation + setTimeout(async () => { + try { + await fetchAllAccountDetails(); + console.log('✅ [ACCOUNT MANAGER] Auto-refresh completed'); + } catch (error) { + console.warn('⚠️ [ACCOUNT MANAGER] Auto-refresh failed:', error); + } + }, 100); + } + + return this.accounts; + } catch (error) { + console.error('❌ [ACCOUNT MANAGER] Error loading accounts:', error); + this.accounts = []; + this.currentIndex = 0; + return []; + } + } + + // Simple linear switching + switchToNext() { + if (this.accounts.length === 0) return null; + + const previousIndex = this.currentIndex; + this.currentIndex = (this.currentIndex + 1) % this.accounts.length; + + console.log(`🔄 [ACCOUNT MANAGER] Switching: ${previousIndex} → ${this.currentIndex}`); + console.log(`🎯 [ACCOUNT MANAGER] Target: ${this.accounts[this.currentIndex].displayName}`); + + // Update current flags + this.accounts.forEach((acc, idx) => { + acc.isCurrent = idx === this.currentIndex; + }); + + return this.accounts[this.currentIndex]; + } + + getCurrentAccount() { + return this.accounts[this.currentIndex] || null; + } + + getAccountByIndex(index) { + return this.accounts[index] || null; + } + + updateAccountData(tokenOrData, dataObject = null) { + let accountData; + let targetAccount; + + if (dataObject !== null) { + // Called with (token, dataObject) format + const token = tokenOrData; + accountData = dataObject; + targetAccount = this.accounts.find(acc => acc.token === token); + if (!targetAccount) { + console.warn(`⚠️ [ACCOUNT MANAGER] Account with token not found for update`); + return; + } + } else { + // Called with (dataObject) format - update current account + accountData = tokenOrData; + targetAccount = this.getCurrentAccount(); + } + + if (targetAccount && accountData) { + // Update all provided properties + if (accountData.charges !== undefined || accountData.Charges !== undefined) { + targetAccount.Charges = Math.floor(accountData.charges || accountData.Charges || 0); + } + if (accountData.max !== undefined || accountData.Max !== undefined) { + targetAccount.Max = Math.floor(accountData.max || accountData.Max || 0); + } + if (accountData.droplets !== undefined || accountData.Droplets !== undefined) { + targetAccount.Droplets = Math.floor(accountData.droplets || accountData.Droplets || 0); + } + if (accountData.id !== undefined || accountData.ID !== undefined) { + targetAccount.ID = accountData.id || accountData.ID || targetAccount.ID; + } + if (accountData.displayName !== undefined) { + targetAccount.displayName = accountData.displayName; + } + if (accountData.isCurrent !== undefined) { + // Clear isCurrent from all accounts first, then set for target + this.accounts.forEach(acc => acc.isCurrent = false); + targetAccount.isCurrent = accountData.isCurrent; + } + if (accountData.cooldown !== undefined) { + targetAccount.cooldown = accountData.cooldown; + } + + console.log(`📊 [ACCOUNT MANAGER] Updated ${targetAccount.displayName}: ⚡${targetAccount.Charges}/${targetAccount.Max} 💧${targetAccount.Droplets}${targetAccount.isCurrent ? ' (CURRENT)' : ''}`); + } + } + + getAccountCount() { + return this.accounts.length; + } + + getAllAccounts() { + return [...this.accounts]; // Return copy to prevent direct mutation + } + + setCurrentIndex(index) { + if (index >= 0 && index < this.accounts.length) { + this.currentIndex = index; + console.log(`🔄 [ACCOUNT MANAGER] Current index set to: ${index}`); + } else { + console.warn(`⚠️ [ACCOUNT MANAGER] Invalid index ${index}, keeping current: ${this.currentIndex}`); + } + } + + getNextAccount() { + if (this.accounts.length === 0) return null; + const nextIndex = (this.currentIndex + 1) % this.accounts.length; + return this.accounts[nextIndex]; + } + } + + // Create global account manager instance + const accountManager = new AccountManager(); + + // GLOBAL STATE + const state = { + running: false, + imageLoaded: false, + imageProcessed: false, // Track if image has been processed through resize panel + processing: false, + totalPixels: 0, + paintedPixels: 0, + preFilteringDone: false, // Track if pre-filtering detection has been done this session + progressResetDone: false, // Track if progress reset has been done this save file session + availableColors: [], + activeColorPalette: [], // User-selected colors for conversion + paintWhitePixels: true, // Default to ON + fullChargeData: null, + fullChargeInterval: null, + paintTransparentPixels: false, // Default to OFF + displayCharges: 0, + preciseCurrentCharges: 0, + maxCharges: 1, // Default max charges + cooldown: CONFIG.COOLDOWN_DEFAULT, + imageData: null, + stopFlag: false, + colorsChecked: false, + startPosition: null, + selectingPosition: false, + region: null, + minimized: false, + lastPosition: { x: 0, y: 0 }, + lastPaintedPosition: { x: 0, y: 0 }, // Track last successfully painted coordinate + estimatedTime: 0, + language: 'en', + paintingSpeed: CONFIG.PAINTING_SPEED.DEFAULT, // pixels batch size + batchMode: CONFIG.BATCH_MODE, // "normal" or "random" + paintingOrder: CONFIG.PAINTING_ORDER, // "sequential" or "color-by-color" + currentPaintingColor: null, // Track current color being painted in color-by-color mode + randomBatchMin: CONFIG.RANDOM_BATCH_RANGE.MIN, // Random range minimum + randomBatchMax: CONFIG.RANDOM_BATCH_RANGE.MAX, // Random range maximum + cooldownChargeThreshold: CONFIG.COOLDOWN_CHARGE_THRESHOLD, + chargesThresholdInterval: null, + tokenSource: CONFIG.TOKEN_SOURCE, // "generator" or "manual" + initialSetupComplete: false, // Track if initial startup setup is complete (only happens once) + overlayOpacity: CONFIG.OVERLAY.OPACITY_DEFAULT, + blueMarbleEnabled: CONFIG.OVERLAY.BLUE_MARBLE_DEFAULT, + ditheringEnabled: false, + // Advanced color matching settings + colorMatchingAlgorithm: 'lab', + enableChromaPenalty: true, + chromaPenaltyWeight: 0.15, + customTransparencyThreshold: CONFIG.TRANSPARENCY_THRESHOLD, + customWhiteThreshold: CONFIG.WHITE_THRESHOLD, + resizeSettings: null, + originalImage: null, + resizeIgnoreMask: null, + paintUnavailablePixels: CONFIG.PAINT_UNAVAILABLE, + // Coordinate generation settings + coordinateMode: CONFIG.COORDINATE_MODE, + coordinateDirection: CONFIG.COORDINATE_DIRECTION, + coordinateSnake: CONFIG.COORDINATE_SNAKE, + blockWidth: CONFIG.COORDINATE_BLOCK_WIDTH, + blockHeight: CONFIG.COORDINATE_BLOCK_HEIGHT, + notificationsEnabled: CONFIG.NOTIFICATIONS.ENABLED, + notifyOnChargesReached: CONFIG.NOTIFICATIONS.ON_CHARGES_REACHED, + notifyOnlyWhenUnfocused: CONFIG.NOTIFICATIONS.ONLY_WHEN_UNFOCUSED, + notificationIntervalMinutes: CONFIG.NOTIFICATIONS.REPEAT_MINUTES, + _lastChargesNotifyAt: 0, + _lastChargesBelow: true, + // Smart save tracking + _lastSavePixelCount: 0, + _lastSaveTime: 0, + _saveInProgress: false, + paintedMap: null, + accountIndex: 0, // Keep for backward compatibility with existing logic + // Legacy state removed - now using accountManager instead + isFetchingAllAccounts: false, + paintingMode: 'auto', // 'auto' or 'assist' - default to auto + }; + + // Expose state globally for the utils manager and other modules + window.state = state; + + let _updateResizePreview = () => { }; + let _resizeDialogCleanup = null; + + // --- OVERLAY UPDATE: Optimized OverlayManager class with performance improvements --- + // OverlayManager class extracted to overlay-manager.js module + + // Overlay management is now handled by the OverlayManager module + // Check for overlay manager availability + if (!window.WPlaceOverlayManager) { + 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) { + overlayManager = new window.WPlaceOverlayManager(); + window.autoImageOverlayManager = overlayManager; + console.log('🎯 Created NEW overlay manager instance'); + } else { + overlayManager = window.autoImageOverlayManager; + console.log('🎯 Using EXISTING overlay manager instance'); + } + + // Token management is now handled by the TokenManager module + // Global references for backward compatibility + if (!window.WPlaceTokenManager) { + 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) => { + tokenManager._resolveToken = resolve; + }); + }; + + // Create global image processor instance for utility functions + if (!window.WPlaceImageProcessor) { + 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; + const MAX_BATCH_RETRIES = 10; // Maximum attempts for batch sending + + function inject(callback) { + const script = document.createElement('script'); + script.textContent = `(${callback})();`; + document.documentElement?.appendChild(script); + script.remove(); + } + + inject(() => { + const fetchedBlobQueue = new Map(); + + window.addEventListener('message', (event) => { + const { source, blobID, blobData } = event.data; + if (source === 'auto-image-overlay' && blobID && blobData) { + const callback = fetchedBlobQueue.get(blobID); + if (typeof callback === 'function') { + callback(blobData); + } + fetchedBlobQueue.delete(blobID); + } + }); + + // Helper function to find closest color ID for assist mode + function findClosestColorId(r, g, b) { + let minDistance = Infinity; + let closestColorId = null; + + for (const colorData of Object.values(CONFIG.COLOR_MAP)) { + if (!colorData.rgb) continue; + + const distance = Math.sqrt( + Math.pow(r - colorData.rgb.r, 2) + + Math.pow(g - colorData.rgb.g, 2) + + Math.pow(b - colorData.rgb.b, 2) + ); + + if (distance < minDistance) { + minDistance = distance; + closestColorId = colorData.id; + } + } + + return closestColorId; + } + + const originalFetch = window.fetch; + // Setup fetch interceptions + console.log('🚀 [Auto-Image] Fetch interception initialized'); + + // Expose state for debugging + window.__WPLACE_AUTOBOT_STATE__ = state; + + window.fetch = async function (...args) { + let url = args[0] instanceof Request ? args[0].url : args[0]; + + // Parse URL if it's a string + let urlObj = null; + if (typeof url === 'string') { + try { + urlObj = new URL(url); + } catch (e) { + // Invalid URL, proceed normally + } + } + + // ASSIST MODE: Intercept pixel placement requests + if (state.paintingMode === 'assist' && urlObj !== null && urlObj.pathname.startsWith('/s0/pixel')) { + console.log(`🎯 [Assist Mode] Intercepting pixel placement:`, urlObj.pathname); + console.log(`🔍 [Assist Mode] Check state - imageData:`, !!state.imageData, 'startPosition:', !!state.startPosition, 'body:', !!args[1]?.body); + + // Check if we have overlay data + if (state.imageData && state.startPosition && args[1]?.body) { + try { + // Parse region coordinates from URL path: /s0/pixel/{tileX}/{tileY} + const pathParts = urlObj.pathname.split('/'); + const regionX = parseInt(pathParts[3]); + const regionY = parseInt(pathParts[4]); + + console.log(`📦 [Assist Mode] Region coords: (${regionX}, ${regionY})`); + + // Parse request body + const payload = typeof args[1].body === 'string' ? JSON.parse(args[1].body) : args[1].body; + console.log(`📝 [Assist Mode] Original payload:`, JSON.stringify(payload)); + + // Debug: Check what's in state.region + console.log(`🔍 [Assist Mode] state.region:`, state.region); + console.log(`🔍 [Assist Mode] state.startPosition:`, state.startPosition); + + // CRITICAL FIX: The issue is that startPosition is in TILE-LOCAL coordinates (0-999) + // but we're clicking on a tile at (1732, 1014). We need to find which tile + // our image overlay is on by checking all loaded tiles. + + // For now, let's use the REQUEST's tile coordinates as a reference + // If the user is clicking within this tile, check if our LOCAL startPosition + // would overlap with the click position + let startX, startY; + + // COORDINATE RECONSTRUCTION + // state.startPosition = LOCAL coords within a 1000x1000 tile (0-999) + // state.region = TILE coordinates {x, y} (e.g., {x: 1732, y: 1014}) + // Absolute canvas coords = region.x * 1000 + startPosition.x + + if (state.region && state.region.x !== undefined && state.region.y !== undefined) { + // Use saved region to reconstruct absolute coordinates + startX = state.region.x * 1000 + state.startPosition.x; + startY = state.region.y * 1000 + state.startPosition.y; + console.log(`[Assist Mode] Using saved region: tile=(${state.region.x},${state.region.y}) + local=(${state.startPosition.x},${state.startPosition.y}) = absolute (${startX},${startY})`); + } else { + // Fallback: assume image is on the clicked tile (may be wrong!) + // Since startPosition appears to be LOCAL coords within a tile, + // we need to convert them to absolute by finding which tile they're on. + // The simplest approach: assume the image is on the tile being clicked! + startX = regionX * 1000 + state.startPosition.x; + startY = regionY * 1000 + state.startPosition.y; + console.log(`[Assist Mode] No saved region - assuming image on clicked tile (${regionX},${regionY}): absolute = (${startX},${startY})`); + } + console.log(`� [Assist Mode] Assuming image is on clicked tile: absolute coords = (${startX},${startY})`); + + const { width, height } = state.imageData; + + console.log(`🖼️ [Assist Mode] Image bounds: start=(${startX},${startY}), size=${width}x${height}`); + + // Calculate which tiles the image spans + // Note: startX/startY are ABSOLUTE canvas coordinates (e.g., 1732432 means tile 1732, pixel 432) + const imageStartTileX = Math.floor(startX / 1000); + const imageStartTileY = Math.floor(startY / 1000); + const imageEndTileX = Math.floor((startX + width - 1) / 1000); + const imageEndTileY = Math.floor((startY + height - 1) / 1000); + + console.log(`🗺️ [Assist Mode] Image tiles: X[${imageStartTileX}-${imageEndTileX}], Y[${imageStartTileY}-${imageEndTileY}]`); + console.log(`🎯 [Assist Mode] Checking if region (${regionX},${regionY}) overlaps...`); + + if (regionX >= imageStartTileX && regionX <= imageEndTileX && + regionY >= imageStartTileY && regionY <= imageEndTileY) { + + console.log(`✅ [Assist Mode] Region (${regionX},${regionY}) overlaps with image - modifying pixels`); + + // Modify the colors array based on overlay data + if (payload.coords && payload.colors) { + let modifiedCount = 0; + + for (let i = 0; i < payload.coords.length; i += 2) { + // Calculate absolute pixel position + const absoluteX = regionX * 1000 + payload.coords[i]; + const absoluteY = regionY * 1000 + payload.coords[i + 1]; + + // Calculate position relative to our image + const imgX = absoluteX - startX; + const imgY = absoluteY - startY; + + console.log(`🎨 [Assist Mode] Pixel ${i/2}: absolute=(${absoluteX},${absoluteY}), relative=(${imgX},${imgY})`); + + // Check if pixel is within our image bounds + if (imgX >= 0 && imgX < width && imgY >= 0 && imgY < height) { + // Get color from processed image data + // Note: state.imageData uses 'pixels' property, not 'data' + const pixelData = state.imageData.pixels || state.imageData.data; + if (!pixelData) { + console.error(`❌ [Assist Mode] No pixel data available in state.imageData`); + continue; + } + + const pixelIndex = (imgY * width + imgX) * 4; + const r = pixelData[pixelIndex]; + const g = pixelData[pixelIndex + 1]; + const b = pixelData[pixelIndex + 2]; + const a = pixelData[pixelIndex + 3]; + + console.log(`🎨 [Assist Mode] Color at (${imgX},${imgY}): rgba(${r},${g},${b},${a}), original colorId: ${payload.colors[i/2]}`); + + // Skip transparent pixels + if (a < CONFIG.TRANSPARENCY_THRESHOLD) { + console.log(`⏭️ [Assist Mode] Skipping transparent pixel`); + continue; + } + + // Find matching color from palette + const colorId = findClosestColorId(r, g, b); + console.log(`🔍 [Assist Mode] Found closest colorId: ${colorId}`); + if (colorId) { + const oldColorId = payload.colors[i / 2]; + payload.colors[i / 2] = colorId; + console.log(`✏️ [Assist Mode] Changed colorId from ${oldColorId} to ${colorId}`); + modifiedCount++; + } + } else { + console.log(`❌ [Assist Mode] Pixel out of bounds`); + } + } + + // Update request body with modified payload + args[1] = { + ...args[1], + body: JSON.stringify(payload) + }; + + console.log(`✅ [Assist Mode] Modified ${modifiedCount} pixel color(s)`); + console.log(`📝 [Assist Mode] Modified payload:`, JSON.stringify(payload)); + } else { + console.warn(`⚠️ [Assist Mode] Payload missing coords or colors`); + } + } else { + console.log(`⏭️ [Assist Mode] Region (${regionX},${regionY}) does NOT overlap with image`); + } + } catch (error) { + console.error('❌ [Assist Mode] Error intercepting pixel placement:', error); + console.error(error.stack); + } + } else { + console.warn('⚠️ [Assist Mode] No overlay data available - imageData:', !!state.imageData, 'startPosition:', !!state.startPosition); + } + } + + const response = await originalFetch.apply(this, args); + + // TILE REQUEST logging removed to reduce console spam + + if (typeof url === 'string') { + if (url.includes('https://backend.wplace.live/s0/pixel/')) { + try { + const payload = JSON.parse(args[1].body); + if (payload.t) { + console.log( + `🔍✅ Turnstile Token Captured - Type: ${typeof payload.t}, Value: ${payload.t + ? typeof payload.t === 'string' + ? payload.t.length > 50 + ? payload.t.substring(0, 50) + '...' + : payload.t + : JSON.stringify(payload.t) + : 'null/undefined' + }, Length: ${payload.t?.length || 0}` + ); + window.postMessage({ source: 'turnstile-capture', token: payload.t }, '*'); + } + } catch (_) { + /* ignore */ + } + } + + const contentType = response.headers.get('content-type') || ''; + if (contentType.includes('image/png') && url.includes('.png')) { + const cloned = response.clone(); + return new Promise(async (resolve) => { + const blobUUID = crypto.randomUUID(); + const originalBlob = await cloned.blob(); + + fetchedBlobQueue.set(blobUUID, (processedBlob) => { + resolve( + new Response(processedBlob, { + headers: cloned.headers, + status: cloned.status, + statusText: cloned.statusText, + }) + ); + }); + + window.postMessage( + { + source: 'auto-image-tile', + endpoint: url, + blobID: blobUUID, + blobData: originalBlob, + }, + '*' + ); + }); + } + } + + return response; + }; + }); + + window.addEventListener('message', (event) => { + const { source, endpoint, blobID, blobData } = event.data; + + if (source === 'auto-image-tile' && endpoint && blobID && blobData) { + overlayManager.processAndRespondToTileRequest(event.data); + } + + // Token capture is now handled by TokenManager module + // No need to handle 'turnstile-capture' events here + }); + + async function detectLanguage() { + try { + const response = await fetch('https://backend.wplace.live/me', { + credentials: 'include', + }); + const data = await response.json(); + state.language = data.language === 'pt' ? 'pt' : 'en'; + } catch { + state.language = navigator.language.startsWith('pt') ? 'pt' : 'en'; + } + } + + // UTILITY FUNCTIONS WRAPPER - Uses WPlaceUtilsManager for modular functionality + const Utils = { + // Basic utilities + 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) : () => { }, + + // Tile calculation + 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(), + ensureTurnstileOverlayContainer: () => window.globalUtilsManager ? window.globalUtilsManager.ensureTurnstileOverlayContainer() : Promise.resolve(), + executeTurnstile: (sitekey, action) => window.globalUtilsManager ? window.globalUtilsManager.executeTurnstile(sitekey, action) : Promise.resolve(null), + createTurnstileWidget: (sitekey, action) => window.globalUtilsManager ? window.globalUtilsManager.createTurnstileWidget(sitekey, action) : Promise.resolve(), + 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) => { + if (window.globalUtilsManager) { + return window.globalUtilsManager.createButton(id, text, icon, onClick, style); + } + const btn = document.createElement('button'); + btn.id = id; + btn.innerHTML = `${icon ? `` : ''}${text}`; + if (onClick) btn.addEventListener('click', onClick); + return btn; + }, + + // Translation with fallback + t: (key, params) => { + if (window.globalUtilsManager) { + return window.globalUtilsManager.t(key, params); + } + return getText(key, params); + }, + + // Open website color palette if not already open + openColorPalette: () => { + try { + // Check if color palette modal is already open + const paletteModal = document.querySelector('dialog[open]'); + if (paletteModal) { + console.log('✅ Color palette already open'); + return true; + } + + // Search for Paint/Pintar button (like Auto-Farm does) + const allButtons = Array.from(document.querySelectorAll('button')); + const paletteButton = allButtons.find(btn => + /(Pintar|Paint)/i.test(btn.textContent.trim()) + ); + + if (paletteButton) { + console.log('🎨 Opening color palette automatically...'); + paletteButton.click(); + return true; + } + + console.warn('⚠️ Color palette button not found'); + return false; + } catch (error) { + console.error('❌ Error opening color palette:', error); + return false; + } + }, + + showAlert: (message, type) => { + if (window.globalUtilsManager) { + window.globalUtilsManager.showAlert(message, type); + } else { + console.log(`Alert [${type}]: ${message}`); + } + }, + + hideAlert: () => { + // Remove existing alerts immediately with safety checks + const alerts = document.querySelectorAll('.wplace-alert-base'); + alerts.forEach(alert => { + try { + if (alert && alert.parentNode) { + alert.parentNode.removeChild(alert); + } + } catch (error) { + // Ignore removeChild errors if element was already removed + console.debug('Alert already removed:', error); + } + }); + }, + + // 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) => { + const input = document.createElement('input'); + input.type = 'file'; + input.accept = 'image/*'; + input.onchange = (e) => { + const file = e.target.files[0]; + if (file) { + const reader = new FileReader(); + reader.onload = () => resolve(reader.result); + reader.onerror = () => reject(new Error('File reading error')); + reader.readAsDataURL(file); + } else { + reject(new Error('No file selected')); + } + }; + input.click(); + }); + }, + + createFileUploader: () => { + return new Promise((resolve, reject) => { + const input = document.createElement('input'); + input.type = 'file'; + input.accept = '.json'; + input.onchange = (e) => { + const file = e.target.files[0]; + if (file) { + const reader = new FileReader(); + reader.onload = () => { + try { + const data = JSON.parse(reader.result); + resolve(data); + } catch (error) { + reject(new Error('Invalid JSON file')); + } + }; + reader.onerror = () => reject(new Error('File reading error')); + reader.readAsText(file); + } else { + reject(new Error('No file selected')); + } + }; + input.click(); + }); + }, + + createFileDownloader: (data, filename) => { + const blob = new Blob([data], { 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); + }, + + loadProgress: () => { + try { + const saved = localStorage.getItem('wplace-bot-progress'); + if (!saved) return null; + return JSON.parse(saved); + } catch (error) { + console.error('Error loading progress:', error); + return null; + } + }, + + saveProgress: () => { + try { + // Use utils manager to build proper save structure like old version + const data = window.globalUtilsManager ? + window.globalUtilsManager.buildProgressData() : + { + timestamp: Date.now(), + version: '2.2', + state: { + totalPixels: window.state.totalPixels, + paintedPixels: window.state.paintedPixels, + lastPosition: window.state.lastPosition, + startPosition: window.state.startPosition, + region: window.state.region, + imageLoaded: window.state.imageLoaded, + colorsChecked: window.state.colorsChecked, + coordinateMode: window.state.coordinateMode, + coordinateDirection: window.state.coordinateDirection, + coordinateSnake: window.state.coordinateSnake, + blockWidth: window.state.blockWidth, + blockHeight: window.state.blockHeight, + availableColors: window.state.availableColors, + }, + imageData: window.state.imageData ? { + width: window.state.imageData.width, + height: window.state.imageData.height, + pixels: Array.from(window.state.imageData.pixels), + totalPixels: window.state.imageData.totalPixels, + } : null, + paintedMapPacked: window.globalUtilsManager ? + window.globalUtilsManager.buildPaintedMapPacked() : null + }; + + // Attempt to save; if payload is too large (QuotaExceeded), retry without heavy pixel data + try { + localStorage.setItem('wplace-bot-progress', JSON.stringify(data)); + return true; + } catch (e) { + // Fallback: strip image pixels to reduce size and retry + if (data && data.imageData && data.imageData.pixels) { + const slimData = { + ...data, + imageData: { + width: data.imageData.width, + height: data.imageData.height, + totalPixels: data.imageData.totalPixels, + pixelsStripped: true, + } + }; + try { + localStorage.setItem('wplace-bot-progress', JSON.stringify(slimData)); + console.warn('Saved progress without raw pixel data due to storage quota limits.'); + return true; + } catch (e2) { + try { + // Last resort: try sessionStorage + sessionStorage.setItem('wplace-bot-progress', JSON.stringify(slimData)); + console.warn('Saved progress to sessionStorage without pixel data due to storage quota limits.'); + return true; + } catch (e3) { + throw e2; // will be handled by outer catch + } + } + } + throw e; + } + } catch (error) { + console.error('Error saving progress:', error); + return false; + } + }, + + clearProgress: () => { + try { + localStorage.removeItem('wplace-bot-progress'); + if (window.state) { + window.state.paintedMap = null; + window.state._lastSavePixelCount = 0; + window.state._lastSaveTime = 0; + } + return true; + } catch (error) { + console.error('Error clearing progress:', error); + return false; + } + }, + + restoreProgress: (savedData) => { + try { + 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); + + // Reset session-specific flags when loading a new save file + window.state.preFilteringDone = false; + window.state.progressResetDone = false; + console.log('🔄 Reset session flags for new save file load'); + + 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 && Array.isArray(savedData.imageData.pixels)) { + 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 if (savedData.imageData && savedData.imageData.pixelsRef) { + console.warn('ℹ️ [DEBUG] Large image detected; loading pixels from IndexedDB via ref:', savedData.imageData.pixelsRef); + window.state.imageData = { + width: savedData.imageData.width, + height: savedData.imageData.height, + totalPixels: savedData.imageData.totalPixels, + pixels: null, + }; + window.state.imageLoaded = false; + + if (window.globalUtilsManager && typeof window.globalUtilsManager.loadPixelsFromIndexedDB === 'function') { + window.globalUtilsManager.loadPixelsFromIndexedDB(savedData.imageData.pixelsRef) + .then(payload => { + if (!payload || !payload.pixels) { + console.warn('⚠️ [DEBUG] Pixels not found in IDB. Ask user to reload image.'); + return; + } + window.state.imageData.pixels = new Uint8ClampedArray(payload.pixels); + window.state.imageLoaded = true; + console.log('✅ [DEBUG] Pixels loaded from IDB'); + + // Try to restore overlay immediately + if (typeof window.globalUtilsManager.restoreOverlayFromData === 'function') { + window.globalUtilsManager.restoreOverlayFromData().catch(() => {}); + } + }) + .catch(err => console.warn('❌ [DEBUG] Failed to load pixels from IDB:', err)); + } + } else if (savedData.imageData && savedData.imageData.pixelsStripped) { + console.warn('⚠️ [DEBUG] Saved progress did not include raw pixel data due to quota limits. Please reload the image file to resume.'); + // Preserve dimensions for UI, but mark image as not loaded + window.state.imageData = { + width: savedData.imageData.width, + height: savedData.imageData.height, + totalPixels: savedData.imageData.totalPixels, + pixels: null, + }; + window.state.imageLoaded = false; + } 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('❌ [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() : + { + timestamp: Date.now(), + version: '2.2', + state: { + totalPixels: window.state.totalPixels, + paintedPixels: window.state.paintedPixels, + lastPosition: window.state.lastPosition, + startPosition: window.state.startPosition, + region: window.state.region, + imageLoaded: window.state.imageLoaded, + colorsChecked: window.state.colorsChecked, + coordinateMode: window.state.coordinateMode, + coordinateDirection: window.state.coordinateDirection, + coordinateSnake: window.state.coordinateSnake, + blockWidth: window.state.blockWidth, + blockHeight: window.state.blockHeight, + availableColors: window.state.availableColors, + }, + imageData: window.state.imageData ? { + width: window.state.imageData.width, + height: window.state.imageData.height, + pixels: Array.from(window.state.imageData.pixels), + totalPixels: window.state.imageData.totalPixels, + } : null, + paintedMapPacked: window.globalUtilsManager ? + window.globalUtilsManager.buildPaintedMapPacked() : null + }; + + const filename = `wplace-bot-progress-${new Date() + .toISOString() + .slice(0, 19) + .replace(/:/g, '-')}.json`; + Utils.createFileDownloader(JSON.stringify(progressData, null, 2), filename); + return true; + } catch (error) { + console.error('Error saving to file:', error); + return false; + } + }, + + loadProgressFromFile: async () => { + try { + console.log('🔍 [DEBUG] Starting file upload process...'); + const data = await Utils.createFileUploader(); + + 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('❌ [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) { + return window.globalUtilsManager.extractAvailableColors(); + } else if (window.globalImageProcessor) { + return window.globalImageProcessor.extractAvailableColors(window.CONFIG.COLOR_MAP); + } else { + return []; + } + }, + 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, + isPixelPainted: (x, y, regionX, regionY) => window.globalUtilsManager ? window.globalUtilsManager.isPixelPainted(x, y, regionX, regionY) : false, + shouldAutoSave: () => window.globalUtilsManager ? window.globalUtilsManager.shouldAutoSave() : false, + performSmartSave: () => window.globalUtilsManager ? window.globalUtilsManager.performSmartSave() : false, + packPaintedMapToBase64: (paintedMap, width, height) => window.globalUtilsManager ? window.globalUtilsManager.packPaintedMapToBase64(paintedMap, width, height) : null, + unpackPaintedMapFromBase64: (base64, width, height) => window.globalUtilsManager ? window.globalUtilsManager.unpackPaintedMapFromBase64(base64, width, height) : null, + migrateProgressToV2: (saved) => window.globalUtilsManager ? window.globalUtilsManager.migrateProgressToV2(saved) : saved, + migrateProgressToV21: (saved) => window.globalUtilsManager ? window.globalUtilsManager.migrateProgressToV21(saved) : saved, + migrateProgressToV22: (data) => window.globalUtilsManager ? window.globalUtilsManager.migrateProgressToV22(data) : data, + buildPaintedMapPacked: () => window.globalUtilsManager ? window.globalUtilsManager.buildPaintedMapPacked() : null, + buildProgressData: () => window.globalUtilsManager ? window.globalUtilsManager.buildProgressData() : {}, + migrateProgress: (saved) => window.globalUtilsManager ? window.globalUtilsManager.migrateProgress(saved) : saved, + restoreOverlayFromData: () => window.globalUtilsManager ? window.globalUtilsManager.restoreOverlayFromData() : Promise.resolve(false), + updateCoordinateUI: (config) => window.globalUtilsManager ? window.globalUtilsManager.updateCoordinateUI(config) : console.log('Coordinate UI update not available'), + }; + + // IMAGE PROCESSOR WRAPPER - Uses WPlaceImageProcessor for modular functionality + class ImageProcessor { + constructor(imageSrc) { + // Create instance of the modular ImageProcessor + this._processor = new window.WPlaceImageProcessor(imageSrc); + + // Legacy compatibility properties + this.imageSrc = imageSrc; + this.img = null; + this.canvas = null; + this.ctx = null; + } + + async load() { + await this._processor.load(); + + // Update legacy properties for compatibility + this.img = this._processor.img; + this.canvas = this._processor.canvas; + this.ctx = this._processor.ctx; + } + + getDimensions() { + return this._processor.getDimensions(); + } + + getPixelData() { + return this._processor.getPixelData(); + } + + 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; + } + + generatePreview(width, height) { + return this._processor.generatePreview(width, height); + } + + // Cleanup method + cleanup() { + if (this._processor) { + this._processor.cleanup(); + } + } + } + + // WPLACE API SERVICE + const WPlaceService = { + async paintPixelInRegion(regionX, regionY, pixelX, pixelY, color) { + try { + await ensureToken(); + if (!getTurnstileToken()) return 'token_error'; + const payload = { + coords: [pixelX, pixelY], + colors: [color], + t: getTurnstileToken(), + fp: fpStr32, + }; + var token = await createWasmToken(regionX, regionY, payload); + const res = await fetch(`https://backend.wplace.live/s0/pixel/${regionX}/${regionY}`, { + method: 'POST', + headers: { 'Content-Type': 'text/plain;charset=UTF-8', "x-pawtect-token": token }, + credentials: 'include', + body: JSON.stringify(payload), + }); + if (res.status === 403) { + console.error('❌ 403 Forbidden. Turnstile token might be invalid or expired.'); + setTurnstileToken(null); + createTokenPromise(); + return 'token_error'; + } + const data = await res.json(); + return data?.painted === 1; + } catch (e) { + console.error('Paint request failed:', e); + return false; + } + }, + + async getCharges() { + try { + const res = await fetch("https://backend.wplace.live/me", { + credentials: "include", + }) + const data = await res.json() + return { + id: data.id, + charges: data.charges?.count || 0, + max: data.charges?.max || 1, + cooldown: data.charges?.next || CONFIG.COOLDOWN_DEFAULT, + droplets: data.droplets || 0, + } + } catch (e) { + console.error("Failed to get charges:", e) + return { + id: null, + charges: 0, + max: 1, + cooldown: CONFIG.COOLDOWN_DEFAULT, + droplets: 0, + } + } + }, + + async fetchCheck() { + try { + const res = await fetch("https://backend.wplace.live/me", { + credentials: "include", + }) + const data = await res.json() + return { + ID: data.id, + Username: data.username || data.name || null, + Charges: data.charges.count, + Max: data.charges.max, + Droplets: data.droplets + } + } catch (e) { + console.error("Failed to get ID:", e) + return { + + } + } + } + }; + + // Desktop Notification Manager + const NotificationManager = { + pollTimer: null, + pollIntervalMs: 60_000, + icon() { + const link = document.querySelector("link[rel~='icon']"); + return link?.href || location.origin + '/favicon.ico'; + }, + async requestPermission() { + if (!('Notification' in window)) { + Utils.showAlert(Utils.t('notificationsNotSupported'), 'warning'); + return 'denied'; + } + if (Notification.permission === 'granted') return 'granted'; + try { + const perm = await Notification.requestPermission(); + return perm; + } catch { + return Notification.permission; + } + }, + canNotify() { + return ( + state.notificationsEnabled && + typeof Notification !== 'undefined' && + Notification.permission === 'granted' + ); + }, + notify(title, body, tag = 'wplace-charges', force = false) { + if (!this.canNotify()) return false; + if (!force && state.notifyOnlyWhenUnfocused && document.hasFocus()) return false; + try { + new Notification(title, { + body, + tag, + renotify: true, + icon: this.icon(), + badge: this.icon(), + silent: false, + }); + return true; + } catch { + // Graceful fallback + Utils.showAlert(body, 'info'); + return false; + } + }, + resetEdgeTracking() { + state._lastChargesBelow = state.displayCharges < state.cooldownChargeThreshold; + state._lastChargesNotifyAt = 0; + }, + maybeNotifyChargesReached(force = false) { + if (!state.notificationsEnabled || !state.notifyOnChargesReached) return; + const reached = state.displayCharges >= state.cooldownChargeThreshold; + const now = Date.now(); + const repeatMs = Math.max(1, Number(state.notificationIntervalMinutes || 5)) * 60_000; + if (reached) { + const shouldEdge = state._lastChargesBelow || force; + const shouldRepeat = now - (state._lastChargesNotifyAt || 0) >= repeatMs; + if (shouldEdge || shouldRepeat) { + const msg = Utils.t('chargesReadyMessage', { + current: state.displayCharges, + max: state.maxCharges, + threshold: state.cooldownChargeThreshold, + }); + this.notify(Utils.t('chargesReadyNotification'), msg, 'wplace-notify-charges'); + state._lastChargesNotifyAt = now; + } + state._lastChargesBelow = false; + } else { + state._lastChargesBelow = true; + } + }, + startPolling() { + this.stopPolling(); + if (!state.notificationsEnabled || !state.notifyOnChargesReached) return; + // lightweight background polling + this.pollTimer = setInterval(async () => { + try { + const { charges, cooldown, max } = await WPlaceService.getCharges(); + state.displayCharges = Math.floor(charges); + state.cooldown = cooldown; + state.maxCharges = Math.max(1, Math.floor(max)); + this.maybeNotifyChargesReached(); + } catch { + /* ignore */ + } + }, this.pollIntervalMs); + }, + stopPolling() { + if (this.pollTimer) { + clearInterval(this.pollTimer); + this.pollTimer = null; + } + }, + syncFromState() { + this.resetEdgeTracking(); + if (state.notificationsEnabled && state.notifyOnChargesReached) this.startPolling(); + else this.stopPolling(); + }, + }; + + // COLOR MATCHING FUNCTION - Optimized with caching + const colorCache = new Map(); + + // UI UPDATE FUNCTIONS (declared early to avoid reference errors) + let updateUI = () => { }; + let updateStats = async () => { }; + let updateDataButtons = () => { }; + + function updateActiveColorPalette() { + state.activeColorPalette = []; + const activeSwatches = document.querySelectorAll('.wplace-color-swatch.active'); + if (activeSwatches) { + activeSwatches.forEach((swatch) => { + const rgbStr = swatch.getAttribute('data-rgb'); + if (rgbStr) { + const rgb = rgbStr.split(',').map(Number); + state.activeColorPalette.push(rgb); + } + }); + } + if (document.querySelector('.resize-container')?.style.display === 'block') { + _updateResizePreview(); + } + } + + function toggleAllColors(select, showingUnavailable = false) { + const swatches = document.querySelectorAll('.wplace-color-swatch'); + if (swatches) { + swatches.forEach((swatch) => { + // Only toggle colors that are available or if we're showing unavailable colors + const isUnavailable = swatch.classList.contains('unavailable'); + if (!isUnavailable || showingUnavailable) { + // Don't try to select unavailable colors + if (!isUnavailable) { + swatch.classList.toggle('active', select); + } + } + }); + } + updateActiveColorPalette(); + } + + function unselectAllPaidColors() { + const swatches = document.querySelectorAll('.wplace-color-swatch'); + if (swatches) { + swatches.forEach((swatch) => { + const colorId = parseInt(swatch.getAttribute('data-color-id'), 10); + if (!isNaN(colorId) && colorId >= 32) { + swatch.classList.toggle('active', false); + } + }); + } + updateActiveColorPalette(); + } + + function initializeColorPalette(container) { + const colorsContainer = container.querySelector('#colors-container'); + const showAllToggle = container.querySelector('#showAllColorsToggle'); + if (!colorsContainer) return; + + // Use already captured colors from state (captured during upload) + // Don't re-fetch colors here, use what was captured when user clicked upload + if (!state.availableColors || state.availableColors.length === 0) { + // If no colors have been captured yet, show message + colorsContainer.innerHTML = `
${Utils.t( + 'uploadImageFirst' + )}
`; + return; + } + + function populateColors(showUnavailable = false) { + colorsContainer.innerHTML = ''; + let availableCount = 0; + let totalCount = 0; + + // Convert COLOR_MAP to array and filter out transparent + const allColors = Object.values(CONFIG.COLOR_MAP).filter((color) => color.rgb !== null); + + allColors.forEach((colorData) => { + const { id, name, rgb } = colorData; + const rgbKey = `${rgb.r},${rgb.g},${rgb.b}`; + totalCount++; + + // Check if this color is available in the captured colors + const isAvailable = state.availableColors.some( + (c) => c.rgb[0] === rgb.r && c.rgb[1] === rgb.g && c.rgb[2] === rgb.b + ); + + // If not showing all colors and this color is not available, skip it + if (!showUnavailable && !isAvailable) { + return; + } + + if (isAvailable) availableCount++; + + const colorItem = Utils.createElement('div', { + className: 'wplace-color-item', + }); + const swatch = Utils.createElement('button', { + className: `wplace-color-swatch ${!isAvailable ? 'unavailable' : ''}`, + title: `${name} (ID: ${id})${!isAvailable ? ' (Unavailable)' : ''}`, + 'data-rgb': rgbKey, + 'data-color-id': id, + }); + swatch.style.backgroundColor = `rgb(${rgb.r}, ${rgb.g}, ${rgb.b})`; + + // Make unavailable colors visually distinct + if (!isAvailable) { + swatch.style.opacity = '0.4'; + swatch.style.filter = 'grayscale(50%)'; + swatch.disabled = true; + } else { + // Select available colors by default + swatch.classList.add('active'); + } + + const nameLabel = Utils.createElement( + 'span', + { + className: 'wplace-color-item-name', + style: !isAvailable ? 'color: #888; font-style: italic;' : '', + }, + name + (!isAvailable ? ' (N/A)' : '') + ); + + // Only add click listener for available colors + if (isAvailable) { + swatch.addEventListener('click', () => { + swatch.classList.toggle('active'); + updateActiveColorPalette(); + }); + } + + colorItem.appendChild(swatch); + colorItem.appendChild(nameLabel); + colorsContainer.appendChild(colorItem); + }); + + updateActiveColorPalette(); + } + + // Initialize with only available colors + populateColors(false); + + // Add toggle functionality + if (showAllToggle) { + showAllToggle.addEventListener('change', (e) => { + populateColors(e.target.checked); + }); + } + + container + .querySelector('#selectAllBtn') + ?.addEventListener('click', () => toggleAllColors(true, showAllToggle?.checked)); + container + .querySelector('#unselectAllBtn') + ?.addEventListener('click', () => toggleAllColors(false, showAllToggle?.checked)); + container + .querySelector('#unselectPaidBtn') + ?.addEventListener('click', () => unselectAllPaidColors()); + } + + async function handleCaptcha(allowGeneration = true) { + const startTime = performance.now(); + + // Check user's token source preference + if (state.tokenSource === 'manual') { + console.log('🎯 Manual token source selected - using pixel placement automation'); + if (!allowGeneration) { + console.log('❌ Token generation disabled during processing'); + return null; + } + return await tokenManager.handleCaptchaFallback(); + } + + // Generator mode (pure) or Hybrid mode - try generator first + try { + // Use optimized token generation with automatic sitekey detection + const { sitekey, token: preGeneratedToken } = await Utils.obtainSitekeyAndToken(); + + if (!sitekey) { + throw new Error('No valid sitekey found'); + } + + console.log('🔑 Generating Turnstile token for sitekey:', sitekey); + console.log( + '🧭 UA:', + navigator.userAgent.substring(0, 50) + '...', + 'Platform:', + navigator.platform + ); + + // Add additional checks before token generation + if (!window.turnstile) { + await Utils.loadTurnstile(); + } + + let token = null; + + // ✅ Reuse pre-generated token if available and valid + if ( + preGeneratedToken && + typeof preGeneratedToken === 'string' && + preGeneratedToken.length > 20 + ) { + console.log('♻️ Reusing pre-generated token from sitekey detection phase'); + token = preGeneratedToken; + } + // ✅ Or use globally cached token if still valid + else if (isTokenValid()) { + console.log('♻️ Using existing cached token (from previous operation)'); + token = getTurnstileToken(); + } + // ✅ Otherwise generate a new one (only if allowed) + else { + if (!allowGeneration) { + console.log('❌ Token expired/missing but generation disabled during processing'); + return null; + } + console.log('🔐 No valid pre-generated or cached token, creating new one...'); + token = await Utils.executeTurnstile(sitekey, 'paint'); + if (token) { + setTurnstileToken(token); + } + } + + console.log( + `🔍 Token received - Type: ${typeof token}, Value: ${token + ? typeof token === 'string' + ? token.length > 50 + ? token.substring(0, 50) + '...' + : token + : JSON.stringify(token) + : 'null/undefined' + }, Length: ${token?.length || 0}` + ); + + // ✅ Final validation + if (typeof token === 'string' && token.length > 20) { + const duration = Math.round(performance.now() - startTime); + console.log(`✅ Turnstile token generated successfully in ${duration}ms`); + return token; + } else { + throw new Error( + `Invalid or empty token received - Type: ${typeof token}, Value: ${JSON.stringify( + token + )}, Length: ${token?.length || 0}` + ); + } + } catch (error) { + const duration = Math.round(performance.now() - startTime); + console.error(`❌ Turnstile token generation failed after ${duration}ms:`, error); + + // Fallback to manual pixel placement for hybrid mode + if (state.tokenSource === 'hybrid') { + console.log( + '🔄 Hybrid mode: Generator failed, automatically switching to manual pixel placement...' + ); + const fbToken = await tokenManager.handleCaptchaFallback(); + return fbToken; + } else { + // Pure generator mode - don't fallback, just fail + throw error; + } + } + } + + async function createUI() { + await detectLanguage(); + + const existingContainer = document.getElementById('wplace-image-bot-container'); + const existingStats = document.getElementById('wplace-stats-container'); + const existingSettings = document.getElementById('wplace-settings-container'); + const existingResizeContainer = document.querySelector('.resize-container'); + const existingResizeOverlay = document.querySelector('.resize-overlay'); + + if (existingContainer) existingContainer.remove(); + if (existingStats) existingStats.remove(); + if (existingSettings) existingSettings.remove(); + if (existingResizeContainer) existingResizeContainer.remove(); + if (existingResizeOverlay) existingResizeOverlay.remove(); + + loadThemePreference(); + await initializeTranslations(); + + const theme = getCurrentTheme(); + applyTheme(); // <- new: set CSS vars and theme class before building UI + + function appendLinkOnce(href, attributes = {}) { + // Check if a link with the same href already exists in the document head + const exists = Array.from(document.head.querySelectorAll('link')).some( + (link) => link.href === href + ); + if (exists) return; + + // Create a new link element + const link = document.createElement('link'); + link.rel = 'stylesheet'; + link.href = href; + + // Add any additional attributes (e.g., data-* attributes) + for (const [key, value] of Object.entries(attributes)) { + link.setAttribute(key, value); + } + + // Append the link element to the document head + document.head.appendChild(link); + } + + appendLinkOnce('https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css'); + + if (theme.fontFamily.includes('Press Start 2P')) { + appendLinkOnce('https://fonts.googleapis.com/css2?family=Press+Start+2P&display=swap'); + } + + // Load auto-image-styles.css - prioritize extension local resources + console.group('%c🎨 Loading Auto-Image Styles', 'color: #8b5cf6; font-weight: bold;'); + + let stylesLoaded = false; + + // First try: Check if extension has loaded local CSS resources + if (window.AUTOBOT_THEMES && window.AUTOBOT_THEMES['auto-image-styles.css']) { + console.log('%c🔍 Found auto-image-styles.css in extension local resources!', 'color: #10b981; font-weight: bold;'); + + // Check if it's already injected by the extension + const existingStyle = document.getElementById('autobot-auto-image-styles'); + if (existingStyle) { + console.log('%c✅ Styles already injected by extension - using local version', 'color: #10b981; font-weight: bold;'); + console.log(' 📍 Source: Extension local file (already in DOM)'); + console.log(' 🆔 Element ID: autobot-auto-image-styles'); + stylesLoaded = true; + } else { + // Inject it ourselves + const styleElement = document.createElement('style'); + styleElement.id = 'autobot-auto-image-styles-script'; + styleElement.setAttribute('data-wplace-theme', 'true'); + styleElement.textContent = window.AUTOBOT_THEMES['auto-image-styles.css']; + document.head.appendChild(styleElement); + + console.log('%c✅ Injected auto-image-styles.css from extension local resources', 'color: #10b981; font-weight: bold;'); + console.log(' 📍 Source: Extension local file'); + console.log(' 📏 Size: ' + window.AUTOBOT_THEMES['auto-image-styles.css'].length + ' characters'); + console.log(' 🆔 Element ID: autobot-auto-image-styles-script'); + stylesLoaded = true; + } + } else { + console.log('%c📝 auto-image-styles.css not found in extension resources', 'color: #f59e0b;'); + console.log(' 🔍 window.AUTOBOT_THEMES:', typeof window.AUTOBOT_THEMES); + if (window.AUTOBOT_THEMES) { + console.log(' 📋 Available themes:', Object.keys(window.AUTOBOT_THEMES)); + } + } + + // Fallback: Load from CDN if local resources not available + if (!stylesLoaded) { + console.log('%c🌐 Falling back to CDN loading...', 'color: #8b5cf6;'); + appendLinkOnce( + 'https://wplace-autobot.github.io/WPlace-AutoBOT/main/auto-image-styles.css', + { 'data-wplace-theme': 'true' } + ); + console.log('%c📚 Loaded auto-image-styles.css from CDN (fallback)', 'color: #f59e0b; font-weight: bold;'); + console.log(' 📍 Source: CDN (wplace-autobot.github.io)'); + console.log(' 🌐 URL: https://wplace-autobot.github.io/WPlace-AutoBOT/main/auto-image-styles.css'); + console.log(' ⚠️ Performance: Network request required'); + } + + console.groupEnd(); + + // Ensure default theme is loaded from extension resources + if (window.applyTheme && typeof window.applyTheme === 'function') { + console.group('%c🎨 Loading Default Theme from Extension', 'color: #8b5cf6; font-weight: bold;'); + + // Determine the current theme file name + let defaultTheme = 'classic'; // fallback + if (CONFIG.currentTheme === 'Neon Retro') { + defaultTheme = 'neon'; + } else if (CONFIG.currentTheme === 'Neon Retro Cyan') { + defaultTheme = 'neon-cyan'; + } else if (CONFIG.currentTheme === 'Neon Retro Light') { + defaultTheme = 'neon-light'; + } else if (CONFIG.currentTheme === 'Classic Light') { + defaultTheme = 'classic-light'; + } else if (CONFIG.currentTheme === 'Acrylic') { + defaultTheme = 'acrylic'; + } + + console.log(`%c🎯 Loading theme: ${defaultTheme} (${CONFIG.currentTheme})`, 'color: #8b5cf6;'); + const success = window.applyTheme(defaultTheme); + + if (success) { + console.log(`%c✅ Default theme loaded from extension local resources`, 'color: #10b981; font-weight: bold;'); + console.log(` 📍 Source: Extension local file (themes/${defaultTheme}.css)`); + console.log(` 🚀 Performance: Instant load (no CDN request)`); + console.log(` 🎨 Theme: ${CONFIG.currentTheme}`); + } else { + console.warn(`%c⚠️ Failed to load theme ${defaultTheme} from extension`, 'color: #f59e0b;'); + console.log(` 📝 Note: Theme CSS classes will still work`); + } + + console.groupEnd(); + } else { + console.warn(`%c⚠️ Extension applyTheme() function not available`, 'color: #f59e0b; font-weight: bold;'); + console.log(` 📝 Themes will be limited to CSS classes only`); + } + + const container = document.createElement('div'); + container.id = 'wplace-image-bot-container'; + container.innerHTML = ` +
+
+ + ${Utils.t('title')} +
+
+ + + + +
+
+
+ +
+
+ ${Utils.t('initMessage')} +
+
+
+
+
+ + +
+
🖼️ Image Management
+
+
+ + +
+
+ + +
+
+ +
+
+
+ + +
+
🎮 Painting Control
+
+
+ + +
+
+ + +
+
+
+ + +
+
⏱️ ${Utils.t('cooldownSettings')}
+
+ +
+
+ +
+
+ + + + ${Utils.t('charges')} + +
+
+
+
+ + +
+
💾 Data Management
+
+
+ + +
+
+ + +
+
+
+
+ `; + + const statsContainer = document.createElement('div'); + statsContainer.id = 'wplace-stats-container'; + statsContainer.style.display = 'none'; + statsContainer.innerHTML = ` +
+
+ + ${Utils.t('paintingStats')} +
+
+ + +
+
+
+
+
+
+
${Utils.t('initMessage')}
+
+
+
+ +
+
+
+ + Account Swapper +
+ +
+
+ +
+
+
+ + Auto Buy Charges +
+
+
+ + + +
+
+
+ +
+
+ + All Accounts + +
+
+
Click the icon to load accounts.
+
+
+
+ `; + + function initPillSelector() { + const buttons = statsContainer.querySelectorAll(".pill-btn"); + const highlight = statsContainer.querySelector(".pill-highlight"); + + buttons.forEach((btn, index) => { + btn.addEventListener("click", () => { + highlight.style.transform = `translateX(${index * 100}%)`; + + buttons.forEach((b) => b.classList.remove("active")); + btn.classList.add("active"); + + CONFIG.autoBuy = btn.dataset.mode; + if (CONFIG.autoBuy === 'none') { + console.log("AutoBuy disabled"); + CONFIG.autoBuyToggle = false; + } + else { + CONFIG.autoBuyToggle = true; + console.log("AutoBuy enabled"); + } + console.log("AutoBuy mode set to:", CONFIG.autoBuy); + }); + }); + } + + initPillSelector(); + + // Modern Settings Container with Theme Support + // Use the theme variable already declared at the top of createUI function + const settingsContainer = document.createElement('div'); + settingsContainer.id = 'wplace-settings-container'; + + // Apply theme-based styling + const themeBackground = theme.primary + ? `linear-gradient(135deg, ${theme.primary} 0%, ${theme.secondary || theme.primary} 100%)` + : `linear-gradient(135deg, #667eea 0%, #764ba2 100%)`; + + settingsContainer.className = 'wplace-settings-container-base'; + // Apply theme-specific background + settingsContainer.style.background = themeBackground; + settingsContainer.style.cssText += ` + min-width: 420px; + max-width: 480px; + z-index: 99999; + color: ${theme.text || 'white'}; + font-family: ${theme.fontFamily || "'Segoe UI', Tahoma, Geneva, Verdana, sans-serif"}; + box-shadow: ${theme.boxShadow || '0 20px 40px rgba(0,0,0,0.3), 0 0 0 1px rgba(255,255,255,0.1)' + }; + backdrop-filter: ${theme.backdropFilter || 'blur(10px)'}; + overflow: hidden; + animation: settings-slide-in 0.4s ease-out; + ${theme.animations?.glow + ? ` + box-shadow: ${theme.boxShadow || '0 20px 40px rgba(0,0,0,0.3)'}, + 0 0 30px ${theme.highlight || theme.neon || '#00ffff'}; + ` + : '' + } + `; + + // noinspection CssInvalidFunction + settingsContainer.innerHTML = ` +
+
+

+ + ${Utils.t('settings')} +

+ +
+
+ +
+ + +
+ +
+ +

+ Generator mode creates tokens automatically. Hybrid mode falls back to manual when generator fails. Manual mode only uses pixel placement. +

+
+
+ + +
+ + +
+ + +
+ +
+ +
+
+ Overlay Opacity +
${Math.round(state.overlayOpacity * 100)}%
+
+ +
+ + +
+
+ + +
+ + +
+ + + + + + +
+
+ + +
+ + + +
+ + +
+ + +
+ + +
+ + +
+
+ ${Utils.t('batchSize')} +
+
+
+ +
+
+
+ + + + pixels +
+
+
+
+ ${CONFIG.PAINTING_SPEED.MIN} + ${CONFIG.PAINTING_SPEED.MAX} +
+
+ + +
+
+
+ + +
+
+ + +
+
+

+ 🎲 Random batch size between min and max values +

+
+ + + +
+ + +
+ + + +
+ + +
+ + +
+ + +
+ + +
+ +
+ + +
+
+
+ + +
+
+ + +
+
+

+ 🧱 Block dimensions for block-based generation modes +

+
+
+ + +
+ +
+ + + +
+ ${Utils.t('repeatEvery')} + + ${Utils.t('minutesPl')} +
+
+ + +
+
+
+ + +
+ +
+ +
+
+ + +
+ +
+ +
+
+
+ + + + + `; + + const resizeContainer = document.createElement('div'); + resizeContainer.className = 'resize-container'; + resizeContainer.innerHTML = ` +

${Utils.t('resizeImage')}

+
+ + + + +
+ +
+ + + + +
+ + + + + + + 100% +
+ Drag to pan • Pinch to zoom • Double‑tap to zoom +
+
+
+ +
+
+
+ + +
+
+
+
+
+
+
+ +
+ + 1 +
+
+
+ +
+ + 1 +
+
+
+
+ +
+ + + +
+
+ + + Shift = Row • Alt = Column +
+
+ +
+
+  Color Palette +
+
+
+ +
+
+ + + +
+
+
+
+ +
+
+  Advanced Color Matching +
+
+ + +
+
+ ${Utils.t('chromaWeight')} + ${state.chromaPenaltyWeight} +
+ +
+ +
+ + +
+ +
+
+ +
+ + + +
+ `; + + const resizeOverlay = document.createElement('div'); + resizeOverlay.className = 'resize-overlay'; + + document.body.appendChild(container); + document.body.appendChild(resizeOverlay); + document.body.appendChild(resizeContainer); + document.body.appendChild(statsContainer); + document.body.appendChild(settingsContainer); + + // Show the main container after all elements are appended + container.style.display = 'block'; + + const uploadBtn = container.querySelector('#uploadBtn'); + const resizeBtn = container.querySelector('#resizeBtn'); + const selectPosBtn = container.querySelector('#selectPosBtn'); + const startBtn = container.querySelector('#startBtn'); + const stopBtn = container.querySelector('#stopBtn'); + const saveBtn = container.querySelector('#saveBtn'); + const loadBtn = container.querySelector('#loadBtn'); + const saveToFileBtn = container.querySelector('#saveToFileBtn'); + const loadFromFileBtn = container.querySelector('#loadFromFileBtn'); + + container.querySelectorAll('.wplace-section-title').forEach((title) => { + // Add a right-side arrow if it doesn't exist + if (!title.querySelector('i.arrow')) { + const arrow = document.createElement('i'); + arrow.className = 'fas fa-chevron-down arrow'; // FontAwesome down arrow + title.appendChild(arrow); + } + + // Click event to toggle collapse/expand of the section + title.addEventListener('click', () => { + const section = title.parentElement; + section.classList.toggle('collapsed'); + }); + }); + + // Disable load/upload buttons until initial setup is complete (startup only) + if (loadBtn) { + loadBtn.disabled = !state.initialSetupComplete; + loadBtn.title = state.initialSetupComplete + ? '' + : '🔄 Waiting for initial setup to complete...'; + } + if (loadFromFileBtn) { + loadFromFileBtn.disabled = !state.initialSetupComplete; + loadFromFileBtn.title = state.initialSetupComplete + ? '' + : '🔄 Waiting for initial setup to complete...'; + } + if (uploadBtn) { + uploadBtn.disabled = !state.initialSetupComplete; + uploadBtn.title = state.initialSetupComplete + ? '' + : '🔄 Waiting for initial setup to complete...'; + } + + const minimizeBtn = container.querySelector('#minimizeBtn'); + const compactBtn = container.querySelector('#compactBtn'); + const statsBtn = container.querySelector('#statsBtn'); + const toggleOverlayBtn = container.querySelector('#toggleOverlayBtn'); + const statusText = container.querySelector('#statusText'); + const progressBar = container.querySelector('#progressBar'); + const statsArea = statsContainer.querySelector('#statsArea'); + const content = container.querySelector('.wplace-content'); + const closeStatsBtn = statsContainer.querySelector('#closeStatsBtn'); + const refreshChargesBtn = statsContainer.querySelector('#refreshChargesBtn'); + const cooldownSlider = container.querySelector('#cooldownSlider'); + const cooldownInput = container.querySelector('#cooldownInput'); + const cooldownDecrease = container.querySelector('#cooldownDecrease'); + const cooldownIncrease = container.querySelector('#cooldownIncrease'); + const cooldownValue = container.querySelector('#cooldownValue'); + + if (!uploadBtn || !selectPosBtn || !startBtn || !stopBtn) { + console.error('Some UI elements not found:', { + uploadBtn: !!uploadBtn, + selectPosBtn: !!selectPosBtn, + startBtn: !!startBtn, + stopBtn: !!stopBtn, + }); + } + + if (!statsContainer || !statsArea || !closeStatsBtn) { + // Note: base CSS now aligns with this layout: main panel at left:20px (width 280), stats at left:330px. + } + + const header = container.querySelector('.wplace-header'); + + makeDraggable(container); + + function makeDraggable(element) { + let pos1 = 0, + pos2 = 0, + pos3 = 0, + pos4 = 0; + let isDragging = false; + const header = + element.querySelector('.wplace-header') || element.querySelector('.wplace-settings-header'); + + if (!header) { + console.warn('No draggable header found for element:', element); + return; + } + + header.onmousedown = dragMouseDown; + + function dragMouseDown(e) { + if (e.target.closest('.wplace-header-btn') || e.target.closest('button')) return; + + e.preventDefault(); + isDragging = true; + + const rect = element.getBoundingClientRect(); + + element.style.transform = 'none'; + element.style.top = rect.top + 'px'; + element.style.left = rect.left + 'px'; + + pos3 = e.clientX; + pos4 = e.clientY; + element.classList.add('wplace-dragging'); + document.onmouseup = closeDragElement; + document.onmousemove = elementDrag; + + document.body.style.userSelect = 'none'; + } + + function elementDrag(e) { + if (!isDragging) return; + + e.preventDefault(); + pos1 = pos3 - e.clientX; + pos2 = pos4 - e.clientY; + pos3 = e.clientX; + pos4 = e.clientY; + + let newTop = element.offsetTop - pos2; + let newLeft = element.offsetLeft - pos1; + + const rect = element.getBoundingClientRect(); + const maxTop = window.innerHeight - rect.height; + const maxLeft = window.innerWidth - rect.width; + + newTop = Math.max(0, Math.min(newTop, maxTop)); + newLeft = Math.max(0, Math.min(newLeft, maxLeft)); + + element.style.top = newTop + 'px'; + element.style.left = newLeft + 'px'; + } + + function closeDragElement() { + isDragging = false; + element.classList.remove('wplace-dragging'); + document.onmouseup = null; + document.onmousemove = null; + document.body.style.userSelect = ''; + } + } + + makeDraggable(statsContainer); + makeDraggable(container); + + if (statsBtn && closeStatsBtn) { + statsBtn.addEventListener('click', () => { + const isVisible = statsContainer.style.display !== 'none'; + if (isVisible) { + statsContainer.style.display = 'none'; + statsBtn.innerHTML = ''; + statsBtn.title = Utils.t('showStats'); + } else { + statsContainer.style.display = 'block'; + statsBtn.innerHTML = ''; + statsBtn.title = Utils.t('hideStats'); + } + }); + + closeStatsBtn.addEventListener('click', () => { + statsContainer.style.display = 'none'; + statsBtn.innerHTML = ''; + statsBtn.title = Utils.t('showStats'); + }); + + if (refreshChargesBtn) { + refreshChargesBtn.addEventListener('click', async () => { + refreshChargesBtn.innerHTML = ''; + refreshChargesBtn.disabled = true; + + try { + await updateStats(); + await updateCurrentAccountInList(); + } catch (error) { + console.error('Error refreshing charges:', error); + } finally { + refreshChargesBtn.innerHTML = ''; + refreshChargesBtn.disabled = false; + } + }); + } + + // Account-related event handlers + const autoSwapToggle = statsContainer.querySelector('#autoSwapToggle'); + const refreshAllAccountsBtn = statsContainer.querySelector('#refreshAllAccountsBtn'); + + if (autoSwapToggle) { + autoSwapToggle.checked = CONFIG.autoSwap; + autoSwapToggle.addEventListener('change', (e) => { + CONFIG.autoSwap = e.target.checked; + console.log(`🔄 Auto-swap ${CONFIG.autoSwap ? 'enabled' : 'disabled'}`); + + // Handle autoBuy toggle dependency + const autoBuyToggle = statsContainer.querySelector('#autoBuyToggle'); + if (autoBuyToggle) { + if (!CONFIG.autoSwap) { + // If autoSwap is disabled, disable and uncheck autoBuy + autoBuyToggle.disabled = true; + autoBuyToggle.checked = false; + CONFIG.autoBuyToggle = false; + console.log(`💰 Auto-buy disabled (requires auto-swap)`); + } else { + // If autoSwap is enabled, enable autoBuy toggle + autoBuyToggle.disabled = false; + } + } + }); + } + + // Auto Buy toggle logic + const autoBuyToggle = statsContainer.querySelector('#autoBuyToggle'); + if (autoBuyToggle) { + autoBuyToggle.checked = CONFIG.autoBuyToggle; + autoBuyToggle.disabled = !CONFIG.autoSwap; // Disable if autoSwap is off + + autoBuyToggle.addEventListener('change', (e) => { + CONFIG.autoBuyToggle = e.target.checked; + console.log(`💰 Auto-buy ${CONFIG.autoBuyToggle ? 'enabled' : 'disabled'}`); + }); + } + + if (refreshAllAccountsBtn) { + refreshAllAccountsBtn.addEventListener('click', fetchAllAccountDetails); + } + } + if (statsContainer && statsBtn) { + // Stats container starts visible by default - user clicks button to hide + statsContainer.style.display = 'block'; + statsBtn.innerHTML = ''; + statsBtn.title = Utils.t('hideStats'); + } + + const settingsBtn = container.querySelector('#settingsBtn'); + const closeSettingsBtn = settingsContainer.querySelector('#closeSettingsBtn'); + const applySettingsBtn = settingsContainer.querySelector('#applySettingsBtn'); + + if (settingsBtn && closeSettingsBtn && applySettingsBtn) { + settingsBtn.addEventListener('click', () => { + const isVisible = settingsContainer.classList.contains('show'); + if (isVisible) { + settingsContainer.style.animation = 'settings-fade-out 0.3s ease-out forwards'; + settingsContainer.classList.remove('show'); + setTimeout(() => { + settingsContainer.style.animation = ''; + }, 300); + } else { + settingsContainer.style.top = '50%'; + settingsContainer.style.left = '50%'; + settingsContainer.style.transform = 'translate(-50%, -50%)'; + settingsContainer.classList.add('show'); + settingsContainer.style.animation = 'settings-slide-in 0.4s ease-out'; + } + }); + + closeSettingsBtn.addEventListener('click', () => { + settingsContainer.style.animation = 'settings-fade-out 0.3s ease-out forwards'; + settingsContainer.classList.remove('show'); + setTimeout(() => { + settingsContainer.style.animation = ''; + settingsContainer.style.top = '50%'; + settingsContainer.style.left = '50%'; + settingsContainer.style.transform = 'translate(-50%, -50%)'; + }, 300); + }); + + applySettingsBtn.addEventListener('click', () => { + // Sync advanced settings before save + const colorAlgorithmSelect = document.getElementById('colorAlgorithmSelect'); + if (colorAlgorithmSelect) state.colorMatchingAlgorithm = colorAlgorithmSelect.value; + const enableChromaPenaltyToggle = document.getElementById('enableChromaPenaltyToggle'); + if (enableChromaPenaltyToggle) + state.enableChromaPenalty = enableChromaPenaltyToggle.checked; + const chromaPenaltyWeightSlider = document.getElementById('chromaPenaltyWeightSlider'); + if (chromaPenaltyWeightSlider) + state.chromaPenaltyWeight = parseFloat(chromaPenaltyWeightSlider.value) || 0.15; + const transparencyThresholdInput = document.getElementById('transparencyThresholdInput'); + if (transparencyThresholdInput) { + const v = parseInt(transparencyThresholdInput.value, 10); + if (!isNaN(v) && v >= 0 && v <= 255) state.customTransparencyThreshold = v; + } + const whiteThresholdInput = document.getElementById('whiteThresholdInput'); + if (whiteThresholdInput) { + const v = parseInt(whiteThresholdInput.value, 10); + if (!isNaN(v) && v >= 200 && v <= 255) state.customWhiteThreshold = v; + } + // Update functional thresholds + CONFIG.TRANSPARENCY_THRESHOLD = state.customTransparencyThreshold; + CONFIG.WHITE_THRESHOLD = state.customWhiteThreshold; + // Notifications + const notifEnabledToggle = document.getElementById('notifEnabledToggle'); + const notifOnChargesToggle = document.getElementById('notifOnChargesToggle'); + const notifOnlyUnfocusedToggle = document.getElementById('notifOnlyUnfocusedToggle'); + const notifIntervalInput = document.getElementById('notifIntervalInput'); + if (notifEnabledToggle) state.notificationsEnabled = !!notifEnabledToggle.checked; + if (notifOnChargesToggle) state.notifyOnChargesReached = !!notifOnChargesToggle.checked; + if (notifOnlyUnfocusedToggle) + state.notifyOnlyWhenUnfocused = !!notifOnlyUnfocusedToggle.checked; + if (notifIntervalInput) { + const v = parseInt(notifIntervalInput.value, 10); + if (!isNaN(v) && v >= 1 && v <= 60) state.notificationIntervalMinutes = v; + } + saveBotSettings(); + Utils.showAlert(Utils.t('settingsSaved'), 'success'); + closeSettingsBtn.click(); + NotificationManager.syncFromState(); + }); + + makeDraggable(settingsContainer); + + const tokenSourceSelect = settingsContainer.querySelector('#tokenSourceSelect'); + if (tokenSourceSelect) { + tokenSourceSelect.addEventListener('change', (e) => { + state.tokenSource = e.target.value; + saveBotSettings(); + console.log(`🔑 Token source changed to: ${state.tokenSource}`); + const sourceNames = { + generator: 'Automatic Generator', + hybrid: 'Generator + Auto Fallback', + manual: 'Manual Pixel Placement', + }; + Utils.showAlert( + Utils.t('tokenSourceSet', { source: sourceNames[state.tokenSource] }), + 'success' + ); + }); + } + + // Painting order controls + const paintingOrderSelect = settingsContainer.querySelector('#paintingOrderSelect'); + if (paintingOrderSelect) { + paintingOrderSelect.addEventListener('change', (e) => { + state.paintingOrder = e.target.value; + saveBotSettings(); + console.log(`🎨 Painting order changed to: ${state.paintingOrder}`); + const orderNames = { + sequential: Utils.t('paintingOrderSequential'), + 'color-by-color': Utils.t('paintingOrderColorByColor'), + }; + Utils.showAlert( + `Painting order set to: ${orderNames[state.paintingOrder]}`, + 'success' + ); + }); + } + + // Batch mode controls + const batchModeSelect = settingsContainer.querySelector('#batchModeSelect'); + const normalBatchControls = settingsContainer.querySelector('#normalBatchControls'); + const randomBatchControls = settingsContainer.querySelector('#randomBatchControls'); + const randomBatchMin = settingsContainer.querySelector('#randomBatchMin'); + const randomBatchMax = settingsContainer.querySelector('#randomBatchMax'); + + if (batchModeSelect) { + batchModeSelect.addEventListener('change', (e) => { + state.batchMode = e.target.value; + + // Switch between normal and random controls + if (normalBatchControls && randomBatchControls) { + if (e.target.value === 'random') { + normalBatchControls.style.display = 'none'; + randomBatchControls.style.display = 'block'; + } else { + normalBatchControls.style.display = 'block'; + randomBatchControls.style.display = 'none'; + } + } + + saveBotSettings(); + console.log(`📦 Batch mode changed to: ${state.batchMode}`); + Utils.showAlert( + Utils.t('batchModeSet', { + mode: + state.batchMode === 'random' ? Utils.t('randomRange') : Utils.t('normalFixedSize'), + }), + 'success' + ); + }); + } + + if (randomBatchMin) { + randomBatchMin.addEventListener('input', (e) => { + const min = parseInt(e.target.value); + if (min >= 1 && min <= 1000) { + state.randomBatchMin = min; + // Ensure min doesn't exceed max + if (randomBatchMax && min > state.randomBatchMax) { + state.randomBatchMax = min; + randomBatchMax.value = min; + } + saveBotSettings(); + } + }); + } + + if (randomBatchMax) { + randomBatchMax.addEventListener('input', (e) => { + const max = parseInt(e.target.value); + if (max >= 1 && max <= 1000) { + state.randomBatchMax = max; + // Ensure max doesn't go below min + if (randomBatchMin && max < state.randomBatchMin) { + state.randomBatchMin = max; + randomBatchMin.value = max; + } + saveBotSettings(); + } + }); + } + + const languageSelect = settingsContainer.querySelector('#languageSelect'); + if (languageSelect) { + languageSelect.addEventListener('change', async (e) => { + const newLanguage = e.target.value; + state.language = newLanguage; + localStorage.setItem('wplace_language', newLanguage); + + // Load the new language translations + await loadTranslations(newLanguage); + + setTimeout(() => { + settingsContainer.style.display = 'none'; + createUI(); + }, 100); + }); + } + + const themeSelect = settingsContainer.querySelector('#themeSelect'); + if (themeSelect) { + themeSelect.addEventListener('change', (e) => { + const newTheme = e.target.value; + switchTheme(newTheme); + }); + } + + const overlayOpacitySlider = settingsContainer.querySelector('#overlayOpacitySlider'); + const overlayOpacityValue = settingsContainer.querySelector('#overlayOpacityValue'); + const enableBlueMarbleToggle = settingsContainer.querySelector('#enableBlueMarbleToggle'); + const settingsPaintWhiteToggle = settingsContainer.querySelector('#settingsPaintWhiteToggle'); + const settingsPaintTransparentToggle = settingsContainer.querySelector( + '#settingsPaintTransparentToggle' + ); + + if (overlayOpacitySlider && overlayOpacityValue) { + const updateOpacity = (newValue) => { + const opacity = parseFloat(newValue); + state.overlayOpacity = opacity; + overlayOpacitySlider.value = opacity; + overlayOpacityValue.textContent = `${Math.round(opacity * 100)}%`; + }; + + overlayOpacitySlider.addEventListener('input', (e) => { + updateOpacity(e.target.value); + }); + + // Add scroll-to-adjust for overlay opacity slider + Utils.createScrollToAdjust(overlayOpacitySlider, updateOpacity, 0, 1, 0.05); + } + + if (settingsPaintWhiteToggle) { + settingsPaintWhiteToggle.checked = state.paintWhitePixels; + settingsPaintWhiteToggle.addEventListener('change', (e) => { + state.paintWhitePixels = e.target.checked; + saveBotSettings(); + console.log(`🎨 Paint white pixels: ${state.paintWhitePixels ? 'ON' : 'OFF'}`); + const statusText = state.paintWhitePixels + ? 'White pixels in the template will be painted' + : 'White pixels will be skipped'; + Utils.showAlert(statusText, 'success'); + }); + } + + if (settingsPaintTransparentToggle) { + settingsPaintTransparentToggle.checked = state.paintTransparentPixels; + settingsPaintTransparentToggle.addEventListener('change', (e) => { + state.paintTransparentPixels = e.target.checked; + saveBotSettings(); + console.log( + `🎨 Paint transparent pixels: ${state.paintTransparentPixels ? 'ON' : 'OFF'}` + ); + const statusText = state.paintTransparentPixels + ? 'Transparent pixels in the template will be painted with the closest available color' + : 'Transparent pixels will be skipped'; + Utils.showAlert(statusText, 'success'); + }); + } + + // Speed controls - both slider and input + const speedSlider = settingsContainer.querySelector('#speedSlider'); + const speedInput = settingsContainer.querySelector('#speedInput'); + const speedDecrease = settingsContainer.querySelector('#speedDecrease'); + const speedIncrease = settingsContainer.querySelector('#speedIncrease'); + const speedValue = settingsContainer.querySelector('#speedValue'); + + if (speedSlider && speedInput && speedValue && speedDecrease && speedIncrease) { + const updateSpeed = (newValue) => { + const speed = Math.max(CONFIG.PAINTING_SPEED.MIN, Math.min(CONFIG.PAINTING_SPEED.MAX, parseInt(newValue))); + state.paintingSpeed = speed; + + // Update both controls (value shows in input, label shows unit only) + speedSlider.value = speed; + speedInput.value = speed; + speedValue.textContent = `pixels`; + + saveBotSettings(); + }; + + // Slider event listener + speedSlider.addEventListener('input', (e) => { + updateSpeed(e.target.value); + }); + + // Number input event listener + speedInput.addEventListener('input', (e) => { + updateSpeed(e.target.value); + }); + + // Decrease button + speedDecrease.addEventListener('click', () => { + updateSpeed(parseInt(speedInput.value) - 1); + }); + + // Increase button + speedIncrease.addEventListener('click', () => { + updateSpeed(parseInt(speedInput.value) + 1); + }); + + // Add scroll-to-adjust for speed slider + Utils.createScrollToAdjust(speedSlider, updateSpeed, CONFIG.PAINTING_SPEED.MIN, CONFIG.PAINTING_SPEED.MAX, 1); + } + + if (enableBlueMarbleToggle) { + enableBlueMarbleToggle.addEventListener('click', async () => { + state.blueMarbleEnabled = enableBlueMarbleToggle.checked; + if (state.imageLoaded && overlayManager.imageBitmap) { + Utils.showAlert(Utils.t('reprocessingOverlay'), 'info'); + await overlayManager.processImageIntoChunks(); + Utils.showAlert(Utils.t('overlayUpdated'), 'success'); + } + }); + } + + // Speed control toggle + const enableSpeedToggle = settingsContainer.querySelector('#enableSpeedToggle'); + if (enableSpeedToggle) { + enableSpeedToggle.addEventListener('change', (e) => { + CONFIG.PAINTING_SPEED_ENABLED = e.target.checked; + saveBotSettings(); + console.log(`⚡ Batch speed control: ${CONFIG.PAINTING_SPEED_ENABLED ? 'ON' : 'OFF'}`); + const statusText = CONFIG.PAINTING_SPEED_ENABLED + ? 'Batch speed control enabled - will use configured batch size' + : 'Batch speed control disabled - will use all available charges immediately'; + Utils.showAlert(statusText, 'success'); + }); + } + + // (Advanced color listeners moved outside to work with resize dialog) + // (Advanced color listeners moved outside to work with resize dialog) + // Notifications listeners + const notifPermBtn = settingsContainer.querySelector('#notifRequestPermBtn'); + const notifTestBtn = settingsContainer.querySelector('#notifTestBtn'); + if (notifPermBtn) { + notifPermBtn.addEventListener('click', async () => { + const perm = await NotificationManager.requestPermission(); + if (perm === 'granted') Utils.showAlert(Utils.t('notificationsEnabled'), 'success'); + else Utils.showAlert(Utils.t('notificationsPermissionDenied'), 'warning'); + }); + } + if (notifTestBtn) { + notifTestBtn.addEventListener('click', () => { + NotificationManager.notify( + Utils.t('testNotificationTitle'), + Utils.t('testNotificationMessage'), + 'wplace-notify-test', + true + ); + }); + } + } + + const widthSlider = resizeContainer.querySelector('#widthSlider'); + const heightSlider = resizeContainer.querySelector('#heightSlider'); + const widthValue = resizeContainer.querySelector('#widthValue'); + const heightValue = resizeContainer.querySelector('#heightValue'); + const keepAspect = resizeContainer.querySelector('#keepAspect'); + const paintWhiteToggle = resizeContainer.querySelector('#paintWhiteToggle'); + const paintTransparentToggle = resizeContainer.querySelector('#paintTransparentToggle'); + const zoomSlider = resizeContainer.querySelector('#zoomSlider'); + const zoomValue = resizeContainer.querySelector('#zoomValue'); + const zoomInBtn = resizeContainer.querySelector('#zoomInBtn'); + const zoomOutBtn = resizeContainer.querySelector('#zoomOutBtn'); + const zoomFitBtn = resizeContainer.querySelector('#zoomFitBtn'); + const zoomActualBtn = resizeContainer.querySelector('#zoomActualBtn'); + const panModeBtn = resizeContainer.querySelector('#panModeBtn'); + const panStage = resizeContainer.querySelector('#resizePanStage'); + const canvasStack = resizeContainer.querySelector('#resizeCanvasStack'); + const baseCanvas = resizeContainer.querySelector('#resizeCanvas'); + const maskCanvas = resizeContainer.querySelector('#maskCanvas'); + const baseCtx = baseCanvas.getContext('2d'); + const maskCtx = maskCanvas.getContext('2d'); + const confirmResize = resizeContainer.querySelector('#confirmResize'); + const cancelResize = resizeContainer.querySelector('#cancelResize'); + const editImageBtn = resizeContainer.querySelector('#editImageBtn'); + const downloadPreviewBtn = resizeContainer.querySelector('#downloadPreviewBtn'); + const clearIgnoredBtn = resizeContainer.querySelector('#clearIgnoredBtn'); + + // Coordinate generation controls with smart visibility + const coordinateModeSelect = settingsContainer.querySelector('#coordinateModeSelect'); + const coordinateDirectionSelect = settingsContainer.querySelector('#coordinateDirectionSelect'); + const coordinateSnakeToggle = settingsContainer.querySelector('#coordinateSnakeToggle'); + const directionControls = settingsContainer.querySelector('#directionControls'); + const snakeControls = settingsContainer.querySelector('#snakeControls'); + const blockControls = settingsContainer.querySelector('#blockControls'); + const blockWidthInput = settingsContainer.querySelector('#blockWidthInput'); + const blockHeightInput = settingsContainer.querySelector('#blockHeightInput'); + const paintUnavailablePixelsToggle = settingsContainer.querySelector( + '#paintUnavailablePixelsToggle' + ); + + if (paintUnavailablePixelsToggle) { + paintUnavailablePixelsToggle.checked = state.paintUnavailablePixels; + paintUnavailablePixelsToggle.addEventListener('change', (e) => { + state.paintUnavailablePixels = e.target.checked; + saveBotSettings(); + console.log(`🎨 Paint unavailable colors: ${state.paintUnavailablePixels ? 'ON' : 'OFF'}`); + const statusText = state.paintUnavailablePixels + ? 'Unavailable template colors will be painted with the closest available color' + : 'Unavailable template colors will be skipped'; + Utils.showAlert(statusText, 'success'); + }); + } + if (coordinateModeSelect) { + coordinateModeSelect.value = state.coordinateMode; + coordinateModeSelect.addEventListener('change', (e) => { + state.coordinateMode = e.target.value; + Utils.updateCoordinateUI({ + mode: state.coordinateMode, + directionControls, + snakeControls, + blockControls, + }); + saveBotSettings(); + console.log(`🔄 Coordinate mode changed to: ${state.coordinateMode}`); + Utils.showAlert(`Coordinate mode set to: ${state.coordinateMode}`, 'success'); + }); + } + + if (coordinateDirectionSelect) { + coordinateDirectionSelect.value = state.coordinateDirection; + coordinateDirectionSelect.addEventListener('change', (e) => { + state.coordinateDirection = e.target.value; + saveBotSettings(); + console.log(`🧭 Coordinate direction changed to: ${state.coordinateDirection}`); + Utils.showAlert(`Coordinate direction set to: ${state.coordinateDirection}`, 'success'); + }); + } + + if (coordinateSnakeToggle) { + coordinateSnakeToggle.checked = state.coordinateSnake; + coordinateSnakeToggle.addEventListener('change', (e) => { + state.coordinateSnake = e.target.checked; + saveBotSettings(); + console.log(`🐍 Snake pattern ${state.coordinateSnake ? 'enabled' : 'disabled'}`); + Utils.showAlert( + `Snake pattern ${state.coordinateSnake ? 'enabled' : 'disabled'}`, + 'success' + ); + }); + } + + if (blockWidthInput) { + blockWidthInput.value = state.blockWidth; + blockWidthInput.addEventListener('input', (e) => { + const width = parseInt(e.target.value); + if (width >= 1 && width <= 50) { + state.blockWidth = width; + saveBotSettings(); + } + }); + } + + if (blockHeightInput) { + blockHeightInput.value = state.blockHeight; + blockHeightInput.addEventListener('change', (e) => { + const height = parseInt(e.target.value); + if (height >= 1 && height <= 50) { + state.blockHeight = height; + saveBotSettings(); + } + }); + } + + if (compactBtn) { + compactBtn.addEventListener('click', () => { + container.classList.toggle('wplace-compact'); + const isCompact = container.classList.contains('wplace-compact'); + + if (isCompact) { + compactBtn.innerHTML = ''; + compactBtn.title = Utils.t('expandMode'); + } else { + compactBtn.innerHTML = ''; + compactBtn.title = Utils.t('compactMode'); + } + }); + } + + if (minimizeBtn) { + minimizeBtn.addEventListener('click', () => { + state.minimized = !state.minimized; + if (state.minimized) { + container.classList.add('wplace-minimized'); + content.classList.add('wplace-hidden'); + minimizeBtn.innerHTML = ''; + minimizeBtn.title = Utils.t('restore'); + } else { + container.classList.remove('wplace-minimized'); + content.classList.remove('wplace-hidden'); + minimizeBtn.innerHTML = ''; + minimizeBtn.title = Utils.t('minimize'); + } + saveBotSettings(); + }); + } + + if (toggleOverlayBtn) { + toggleOverlayBtn.addEventListener('click', () => { + const isEnabled = overlayManager.toggle(); + toggleOverlayBtn.classList.toggle('active', isEnabled); + toggleOverlayBtn.setAttribute('aria-pressed', isEnabled ? 'true' : 'false'); + Utils.showAlert(isEnabled ? Utils.t('overlayEnabled') : Utils.t('overlayDisabled'), 'info'); + }); + } + + if (state.minimized) { + container.classList.add('wplace-minimized'); + content.classList.add('wplace-hidden'); + if (minimizeBtn) { + minimizeBtn.innerHTML = ''; + minimizeBtn.title = Utils.t('restore'); + } + } else { + container.classList.remove('wplace-minimized'); + content.classList.remove('wplace-hidden'); + if (minimizeBtn) { + minimizeBtn.innerHTML = ''; + minimizeBtn.title = Utils.t('minimize'); + } + } + + if (saveBtn) { + saveBtn.addEventListener('click', () => { + if (!state.imageLoaded) { + Utils.showAlert(Utils.t('missingRequirements'), 'error'); + return; + } + + const success = Utils.saveProgress(); + if (success) { + updateUI('autoSaved', 'success'); + Utils.showAlert(Utils.t('autoSaved'), 'success'); + } else { + Utils.showAlert(Utils.t('errorSavingProgress'), 'error'); + } + }); + } + + if (loadBtn) { + loadBtn.addEventListener('click', async () => { + // Auto-open color palette if not already open + Utils.openColorPalette(); + + const savedData = Utils.loadProgress(); + if (!savedData) { + updateUI('noSavedData', 'warning'); + Utils.showAlert(Utils.t('noSavedData'), 'warning'); + return; + } + + // 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; + + if (!state.initialSetupComplete && !hasCompleteData) { + Utils.showAlert(Utils.t('pleaseWaitInitialSetup'), 'warning'); + return; + } + + const confirmLoad = confirm( + `${Utils.t('savedDataFound')}\n\n` + + `Saved: ${new Date(savedData.timestamp).toLocaleString()}\n` + + `Progress: ${savedData.state.paintedPixels}/${savedData.state.totalPixels} pixels` + ); + + if (confirmLoad) { + const success = Utils.restoreProgress(savedData); + if (success) { + // CRITICAL FIX: Set initial setup complete if save had complete data + if (hasCompleteData) { + state.initialSetupComplete = true; + } + + updateUI('dataLoaded', 'success'); + Utils.showAlert(Utils.t('dataLoaded'), 'success'); + updateDataButtons(); + + // CRITICAL FIX: Force immediate stats update after save loading + 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(async () => { + await updateStats(); + }, 100); + } + + // Restore overlay if image data was loaded from localStorage + Utils.restoreOverlayFromData().catch((error) => { + console.error('Failed to restore overlay from localStorage:', error); + }); + + 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; + } + + if (state.imageLoaded && state.startPosition && state.region && state.colorsChecked) { + startBtn.disabled = false; + } + } else { + Utils.showAlert(Utils.t('errorLoadingProgress'), 'error'); + } + } + }); + } + + if (saveToFileBtn) { + saveToFileBtn.addEventListener('click', () => { + const success = Utils.saveProgressToFile(); + if (success) { + updateUI('fileSaved', 'success'); + Utils.showAlert(Utils.t('fileSaved'), 'success'); + } else { + Utils.showAlert(Utils.t('fileError'), 'error'); + } + }); + } + + 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; + + 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; + } + + updateUI('fileLoaded', 'success'); + 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('❌ [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 { + console.error('❌ [DEBUG] General file error:', error.message); + Utils.showAlert(`File error: ${error.message}`, 'error'); + } + } + }); + } + + updateUI = (messageKey, type = 'default', params = {}, silent = false) => { + const message = Utils.t(messageKey, params); + statusText.textContent = message; + statusText.className = `wplace-status status-${type}`; + + if (!silent) { + // Trigger animation only when silent = false + statusText.style.animation = 'none'; + void statusText.offsetWidth; // trick to restart the animation + statusText.style.animation = 'slide-in 0.3s ease-out'; + } + }; + + function updateChargeStatsDisplay(intervalMs) { + const currentChargesEl = document.getElementById('wplace-stat-charges-value'); + const fullChargeEl = document.getElementById('wplace-stat-fullcharge-value'); + if (!fullChargeEl && !currentChargesEl) return; + if (!state.fullChargeData) { + fullChargeEl.textContent = '--:--:--'; + return; + } + + const { current, max, cooldownMs, startTime, spentSinceShot } = state.fullChargeData; + const elapsed = Date.now() - startTime; + + // total charges including elapsed time and spent during painting since snapshot + const chargesGained = elapsed / cooldownMs; + const rawCharges = current + chargesGained - spentSinceShot; + const cappedCharges = Math.min(rawCharges, max); + + // rounding with 0.95 threshold + let displayCharges; + const fraction = cappedCharges - Math.floor(cappedCharges); + if (fraction >= 0.95) { + displayCharges = Math.ceil(cappedCharges); + } else { + displayCharges = Math.floor(cappedCharges); + } + + state.displayCharges = Math.max(0, displayCharges); + state.preciseCurrentCharges = cappedCharges; + + const remainingMs = getMsToTargetCharges(cappedCharges, max, state.cooldown, intervalMs); + const timeText = Utils.msToTimeText(remainingMs); + + if (currentChargesEl) { + currentChargesEl.innerHTML = `${state.displayCharges} / ${state.maxCharges}`; + } + + if ( + state.displayCharges < state.cooldownChargeThreshold && + !state.stopFlag && + state.running + ) { + updateChargesThresholdUI(intervalMs); + } + + if (fullChargeEl) { + if (state.displayCharges >= max) { + fullChargeEl.innerHTML = `FULL`; + } else { + fullChargeEl.innerHTML = ` + ${timeText} + `; + } + } + } + + updateStats = async (isManualRefresh = false) => { + const isFirstCheck = !state.fullChargeData?.startTime; + + const minUpdateInterval = 60_000; + const maxUpdateInterval = 90_000; + const randomUpdateThreshold = + minUpdateInterval + Math.random() * (maxUpdateInterval - minUpdateInterval); + const timeSinceLastUpdate = Date.now() - (state.fullChargeData?.startTime || 0); + const isTimeToUpdate = timeSinceLastUpdate >= randomUpdateThreshold; + + const shouldCallApi = isFirstCheck || isTimeToUpdate; + + 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(); + + if (state.fullChargeInterval) { + clearInterval(state.fullChargeInterval); + state.fullChargeInterval = null; + } + const intervalMs = 1000; + state.fullChargeInterval = setInterval( + () => updateChargeStatsDisplay(intervalMs), + intervalMs + ); + + if (cooldownSlider && cooldownSlider.max !== state.maxCharges) { + cooldownSlider.max = state.maxCharges; + } + if (cooldownInput && cooldownInput.max !== state.maxCharges) { + cooldownInput.max = state.maxCharges; + } + + // // 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 { + batchSize = typeof calculateBatchSize === 'function' ? calculateBatchSize() : state.paintingSpeed || 5; + } 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}%`; + + imageStatsHTML = ` +
+
${Utils.t('progress')}
+
${progress}%
+
+
+
${Utils.t( + 'pixels' + )}
+
${state.paintedPixels}/${state.totalPixels}
+
+ `; + } + + let colorSwatchesHTML = ''; + state.availableColors = state.availableColors.filter( + (c) => c.name !== 'Unknown CoIor NaN' && c.id !== null + ); + + const availableColors = Utils.extractAvailableColors(); + const newCount = Array.isArray(availableColors) ? availableColors.length : 0; + + if (newCount === 0 && isManualRefresh) { + Utils.showAlert(Utils.t('noColorsFound'), 'warning'); + } else if (newCount > 0 && state.availableColors.length < newCount) { + const oldCount = state.availableColors.length; + + Utils.showAlert( + Utils.t('colorsUpdated', { + oldCount, + newCount: newCount, + diffCount: newCount - oldCount, + }), + 'success' + ); + state.availableColors = availableColors; + } + if (state.colorsChecked) { + colorSwatchesHTML = state.availableColors + .map((color) => { + const rgbString = `rgb(${color.rgb.join(',')})`; + return `
`; + }) + .join(''); + } + + // Calculate multi-account statistics + let totalAllCharges = 0; + let totalMaxCharges = 0; + const accounts = accountManager.getAllAccounts(); + if (accounts.length > 0) { + // Use real-time charges for current account, stored data for others + const currentAccount = accountManager.getCurrentAccount(); + totalAllCharges = accounts.reduce((sum, acc) => { + if (currentAccount && acc.token === currentAccount.token) { + // Use real-time data for current account + return sum + Math.floor(state.displayCharges || state.preciseCurrentCharges || 0); + } else { + // Use stored data for other accounts + return sum + Math.floor(acc.Charges || 0); + } + }, 0); + totalMaxCharges = accounts.reduce((sum, acc) => { + if (currentAccount && acc.token === currentAccount.token) { + // Use real-time max charges for current account + return sum + Math.floor(state.maxCharges || acc.Max || 0); + } else { + // Use stored data for other accounts + return sum + Math.floor(acc.Max || 0); + } + }, 0); + } + + statsArea.innerHTML = ` + ${imageStatsHTML} + ${accounts.length > 0 ? ` +
+
+ Total All Accounts Charges +
+
+ ${totalAllCharges}/${totalMaxCharges} +
+
+ ` : ''} +
+
+ ${Utils.t('charges')} +
+
+ ${state.displayCharges} / ${state.maxCharges} +
+
+
+
+ ${Utils.t('fullChargeIn')} +
+
--:--:--
+
+ ${state.colorsChecked + ? ` +
+
${Utils.t( + 'availableColors', + { count: state.availableColors.length } + )}
+
+ ${colorSwatchesHTML} +
+
+ ` + : '' + } + `; + + // should be after statsArea.innerHTML = '...'. todo make full stats ui update partial + updateChargeStatsDisplay(intervalMs); + }; + + updateDataButtons = () => { + const hasImageData = state.imageLoaded && state.imageData; + saveBtn.disabled = !hasImageData; + saveToFileBtn.disabled = !hasImageData; + }; + + updateDataButtons(); + + function showMoveArtworkPanel() { + // Create move artwork control panel like status panels + const movePanel = document.createElement('div'); + movePanel.id = 'moveArtworkPanel'; + movePanel.className = 'wplace-move-panel'; + + movePanel.innerHTML = ` +
+
+ + Move Artwork +
+
+ +
+
+
+
+ +
+ +
1px
+ +
+ +
+
+ `; + + // No overlay - append directly to body like other panels + document.body.appendChild(movePanel); + + // Make the panel draggable + makeDraggable(movePanel); + + // Add event listeners for movement + let currentMessageTimeout = null; + + function moveArtwork(deltaX, deltaY) { + if (state.startPosition && state.region) { + const newX = state.startPosition.x + deltaX; + const newY = state.startPosition.y + deltaY; + + console.log(`🔄 Moving artwork from (${state.startPosition.x}, ${state.startPosition.y}) to (${newX}, ${newY})`); + + state.startPosition.x = newX; + state.startPosition.y = newY; + + // Clear any existing message timeout and alerts immediately + if (currentMessageTimeout) { + clearTimeout(currentMessageTimeout); + currentMessageTimeout = null; + } + Utils.hideAlert(); // Hide current alert immediately + + // Update overlay position if available + if (overlayManager) { + overlayManager.setPosition(state.startPosition, state.region) + .then(() => { + console.log('✅ Overlay position updated'); + Utils.showAlert(`Moved to position (${newX}, ${newY})`, 'success'); + // Set new timeout for this message + currentMessageTimeout = setTimeout(() => { + Utils.hideAlert(); + currentMessageTimeout = null; + }, 1500); // Message disappears after 1.5 seconds + }) + .catch(err => { + console.error('❌ Failed to update overlay:', err); + Utils.showAlert('Failed to update overlay position', 'error'); + }); + } else { + Utils.showAlert(`Position updated to (${newX}, ${newY})`, 'success'); + // Set new timeout for this message + currentMessageTimeout = setTimeout(() => { + Utils.hideAlert(); + currentMessageTimeout = null; + }, 1500); + } + } else { + Utils.showAlert('Please select a position first before moving artwork', 'warning'); + } + } + + document.getElementById('moveUp').addEventListener('click', () => moveArtwork(0, -1)); + document.getElementById('moveDown').addEventListener('click', () => moveArtwork(0, 1)); + document.getElementById('moveLeft').addEventListener('click', () => moveArtwork(-1, 0)); + document.getElementById('moveRight').addEventListener('click', () => moveArtwork(1, 0)); + + // Close panel + function closeMovePanel() { + if (currentMessageTimeout) { + clearTimeout(currentMessageTimeout); + currentMessageTimeout = null; + } + Utils.hideAlert(); + document.body.removeChild(movePanel); + } + + document.getElementById('closeMovePanel').addEventListener('click', closeMovePanel); + } + + function showResizeDialog(processor) { + let baseProcessor = processor; + let width, height; + if (state.originalImage?.dataUrl) { + baseProcessor = new ImageProcessor(state.originalImage.dataUrl); + width = state.originalImage.width; + height = state.originalImage.height; + } else { + const dims = processor.getDimensions(); + width = dims.width; + height = dims.height; + } + const aspectRatio = width / height; + + const rs = state.resizeSettings; + widthSlider.max = width * 2; + heightSlider.max = height * 2; + let initialW = width; + let initialH = height; + if ( + rs && + Number.isFinite(rs.width) && + Number.isFinite(rs.height) && + rs.width > 0 && + rs.height > 0 + ) { + initialW = rs.width; + initialH = rs.height; + } + // Clamp to slider ranges + initialW = Math.max( + parseInt(widthSlider.min, 10) || 10, + Math.min(initialW, parseInt(widthSlider.max, 10)) + ); + initialH = Math.max( + parseInt(heightSlider.min, 10) || 10, + Math.min(initialH, parseInt(heightSlider.max, 10)) + ); + widthSlider.value = initialW; + heightSlider.value = initialH; + widthValue.textContent = initialW; + heightValue.textContent = initialH; + zoomSlider.value = 1; + if (zoomValue) zoomValue.textContent = '100%'; + paintWhiteToggle.checked = state.paintWhitePixels; + paintTransparentToggle.checked = state.paintTransparentPixels; + + let _previewTimer = null; + let _previewJobId = 0; + let _isDraggingSize = false; + let _zoomLevel = 1; + let _ditherWorkBuf = null; + let _ditherEligibleBuf = null; + const ensureDitherBuffers = (n) => { + if (!_ditherWorkBuf || _ditherWorkBuf.length !== n * 3) + _ditherWorkBuf = new Float32Array(n * 3); + if (!_ditherEligibleBuf || _ditherEligibleBuf.length !== n) + _ditherEligibleBuf = new Uint8Array(n); + return { work: _ditherWorkBuf, eligible: _ditherEligibleBuf }; + }; + let _maskImageData = null; + let _maskData = null; + let _dirty = null; + const _resetDirty = () => { + _dirty = { minX: Infinity, minY: Infinity, maxX: -1, maxY: -1 }; + }; + const _markDirty = (x, y) => { + if (!_dirty) _resetDirty(); + if (x < _dirty.minX) _dirty.minX = x; + if (y < _dirty.minY) _dirty.minY = y; + if (x > _dirty.maxX) _dirty.maxX = x; + if (y > _dirty.maxY) _dirty.maxY = y; + }; + const _flushDirty = () => { + if (!_dirty || _dirty.maxX < _dirty.minX || _dirty.maxY < _dirty.minY) return; + const x = Math.max(0, _dirty.minX); + const y = Math.max(0, _dirty.minY); + const w = Math.min(maskCanvas.width - x, _dirty.maxX - x + 1); + const h = Math.min(maskCanvas.height - y, _dirty.maxY - y + 1); + if (w > 0 && h > 0) maskCtx.putImageData(_maskImageData, 0, 0, x, y, w, h); + _resetDirty(); + }; + const _ensureMaskOverlayBuffers = (w, h, rebuildFromMask = false) => { + if (!_maskImageData || _maskImageData.width !== w || _maskImageData.height !== h) { + _maskImageData = maskCtx.createImageData(w, h); + _maskData = _maskImageData.data; + rebuildFromMask = true; + } + if (rebuildFromMask) { + const m = state.resizeIgnoreMask; + const md = _maskData; + md.fill(0); + if (m) { + for (let i = 0; i < m.length; i++) + if (m[i]) { + const p = i * 4; + md[p] = 255; + md[p + 1] = 0; + md[p + 2] = 0; + md[p + 3] = 150; + } + } + maskCtx.putImageData(_maskImageData, 0, 0); + _resetDirty(); + } + }; + const ensureMaskSize = (w, h) => { + if (!state.resizeIgnoreMask || state.resizeIgnoreMask.length !== w * h) { + state.resizeIgnoreMask = new Uint8Array(w * h); + } + baseCanvas.width = w; + baseCanvas.height = h; + maskCanvas.width = w; + maskCanvas.height = h; + maskCtx.clearRect(0, 0, maskCanvas.width, maskCanvas.height); + // Ensure overlay buffers exist and rebuild from mask when dimensions change + _ensureMaskOverlayBuffers(w, h, true); + }; + _updateResizePreview = async () => { + const jobId = ++_previewJobId; + const newWidth = parseInt(widthSlider.value, 10); + const newHeight = parseInt(heightSlider.value, 10); + _zoomLevel = parseFloat(zoomSlider.value); + + widthValue.textContent = newWidth; + heightValue.textContent = newHeight; + + ensureMaskSize(newWidth, newHeight); + canvasStack.style.width = newWidth + 'px'; + canvasStack.style.height = newHeight + 'px'; + baseCtx.imageSmoothingEnabled = false; + + if (!state.availableColors || state.availableColors.length === 0) { + if (baseProcessor !== processor && (!baseProcessor.img || !baseProcessor.canvas)) { + await baseProcessor.load(); + } + baseCtx.clearRect(0, 0, newWidth, newHeight); + baseCtx.drawImage(baseProcessor.img, 0, 0, newWidth, newHeight); + // Draw existing mask overlay buffer + maskCtx.clearRect(0, 0, maskCanvas.width, maskCanvas.height); + if (_maskImageData) maskCtx.putImageData(_maskImageData, 0, 0); + updateZoomLayout(); + return; + } + + if (baseProcessor !== processor && (!baseProcessor.img || !baseProcessor.canvas)) { + await baseProcessor.load(); + } + baseCtx.clearRect(0, 0, newWidth, newHeight); + baseCtx.drawImage(baseProcessor.img, 0, 0, newWidth, newHeight); + const imgData = baseCtx.getImageData(0, 0, newWidth, newHeight); + const data = imgData.data; + + const tThresh = state.customTransparencyThreshold || CONFIG.TRANSPARENCY_THRESHOLD; + + const applyFSDither = () => { + const w = newWidth, + h = newHeight; + const n = w * h; + const { work, eligible } = ensureDitherBuffers(n); + for (let y = 0; y < h; y++) { + for (let x = 0; x < w; x++) { + const idx = y * w + x; + const i4 = idx * 4; + const r = data[i4], + g = data[i4 + 1], + b = data[i4 + 2], + a = data[i4 + 3]; + const isEligible = + (state.paintTransparentPixels || a >= tThresh) && + (state.paintWhitePixels || !Utils.isWhitePixel(r, g, b)); + eligible[idx] = isEligible ? 1 : 0; + work[idx * 3] = r; + work[idx * 3 + 1] = g; + work[idx * 3 + 2] = b; + if (!isEligible) { + data[i4 + 3] = 0; // transparent in preview overlay + } + } + } + + const diffuse = (nx, ny, er, eg, eb, factor) => { + if (nx < 0 || nx >= w || ny < 0 || ny >= h) return; + const nidx = ny * w + nx; + if (!eligible[nidx]) return; + const base = nidx * 3; + work[base] = Math.min(255, Math.max(0, work[base] + er * factor)); + work[base + 1] = Math.min(255, Math.max(0, work[base + 1] + eg * factor)); + work[base + 2] = Math.min(255, Math.max(0, work[base + 2] + eb * factor)); + }; + + for (let y = 0; y < h; y++) { + for (let x = 0; x < w; x++) { + const idx = y * w + x; + if (!eligible[idx]) continue; + const base = idx * 3; + const r0 = work[base], + g0 = work[base + 1], + b0 = work[base + 2]; + const [nr, ng, nb] = Utils.findClosestPaletteColor( + r0, + g0, + b0, + state.activeColorPalette + ); + const i4 = idx * 4; + data[i4] = nr; + data[i4 + 1] = ng; + data[i4 + 2] = nb; + data[i4 + 3] = 255; + + const er = r0 - nr; + const eg = g0 - ng; + const eb = b0 - nb; + + diffuse(x + 1, y, er, eg, eb, 7 / 16); + diffuse(x - 1, y + 1, er, eg, eb, 3 / 16); + diffuse(x, y + 1, er, eg, eb, 5 / 16); + diffuse(x + 1, y + 1, er, eg, eb, 1 / 16); + } + } + }; + + // Skip expensive dithering while user is dragging sliders + console.log(`🎨 Preview render - ditheringEnabled: ${state.ditheringEnabled}, _isDraggingSize: ${_isDraggingSize}`); + if (state.ditheringEnabled && !_isDraggingSize) { + console.log('✅ Applying dithering to preview'); + applyFSDither(); + } else { + console.log('⛔ Skipping dithering - applying direct color conversion'); + for (let i = 0; i < data.length; i += 4) { + const r = data[i], + g = data[i + 1], + b = data[i + 2], + a = data[i + 3]; + if ( + (!state.paintTransparentPixels && a < tThresh) || + (!state.paintWhitePixels && Utils.isWhitePixel(r, g, b)) + ) { + data[i + 3] = 0; + continue; + } + const [nr, ng, nb] = Utils.findClosestPaletteColor(r, g, b, state.activeColorPalette); + data[i] = nr; + data[i + 1] = ng; + data[i + 2] = nb; + data[i + 3] = 255; + } + } + + if (jobId !== _previewJobId) return; + baseCtx.putImageData(imgData, 0, 0); + maskCtx.clearRect(0, 0, maskCanvas.width, maskCanvas.height); + if (_maskImageData) maskCtx.putImageData(_maskImageData, 0, 0); + updateZoomLayout(); + }; + + const onWidthInput = () => { + if (keepAspect.checked) { + heightSlider.value = Math.round(parseInt(widthSlider.value, 10) / aspectRatio); + } + _updateResizePreview(); + const curW = parseInt(widthSlider.value, 10); + const curH = parseInt(heightSlider.value, 10); + state.resizeSettings = { + baseWidth: width, + baseHeight: height, + width: curW, + height: curH, + }; + saveBotSettings(); + // Auto-fit after size changes + const fit = typeof computeFitZoom === 'function' ? computeFitZoom() : 1; + if (!isNaN(fit) && isFinite(fit)) applyZoom(fit); + }; + + const onHeightInput = () => { + if (keepAspect.checked) { + widthSlider.value = Math.round(parseInt(heightSlider.value, 10) * aspectRatio); + } + _updateResizePreview(); + const curW = parseInt(widthSlider.value, 10); + const curH = parseInt(heightSlider.value, 10); + state.resizeSettings = { + baseWidth: width, + baseHeight: height, + width: curW, + height: curH, + }; + saveBotSettings(); + // Auto-fit after size changes + const fit = typeof computeFitZoom === 'function' ? computeFitZoom() : 1; + if (!isNaN(fit) && isFinite(fit)) applyZoom(fit); + }; + + paintWhiteToggle.onchange = (e) => { + state.paintWhitePixels = e.target.checked; + _updateResizePreview(); + saveBotSettings(); + }; + + paintTransparentToggle.onchange = (e) => { + state.paintTransparentPixels = e.target.checked; + _updateResizePreview(); + saveBotSettings(); + }; + + let panX = 0, + panY = 0; + const clampPan = () => { + const wrapRect = panStage?.getBoundingClientRect() || { + width: 0, + height: 0, + }; + const w = (baseCanvas.width || 1) * _zoomLevel; + const h = (baseCanvas.height || 1) * _zoomLevel; + if (w <= wrapRect.width) { + panX = Math.floor((wrapRect.width - w) / 2); + } else { + const minX = wrapRect.width - w; + panX = Math.min(0, Math.max(minX, panX)); + } + if (h <= wrapRect.height) { + panY = Math.floor((wrapRect.height - h) / 2); + } else { + const minY = wrapRect.height - h; + panY = Math.min(0, Math.max(minY, panY)); + } + }; + let _panRaf = 0; + const applyPan = () => { + if (_panRaf) return; + _panRaf = requestAnimationFrame(() => { + clampPan(); + canvasStack.style.transform = `translate3d(${Math.round( + panX + )}px, ${Math.round(panY)}px, 0) scale(${_zoomLevel})`; + _panRaf = 0; + }); + }; + + const updateZoomLayout = () => { + const w = baseCanvas.width || 1, + h = baseCanvas.height || 1; + baseCanvas.style.width = w + 'px'; + baseCanvas.style.height = h + 'px'; + maskCanvas.style.width = w + 'px'; + maskCanvas.style.height = h + 'px'; + canvasStack.style.width = w + 'px'; + canvasStack.style.height = h + 'px'; + applyPan(); + }; + const applyZoom = (z) => { + _zoomLevel = Math.max(0.05, Math.min(20, z || 1)); + zoomSlider.value = _zoomLevel; + updateZoomLayout(); + if (zoomValue) zoomValue.textContent = `${Math.round(_zoomLevel * 100)}%`; + }; + zoomSlider.addEventListener('input', () => { + applyZoom(parseFloat(zoomSlider.value)); + }); + if (zoomInBtn) + zoomInBtn.addEventListener('click', () => applyZoom(parseFloat(zoomSlider.value) + 0.1)); + if (zoomOutBtn) + zoomOutBtn.addEventListener('click', () => applyZoom(parseFloat(zoomSlider.value) - 0.1)); + const computeFitZoom = () => { + const wrapRect = panStage?.getBoundingClientRect(); + if (!wrapRect) return 1; + const w = baseCanvas.width || 1; + const h = baseCanvas.height || 1; + const margin = 10; + const scaleX = (wrapRect.width - margin) / w; + const scaleY = (wrapRect.height - margin) / h; + return Math.max(0.05, Math.min(20, Math.min(scaleX, scaleY))); + }; + if (zoomFitBtn) + zoomFitBtn.addEventListener('click', () => { + applyZoom(computeFitZoom()); + centerInView(); + }); + if (zoomActualBtn) + zoomActualBtn.addEventListener('click', () => { + applyZoom(1); + centerInView(); + }); + + const centerInView = () => { + if (!panStage) return; + const rect = panStage.getBoundingClientRect(); + const w = (baseCanvas.width || 1) * _zoomLevel; + const h = (baseCanvas.height || 1) * _zoomLevel; + panX = Math.floor((rect.width - w) / 2); + panY = Math.floor((rect.height - h) / 2); + applyPan(); + }; + + let isPanning = false; + let startX = 0, + startY = 0, + startPanX = 0, + startPanY = 0; + let allowPan = false; // Space key + let panMode = false; // Explicit pan mode toggle for touch/one-button mice + const isPanMouseButton = (e) => e.button === 1 || e.button === 2; + const setCursor = (val) => { + if (panStage) panStage.style.cursor = val; + }; + const isPanActive = (e) => panMode || allowPan || isPanMouseButton(e); + const updatePanModeBtn = () => { + if (!panModeBtn) return; + panModeBtn.classList.toggle('active', panMode); + panModeBtn.setAttribute('aria-pressed', panMode ? 'true' : 'false'); + }; + if (panModeBtn) { + updatePanModeBtn(); + panModeBtn.addEventListener('click', () => { + panMode = !panMode; + updatePanModeBtn(); + setCursor(panMode ? 'grab' : ''); + }); + } + if (panStage) { + panStage.addEventListener('contextmenu', (e) => { + if (allowPan) e.preventDefault(); + }); + window.addEventListener('keydown', (e) => { + if (e.code === 'Space') { + allowPan = true; + setCursor('grab'); + } + }); + window.addEventListener('keyup', (e) => { + if (e.code === 'Space') { + allowPan = false; + if (!isPanning) setCursor(''); + } + }); + panStage.addEventListener('mousedown', (e) => { + if (!isPanActive(e)) return; + e.preventDefault(); + isPanning = true; + startX = e.clientX; + startY = e.clientY; + startPanX = panX; + startPanY = panY; + setCursor('grabbing'); + }); + window.addEventListener('mousemove', (e) => { + if (!isPanning) return; + const dx = e.clientX - startX; + const dy = e.clientY - startY; + panX = startPanX + dx; + panY = startPanY + dy; + applyPan(); + }); + window.addEventListener('mouseup', () => { + if (isPanning) { + isPanning = false; + setCursor(allowPan ? 'grab' : ''); + } + }); + panStage.addEventListener( + 'wheel', + (e) => { + if (!e.ctrlKey && !e.metaKey) return; + e.preventDefault(); + const rect = panStage.getBoundingClientRect(); + const cx = e.clientX - rect.left - panX; + const cy = e.clientY - rect.top - panY; + const before = _zoomLevel; + const step = Math.max(0.05, Math.min(0.5, Math.abs(e.deltaY) > 20 ? 0.2 : 0.1)); + const next = Math.max(0.05, Math.min(20, before + (e.deltaY > 0 ? -step : step))); + if (next === before) return; + const scale = next / before; + panX = panX - cx * (scale - 1); + panY = panY - cy * (scale - 1); + applyZoom(next); + }, + { passive: false } + ); + let lastTouchDist = null; + let touchStartTime = 0; + let doubleTapTimer = null; + panStage.addEventListener( + 'touchstart', + (e) => { + if (e.touches.length === 1) { + const t = e.touches[0]; + isPanning = true; + startX = t.clientX; + startY = t.clientY; + startPanX = panX; + startPanY = panY; + setCursor('grabbing'); + const now = Date.now(); + if (now - touchStartTime < 300) { + // double tap -> toggle 100%/fit + const z = Math.abs(_zoomLevel - 1) < 0.01 ? computeFitZoom() : 1; + applyZoom(z); + centerInView(); + if (doubleTapTimer) clearTimeout(doubleTapTimer); + } else { + touchStartTime = now; + doubleTapTimer = setTimeout(() => { + doubleTapTimer = null; + }, 320); + } + } else if (e.touches.length === 2) { + // Pinch start + const [a, b] = e.touches; + lastTouchDist = Math.hypot(b.clientX - a.clientX, b.clientY - a.clientY); + } + }, + { passive: true } + ); + panStage.addEventListener( + 'touchmove', + (e) => { + if (e.touches.length === 1 && isPanning) { + const t = e.touches[0]; + const dx = t.clientX - startX; + const dy = t.clientY - startY; + panX = startPanX + dx; + panY = startPanY + dy; + applyPan(); + } else if (e.touches.length === 2 && lastTouchDist != null) { + e.preventDefault(); + const [a, b] = e.touches; + const dist = Math.hypot(b.clientX - a.clientX, b.clientY - a.clientY); + const rect = panStage.getBoundingClientRect(); + const centerX = (a.clientX + b.clientX) / 2 - rect.left - panX; + const centerY = (a.clientY + b.clientY) / 2 - rect.top - panY; + const before = _zoomLevel; + const scale = dist / (lastTouchDist || dist); + const next = Math.max(0.05, Math.min(20, before * scale)); + if (next !== before) { + panX = panX - centerX * (next / before - 1); + panY = panY - centerY * (next / before - 1); + applyZoom(next); + } + lastTouchDist = dist; + } + }, + { passive: false } + ); + panStage.addEventListener('touchend', () => { + isPanning = false; + lastTouchDist = null; + setCursor(panMode || allowPan ? 'grab' : ''); + }); + } + const schedulePreview = () => { + if (_previewTimer) clearTimeout(_previewTimer); + const run = () => { + _previewTimer = null; + _updateResizePreview(); + }; + if (window.requestIdleCallback) { + _previewTimer = setTimeout(() => requestIdleCallback(run, { timeout: 150 }), 50); + } else { + _previewTimer = setTimeout(() => requestAnimationFrame(run), 50); + } + }; + // Track dragging to reduce work and skip dithering during drag + const markDragStart = () => { + _isDraggingSize = true; + }; + const markDragEnd = () => { + _isDraggingSize = false; + schedulePreview(); + }; + widthSlider.addEventListener('pointerdown', markDragStart); + heightSlider.addEventListener('pointerdown', markDragStart); + widthSlider.addEventListener('pointerup', markDragEnd); + heightSlider.addEventListener('pointerup', markDragEnd); + widthSlider.addEventListener('input', () => { + onWidthInput(); + schedulePreview(); + }); + heightSlider.addEventListener('input', () => { + onHeightInput(); + schedulePreview(); + }); + + // Mask painting UX: brush size, modes, row/column fills, and precise coords + let draggingMask = false; + let lastPaintX = -1, + lastPaintY = -1; + let brushSize = 1; + let rowColSize = 1; + let maskMode = 'ignore'; // 'ignore' | 'unignore' | 'toggle' + const brushEl = resizeContainer.querySelector('#maskBrushSize'); + const brushValEl = resizeContainer.querySelector('#maskBrushSizeValue'); + const btnIgnore = resizeContainer.querySelector('#maskModeIgnore'); + const btnUnignore = resizeContainer.querySelector('#maskModeUnignore'); + const btnToggle = resizeContainer.querySelector('#maskModeToggle'); + const clearIgnoredBtnEl = resizeContainer.querySelector('#clearIgnoredBtn'); + const invertMaskBtn = resizeContainer.querySelector('#invertMaskBtn'); + const rowColSizeEl = resizeContainer.querySelector('#rowColSize'); + const rowColSizeValEl = resizeContainer.querySelector('#rowColSizeValue'); + + const updateModeButtons = () => { + const map = [ + [btnIgnore, 'ignore'], + [btnUnignore, 'unignore'], + [btnToggle, 'toggle'], + ]; + for (const [el, m] of map) { + if (!el) continue; + const active = maskMode === m; + el.classList.toggle('active', active); + el.setAttribute('aria-pressed', active ? 'true' : 'false'); + } + }; + const setMode = (mode) => { + maskMode = mode; + updateModeButtons(); + }; + if (brushEl && brushValEl) { + brushEl.addEventListener('input', () => { + brushSize = parseInt(brushEl.value, 10) || 1; + brushValEl.textContent = brushSize; + }); + brushValEl.textContent = brushEl.value; + brushSize = parseInt(brushEl.value, 10) || 1; + } + if (rowColSizeEl && rowColSizeValEl) { + rowColSizeEl.addEventListener('input', () => { + rowColSize = parseInt(rowColSizeEl.value, 10) || 1; + rowColSizeValEl.textContent = rowColSize; + }); + rowColSizeValEl.textContent = rowColSizeEl.value; + rowColSize = parseInt(rowColSizeEl.value, 10) || 1; + } + if (btnIgnore) btnIgnore.addEventListener('click', () => setMode('ignore')); + if (btnUnignore) btnUnignore.addEventListener('click', () => setMode('unignore')); + if (btnToggle) btnToggle.addEventListener('click', () => setMode('toggle')); + // Initialize button state (default to toggle mode) + updateModeButtons(); + + const mapClientToPixel = (clientX, clientY) => { + // Compute without rounding until final step to avoid drift at higher zoom + const rect = baseCanvas.getBoundingClientRect(); + const scaleX = rect.width / baseCanvas.width; + const scaleY = rect.height / baseCanvas.height; + const dx = (clientX - rect.left) / scaleX; + const dy = (clientY - rect.top) / scaleY; + const x = Math.floor(dx); + const y = Math.floor(dy); + return { x, y }; + }; + + const ensureMask = (w, h) => { + if (!state.resizeIgnoreMask || state.resizeIgnoreMask.length !== w * h) { + state.resizeIgnoreMask = new Uint8Array(w * h); + } + }; + + const paintCircle = (cx, cy, radius, value) => { + const w = baseCanvas.width, + h = baseCanvas.height; + ensureMask(w, h); + const r2 = radius * radius; + for (let yy = cy - radius; yy <= cy + radius; yy++) { + if (yy < 0 || yy >= h) continue; + for (let xx = cx - radius; xx <= cx + radius; xx++) { + if (xx < 0 || xx >= w) continue; + const dx = xx - cx, + dy = yy - cy; + if (dx * dx + dy * dy <= r2) { + const idx = yy * w + xx; + let val = state.resizeIgnoreMask[idx]; + if (maskMode === 'toggle') { + val = val ? 0 : 1; + } else if (maskMode === 'ignore') { + val = 1; + } else { + val = 0; + } + state.resizeIgnoreMask[idx] = val; + if (_maskData) { + const p = idx * 4; + if (val) { + _maskData[p] = 255; + _maskData[p + 1] = 0; + _maskData[p + 2] = 0; + _maskData[p + 3] = 150; + } else { + _maskData[p] = 0; + _maskData[p + 1] = 0; + _maskData[p + 2] = 0; + _maskData[p + 3] = 0; + } + _markDirty(xx, yy); + } + } + } + } + }; + + const paintRow = (y, value) => { + const w = baseCanvas.width, + h = baseCanvas.height; + ensureMask(w, h); + if (y < 0 || y >= h) return; + + // Paint multiple rows based on rowColSize + const halfSize = Math.floor(rowColSize / 2); + const startY = Math.max(0, y - halfSize); + const endY = Math.min(h - 1, y + halfSize); + + for (let rowY = startY; rowY <= endY; rowY++) { + for (let x = 0; x < w; x++) { + const idx = rowY * w + x; + let val = state.resizeIgnoreMask[idx]; + if (maskMode === 'toggle') { + val = val ? 0 : 1; + } else if (maskMode === 'ignore') { + val = 1; + } else { + val = 0; + } + state.resizeIgnoreMask[idx] = val; + if (_maskData) { + const p = idx * 4; + if (val) { + _maskData[p] = 255; + _maskData[p + 1] = 0; + _maskData[p + 2] = 0; + _maskData[p + 3] = 150; + } else { + _maskData[p] = 0; + _maskData[p + 1] = 0; + _maskData[p + 2] = 0; + _maskData[p + 3] = 0; + } + } + } + if (_maskData) { + _markDirty(0, rowY); + _markDirty(w - 1, rowY); + } + } + }; + + const paintColumn = (x, value) => { + const w = baseCanvas.width, + h = baseCanvas.height; + ensureMask(w, h); + if (x < 0 || x >= w) return; + + // Paint multiple columns based on rowColSize + const halfSize = Math.floor(rowColSize / 2); + const startX = Math.max(0, x - halfSize); + const endX = Math.min(w - 1, x + halfSize); + + for (let colX = startX; colX <= endX; colX++) { + for (let y = 0; y < h; y++) { + const idx = y * w + colX; + let val = state.resizeIgnoreMask[idx]; + if (maskMode === 'toggle') { + val = val ? 0 : 1; + } else if (maskMode === 'ignore') { + val = 1; + } else { + val = 0; + } + state.resizeIgnoreMask[idx] = val; + if (_maskData) { + const p = idx * 4; + if (val) { + _maskData[p] = 255; + _maskData[p + 1] = 0; + _maskData[p + 2] = 0; + _maskData[p + 3] = 150; + } else { + _maskData[p] = 0; + _maskData[p + 1] = 0; + _maskData[p + 2] = 0; + _maskData[p + 3] = 0; + } + } + } + if (_maskData) { + _markDirty(colX, 0); + _markDirty(colX, h - 1); + } + } + }; + + const redrawMaskOverlay = () => { + // Only flush the dirty region; full rebuild happens on size change + _flushDirty(); + }; + + const handlePaint = (e) => { + // Suppress painting while panning + if ((e.buttons & 4) === 4 || (e.buttons & 2) === 2 || allowPan) return; + const { x, y } = mapClientToPixel(e.clientX, e.clientY); + const w = baseCanvas.width, + h = baseCanvas.height; + if (x < 0 || y < 0 || x >= w || y >= h) return; + const radius = Math.max(1, Math.floor(brushSize / 2)); + if (e.shiftKey) { + paintRow(y); + } else if (e.altKey) { + paintColumn(x); + } else { + paintCircle(x, y, radius); + } + lastPaintX = x; + lastPaintY = y; + redrawMaskOverlay(); + }; + + maskCanvas.addEventListener('mousedown', (e) => { + if (e.button === 1 || e.button === 2 || allowPan) return; // let pan handler manage + draggingMask = true; + handlePaint(e); + }); + // Avoid hijacking touch gestures for panning/zooming + maskCanvas.addEventListener( + 'touchstart', + (e) => { + /* let panStage handle */ + }, + { passive: true } + ); + maskCanvas.addEventListener( + 'touchmove', + (e) => { + /* let panStage handle */ + }, + { passive: true } + ); + maskCanvas.addEventListener( + 'touchend', + (e) => { + /* let panStage handle */ + }, + { passive: true } + ); + window.addEventListener('mousemove', (e) => { + if (draggingMask) handlePaint(e); + }); + window.addEventListener('mouseup', () => { + if (draggingMask) { + draggingMask = false; + saveBotSettings(); + } + }); + + if (clearIgnoredBtnEl) + clearIgnoredBtnEl.addEventListener('click', () => { + const w = baseCanvas.width, + h = baseCanvas.height; + if (state.resizeIgnoreMask) state.resizeIgnoreMask.fill(0); + _ensureMaskOverlayBuffers(w, h, true); + _updateResizePreview(); + saveBotSettings(); + }); + + if (invertMaskBtn) + invertMaskBtn.addEventListener('click', () => { + if (!state.resizeIgnoreMask) return; + for (let i = 0; i < state.resizeIgnoreMask.length; i++) + state.resizeIgnoreMask[i] = state.resizeIgnoreMask[i] ? 0 : 1; + const w = baseCanvas.width, + h = baseCanvas.height; + _ensureMaskOverlayBuffers(w, h, true); + _updateResizePreview(); + saveBotSettings(); + }); + + confirmResize.onclick = async () => { + const newWidth = parseInt(widthSlider.value, 10); + const newHeight = parseInt(heightSlider.value, 10); + + // Generate the final paletted image data + const tempCanvas = document.createElement('canvas'); + const tempCtx = tempCanvas.getContext('2d'); + tempCanvas.width = newWidth; + tempCanvas.height = newHeight; + tempCtx.imageSmoothingEnabled = false; + if (baseProcessor !== processor && (!baseProcessor.img || !baseProcessor.canvas)) { + await baseProcessor.load(); + } + tempCtx.drawImage(baseProcessor.img, 0, 0, newWidth, newHeight); + const imgData = tempCtx.getImageData(0, 0, newWidth, newHeight); + const data = imgData.data; + const tThresh2 = state.customTransparencyThreshold || CONFIG.TRANSPARENCY_THRESHOLD; + let totalValidPixels = 0; + const mask = + state.resizeIgnoreMask && state.resizeIgnoreMask.length === newWidth * newHeight + ? state.resizeIgnoreMask + : null; + + const applyFSDitherFinal = async () => { + const w = newWidth, + h = newHeight; + const n = w * h; + const { work, eligible } = ensureDitherBuffers(n); + for (let y = 0; y < h; y++) { + for (let x = 0; x < w; x++) { + const idx = y * w + x; + const i4 = idx * 4; + const r = data[i4], + g = data[i4 + 1], + b = data[i4 + 2], + a = data[i4 + 3]; + const masked = mask && mask[idx]; + const isEligible = + !masked && + (state.paintTransparentPixels || a >= tThresh2) && + (state.paintWhitePixels || !Utils.isWhitePixel(r, g, b)); + eligible[idx] = isEligible ? 1 : 0; + work[idx * 3] = r; + work[idx * 3 + 1] = g; + work[idx * 3 + 2] = b; + if (!isEligible) { + data[i4 + 3] = 0; + } + } + // Yield to keep UI responsive + if ((y & 15) === 0) await Promise.resolve(); + } + + const diffuse = (nx, ny, er, eg, eb, factor) => { + if (nx < 0 || nx >= w || ny < 0 || ny >= h) return; + const nidx = ny * w + nx; + if (!eligible[nidx]) return; + const base = nidx * 3; + work[base] = Math.min(255, Math.max(0, work[base] + er * factor)); + work[base + 1] = Math.min(255, Math.max(0, work[base + 1] + eg * factor)); + work[base + 2] = Math.min(255, Math.max(0, work[base + 2] + eb * factor)); + }; + + for (let y = 0; y < h; y++) { + for (let x = 0; x < w; x++) { + const idx = y * w + x; + if (!eligible[idx]) continue; + const base = idx * 3; + const r0 = work[base], + g0 = work[base + 1], + b0 = work[base + 2]; + const [nr, ng, nb] = Utils.findClosestPaletteColor( + r0, + g0, + b0, + state.activeColorPalette + ); + const i4 = idx * 4; + data[i4] = nr; + data[i4 + 1] = ng; + data[i4 + 2] = nb; + data[i4 + 3] = 255; + totalValidPixels++; + + const er = r0 - nr; + const eg = g0 - ng; + const eb = b0 - nb; + + diffuse(x + 1, y, er, eg, eb, 7 / 16); + diffuse(x - 1, y + 1, er, eg, eb, 3 / 16); + diffuse(x, y + 1, er, eg, eb, 5 / 16); + diffuse(x + 1, y + 1, er, eg, eb, 1 / 16); + } + // Yield every row to reduce jank + await Promise.resolve(); + } + }; + + if (state.ditheringEnabled) { + await applyFSDitherFinal(); + } else { + for (let i = 0; i < data.length; i += 4) { + const r = data[i], + g = data[i + 1], + b = data[i + 2], + a = data[i + 3]; + const masked = mask && mask[i >> 2]; + const isTransparent = (!state.paintTransparentPixels && a < tThresh2) || masked; + const isWhiteAndSkipped = !state.paintWhitePixels && Utils.isWhitePixel(r, g, b); + if (isTransparent || isWhiteAndSkipped) { + data[i + 3] = 0; // overlay transparency + continue; + } + totalValidPixels++; + const [nr, ng, nb] = Utils.findClosestPaletteColor(r, g, b, state.activeColorPalette); + data[i] = nr; + data[i + 1] = ng; + data[i + 2] = nb; + data[i + 3] = 255; + } + } + tempCtx.putImageData(imgData, 0, 0); + + // Save the final pixel data for painting + // Persist the paletted (and possibly dithered) pixels so painting uses the same output seen in overlay + const palettedPixels = new Uint8ClampedArray(imgData.data); + state.imageData.pixels = palettedPixels; + state.imageData.width = newWidth; + state.imageData.height = newHeight; + state.imageData.totalPixels = totalValidPixels; + state.totalPixels = totalValidPixels; + state.paintedPixels = 0; + + state.resizeSettings = { + baseWidth: width, + baseHeight: height, + width: newWidth, + height: newHeight, + }; + saveBotSettings(); + + const finalImageBitmap = await createImageBitmap(tempCanvas); + await overlayManager.setImage(finalImageBitmap); + overlayManager.enable(); + toggleOverlayBtn.classList.add('active'); + toggleOverlayBtn.setAttribute('aria-pressed', 'true'); + + // Keep state.imageData.processor as the original-based source; painting uses paletted pixels already stored + + await updateStats(); + updateUI('resizeSuccess', 'success', { + width: newWidth, + height: newHeight, + }); + + // ✅ Mark image as processed and enable position selection + state.imageProcessed = true; + selectPosBtn.disabled = false; + selectPosBtn.title = Utils.t('selectPosition') || 'Select starting position on canvas'; + + // Enable start button if position is already set + if (state.startPosition) { + startBtn.disabled = false; + } + + closeResizeDialog(); + }; + + downloadPreviewBtn.onclick = () => { + try { + const w = baseCanvas.width, + h = baseCanvas.height; + const out = document.createElement('canvas'); + out.width = w; + out.height = h; + const octx = out.getContext('2d'); + octx.imageSmoothingEnabled = false; + octx.drawImage(baseCanvas, 0, 0); + octx.drawImage(maskCanvas, 0, 0); + const link = document.createElement('a'); + link.download = 'wplace-preview.png'; + link.href = out.toDataURL(); + link.click(); + } catch (e) { + console.warn('Failed to download preview:', e); + } + }; + + cancelResize.onclick = closeResizeDialog; + + editImageBtn.onclick = () => { + showEditPanel(); + }; + + resizeOverlay.style.display = 'block'; + resizeContainer.style.display = 'block'; + + // Reinitialize color palette with current available colors + initializeColorPalette(resizeContainer); + + _updateResizePreview(); + _resizeDialogCleanup = () => { + try { + zoomSlider.replaceWith(zoomSlider.cloneNode(true)); + } catch { } + try { + if (zoomInBtn) zoomInBtn.replaceWith(zoomInBtn.cloneNode(true)); + } catch { } + try { + if (zoomOutBtn) zoomOutBtn.replaceWith(zoomOutBtn.cloneNode(true)); + } catch { } + }; + setTimeout(() => { + if (typeof computeFitZoom === 'function') { + const z = computeFitZoom(); + if (!isNaN(z) && isFinite(z)) { + applyZoom(z); + centerInView(); + } + } else { + centerInView(); + } + }, 0); + } + + function closeResizeDialog() { + try { + if (typeof _resizeDialogCleanup === 'function') { + _resizeDialogCleanup(); + } + } catch { } + resizeOverlay.style.display = 'none'; + resizeContainer.style.display = 'none'; + _updateResizePreview = () => { }; + try { + if (typeof cancelAnimationFrame === 'function' && _panRaf) { + cancelAnimationFrame(_panRaf); + } + } catch { } + try { + if (_previewTimer) { + clearTimeout(_previewTimer); + _previewTimer = null; + } + } catch { } + _maskImageData = null; + _maskData = null; + _dirty = null; + _ditherWorkBuf = null; + _ditherEligibleBuf = null; + _resizeDialogCleanup = null; + } + + function showEditPanel() { + try { + // Validate that we have a valid base canvas + if (!baseCanvas || !baseCanvas.width || !baseCanvas.height) { + Utils.showAlert('No image available for editing. Please upload an image first.', 'error'); + return; + } + + // Hide resize panel + resizeContainer.style.display = 'none'; + + // Create edit panel if it doesn't exist + let editOverlay = document.getElementById('editOverlay'); + if (!editOverlay) { + createEditPanel(); + editOverlay = document.getElementById('editOverlay'); + } + + // Get current image data from baseCanvas + const imageData = baseCanvas.toDataURL(); + + // Initialize edit panel with current image + initializeEditPanel(imageData); + + // Show edit panel + editOverlay.style.display = 'block'; + + console.log('✨ Pixel Art Editor opened successfully'); + } catch (error) { + console.error('Error opening pixel art editor:', error); + Utils.showAlert('Failed to open pixel art editor. Please try again.', 'error'); + } + } + + function createEditPanel() { + const editOverlay = document.createElement('div'); + editOverlay.id = 'editOverlay'; + editOverlay.className = 'edit-overlay'; + + editOverlay.innerHTML = ` +
+
+
+

Manual Pixel Art Editor

+
+ 🖱️ Left click: Draw | Right click/Ctrl+drag: Pan | Mouse wheel: Zoom +
+
+
+
+ + + + + +
+
+ + +
+
+
+ +
+
+
+
+ + + + + +
+ +
+ + + 1 + +
+ +
+ + + +
+
+ +
+
+
+ +
+
+
Navigator
+
+ +
+
+
+
+
+
+ +
+
+ Position: (0, 0) | Color: #000000 | Zoom: 100% +
+
+ +
+
+
+ +
+ +
+
+
+
+
+ + `; + + document.body.appendChild(editOverlay); + + // Set up event handlers + setupEditPanelEvents(); + } + + function setupEditPanelEvents() { + const editBackBtn = document.getElementById('editBackBtn'); + const editApplyBtn = document.getElementById('editApplyBtn'); + const paintBrush = document.getElementById('paintBrush'); + const eraseTool = document.getElementById('eraseTool'); + const eyedropperTool = document.getElementById('eyedropperTool'); + const fillTool = document.getElementById('fillTool'); + const brushSize = document.getElementById('brushSize'); + const brushSizeValue = document.getElementById('brushSizeValue'); + const showGrid = document.getElementById('showGrid'); + const undoBtn = document.getElementById('undoBtn'); + const redoBtn = document.getElementById('redoBtn'); + const resetViewBtn = document.getElementById('resetViewBtn'); + const editZoomIn = document.getElementById('editZoomIn'); + const editZoomOut = document.getElementById('editZoomOut'); + const zoomSelect = document.getElementById('zoomSelect'); + const zoomFit = document.getElementById('zoomFit'); + const zoom100 = document.getElementById('zoom100'); + const minimapCanvas = document.getElementById('minimapCanvas'); + + // Back to resize panel + editBackBtn.onclick = () => { + document.getElementById('editOverlay').style.display = 'none'; + resizeContainer.style.display = 'block'; + }; + + // Apply changes + editApplyBtn.onclick = () => { + applyEditChanges(); + document.getElementById('editOverlay').style.display = 'none'; + resizeContainer.style.display = 'block'; + }; + + // Tool selection + paintBrush.onclick = () => { + selectTool('paint'); + }; + + eraseTool.onclick = () => { + selectTool('erase'); + }; + + eyedropperTool.onclick = () => { + selectTool('eyedropper'); + }; + + fillTool.onclick = () => { + selectTool('fill'); + }; + + // Grid toggle + showGrid.onclick = () => { + editState.showGrid = !editState.showGrid; + showGrid.classList.toggle('active', editState.showGrid); + redrawCanvas(); + }; + + // Brush size + brushSize.oninput = () => { + brushSizeValue.textContent = brushSize.value; + updateBrushSize(parseInt(brushSize.value)); + }; + + // Undo/Redo + undoBtn.onclick = () => undoEdit(); + redoBtn.onclick = () => redoEdit(); + + // Reset view + resetViewBtn.onclick = () => resetEditView(); + + // Enhanced zoom controls + editZoomIn.onclick = () => zoomIn(); + editZoomOut.onclick = () => zoomOut(); + + zoomSelect.onchange = () => { + const newZoom = parseFloat(zoomSelect.value); + setZoom(newZoom); + }; + + zoomFit.onclick = () => fitToWindow(); + zoom100.onclick = () => setZoom(1); + + // Minimap navigation + if (minimapCanvas) { + minimapCanvas.onclick = (e) => navigateToMinimapPosition(e); + } + } + + const ZOOM_LEVELS = [0.1, 0.25, 0.5, 0.75, 1, 1.5, 2, 3, 4, 6, 8, 12, 16, 24, 32]; + + let editState = { + currentTool: 'paint', + currentColor: '#000000', + currentColorId: null, + brushSize: 1, + zoom: 1, + panX: 0, + panY: 0, + undoStack: [], + redoStack: [], + isDrawing: false, + isPanning: false, + showGrid: false, + + mouseX: 0, + mouseY: 0, + canvasWidth: 0, + canvasHeight: 0, + updatePending: false, + lastPaintPos: null, + touchStart: null, + lastTouchDistance: 0, + lastTouchCenter: { x: 0, y: 0 } + }; + + function calculateOptimalPanelSize(imageWidth, imageHeight) { + const viewportWidth = window.innerWidth; + const viewportHeight = window.innerHeight; + + // Reserve minimal space for UI elements for almost fullscreen experience + const uiReserved = { + header: 80, + toolbar: 60, + bottomBar: 120, + padding: 40 + }; + + const maxCanvasWidth = viewportWidth - uiReserved.padding; + const maxCanvasHeight = viewportHeight - uiReserved.header - uiReserved.toolbar - uiReserved.bottomBar - uiReserved.padding; + + // Calculate optimal initial zoom to fit image + const scaleX = maxCanvasWidth / imageWidth; + const scaleY = maxCanvasHeight / imageHeight; + const initialZoom = Math.min(scaleX, scaleY, 1); // Don't zoom in initially + + return { + panelWidth: Math.min(viewportWidth * 0.95, imageWidth * initialZoom + uiReserved.colorPanel + uiReserved.padding), + panelHeight: Math.min(viewportHeight * 0.95, imageHeight * initialZoom + uiReserved.toolbar + uiReserved.statusBar + uiReserved.canvasToolbar + uiReserved.padding), + canvasWidth: Math.min(maxCanvasWidth, imageWidth * initialZoom), + canvasHeight: Math.min(maxCanvasHeight, imageHeight * initialZoom), + initialZoom: Math.max(0.1, initialZoom) + }; + } + + function initializeEditPanel(imageData) { + const editCanvas = document.getElementById('editCanvas'); + const ctx = editCanvas.getContext('2d'); + + // Reset edit state + editState.zoom = 1; + editState.panX = 0; + editState.panY = 0; + editState.undoStack = []; + editState.redoStack = []; + editState.currentTool = 'paint'; + editState.brushSize = 1; + editState.showGrid = false; + editState.isPanning = false; + editState.isDrawing = false; + editState.lastPaintPos = null; + + // Set canvas size to match baseCanvas + editCanvas.width = baseCanvas.width; + editCanvas.height = baseCanvas.height; + editState.canvasWidth = editCanvas.width; + editState.canvasHeight = editCanvas.height; + + // Configure canvas context for pixel art + ctx.imageSmoothingEnabled = false; + ctx.webkitImageSmoothingEnabled = false; + ctx.mozImageSmoothingEnabled = false; + ctx.msImageSmoothingEnabled = false; + + // Calculate optimal panel size + const panelSize = calculateOptimalPanelSize(editCanvas.width, editCanvas.height); + const editContainer = document.querySelector('.edit-container'); + if (editContainer) { + editContainer.style.width = panelSize.panelWidth + 'px'; + editContainer.style.height = panelSize.panelHeight + 'px'; + } + + // Setup canvas container + setupCanvasContainer(); + + // Add CSS checkerboard background to the wrapper element (not the canvas) + // This allows users to see transparency without saving it to the template + const canvasWrapper = document.getElementById('editCanvasWrapper'); + if (canvasWrapper) { + canvasWrapper.style.background = ` + repeating-conic-gradient(#f0f0f0 0% 25%, #e0e0e0 0% 50%) + 50% / 16px 16px + `; + } + + // Load image onto canvas + const img = new Image(); + img.onload = () => { + ctx.clearRect(0, 0, editCanvas.width, editCanvas.height); + // REMOVED: drawCheckerboardBackground - prevents checkerboard from being saved with template + ctx.drawImage(img, 0, 0); + + // Always fit artwork to the visible area on start and center + fitToWindow(); + centerCanvas(); + + // Setup minimap + setupMinimap(); + + // Save initial state for undo + saveEditState(); + + // Set up canvas drawing events + setupCanvasDrawing(); + + // Initialize color palette + initializeEditColorPalette(); + + // Setup keyboard shortcuts + setupKeyboardShortcuts(); + + // Setup touch support + setupTouchSupport(); + + // Set initial tool + selectTool('paint'); + + // Update status bar + updateStatusBar(0, 0); + + // Center canvas initially + centerCanvas(); + }; + img.src = imageData; + } + + // mapClientToCanvas function for coordinate mapping + function mapClientToCanvas(clientX, clientY) { + const canvas = document.getElementById('editCanvas'); + const wrapper = document.getElementById('editCanvasWrapper'); + const container = document.getElementById('editCanvasContainer'); + + if (!canvas || !wrapper || !container) return null; + + // Get the actual canvas element bounds (after CSS transform) + const canvasRect = canvas.getBoundingClientRect(); + + // Calculate relative position within the actual canvas bounds + const relativeX = (clientX - canvasRect.left) / canvasRect.width * canvas.width; + const relativeY = (clientY - canvasRect.top) / canvasRect.height * canvas.height; + + // Convert to canvas coordinates + const canvasX = Math.floor(relativeX); + const canvasY = Math.floor(relativeY); + + // Bounds checking + if (canvasX < 0 || canvasX >= canvas.width || canvasY < 0 || canvasY >= canvas.height) { + return null; + } + + return { x: canvasX, y: canvasY }; + } + + function setupCanvasDrawing() { + const editCanvas = document.getElementById('editCanvas'); + const ctx = editCanvas.getContext('2d'); + + let isDrawing = false; + let isPanning = false; + let lastX = 0; + let lastY = 0; + let panStartX = 0; + let panStartY = 0; + + const getMousePos = (e) => { + return mapClientToCanvas(e.clientX, e.clientY); + }; + + editCanvas.onmousedown = (e) => { + if (e.button === 2 || e.ctrlKey) { // Right click or Ctrl+click for panning + editState.isPanning = true; + panStartX = e.clientX - editState.panX; + panStartY = e.clientY - editState.panY; + editCanvas.style.cursor = 'move'; + e.preventDefault(); + return; + } + + const pos = getMousePos(e); + if (!pos) return; + + if (editState.currentTool === 'eyedropper') { + handleEyedropper(pos.x, pos.y); + return; + } + + if (editState.currentTool === 'fill') { + floodFill(pos.x, pos.y, editState.currentColor); + saveEditState(); + return; + } + + editState.isDrawing = true; + editState.lastPaintPos = { x: pos.x, y: pos.y }; + lastX = pos.x; + lastY = pos.y; + + paintAtPosition(pos.x, pos.y, true); + }; + + editCanvas.onmousemove = (e) => { + const pos = getMousePos(e); + + if (pos) { + editState.mouseX = pos.x; + editState.mouseY = pos.y; + updateStatusBar(pos.x, pos.y); + } + + if (editState.isPanning) { + editState.panX = e.clientX - panStartX; + editState.panY = e.clientY - panStartY; + constrainPan(); + updateCanvasTransform(); + updateMinimap(); + return; + } + + if (!editState.isDrawing) { + return; + } + + if (pos && editState.lastPaintPos) { + paintAtPosition(pos.x, pos.y, true); + editState.lastPaintPos = { x: pos.x, y: pos.y }; + } + }; + + editCanvas.onmouseup = (e) => { + if (editState.isPanning) { + editState.isPanning = false; + selectTool(editState.currentTool); // Restore cursor + return; + } + + if (editState.isDrawing) { + editState.isDrawing = false; + editState.lastPaintPos = null; + saveEditState(); + } + }; + + editCanvas.onmouseleave = () => { + if (editState.isPanning) { + editState.isPanning = false; + selectTool(editState.currentTool); // Restore cursor + } + + if (editState.isDrawing) { + editState.isDrawing = false; + editState.lastPaintPos = null; + saveEditState(); + } + }; + + // Prevent context menu on right click + editCanvas.oncontextmenu = (e) => { + e.preventDefault(); + return false; + }; + + // Enhanced zoom with mouse wheel (zoom to cursor) + editCanvas.onwheel = (e) => { + e.preventDefault(); + const zoomFactor = e.deltaY < 0 ? 1.2 : 1 / 1.2; + zoomToPoint(editState.zoom * zoomFactor, e.clientX, e.clientY); + }; + } + + function paintAtPosition(x, y, isMouseDown = true) { + if (!isMouseDown || !editState.currentColor) return; + + const canvas = document.getElementById('editCanvas'); + const ctx = canvas.getContext('2d'); + + if (editState.lastPaintPos && editState.currentTool === 'paint') { + // Draw line from last position to current position + drawLine(ctx, editState.lastPaintPos.x, editState.lastPaintPos.y, x, y, editState.currentTool); + } else { + // Single brush stroke + drawBrush(ctx, x, y, editState.currentTool); + } + + editState.lastPaintPos = { x, y }; + + // Update minimap thumbnail + if (!editState.updatePending) { + editState.updatePending = true; + requestAnimationFrame(() => { + setupMinimap(); + editState.updatePending = false; + }); + } + } + + function drawBrush(ctx, x, y, tool) { + const size = editState.brushSize; + const halfSize = Math.floor(size / 2); + + if (tool === 'paint') { + ctx.fillStyle = editState.currentColor; + for (let dx = 0; dx < size; dx++) { + for (let dy = 0; dy < size; dy++) { + // Center the brush at the cursor position + const px = x - halfSize + dx; + const py = y - halfSize + dy; + + if (px >= 0 && px < ctx.canvas.width && py >= 0 && py < ctx.canvas.height) { + ctx.fillRect(px, py, 1, 1); + } + } + } + } else if (tool === 'erase') { + for (let dx = 0; dx < size; dx++) { + for (let dy = 0; dy < size; dy++) { + // Center the brush at the cursor position + const px = x - halfSize + dx; + const py = y - halfSize + dy; + + if (px >= 0 && px < ctx.canvas.width && py >= 0 && py < ctx.canvas.height) { + ctx.clearRect(px, py, 1, 1); + } + } + } + } + } + + function drawPixel(x, y) { + drawBrush(document.getElementById('editCanvas').getContext('2d'), x, y, 'paint'); + } + + function erasePixel(x, y) { + const editCanvas = document.getElementById('editCanvas'); + const ctx = editCanvas.getContext('2d'); + + const size = editState.brushSize; + const halfSize = Math.floor(size / 2); + + for (let dx = 0; dx < size; dx++) { + for (let dy = 0; dy < size; dy++) { + // Center the brush at the cursor position (consistent with drawBrush) + const px = x - halfSize + dx; + const py = y - halfSize + dy; + + if (px >= 0 && px < editCanvas.width && py >= 0 && py < editCanvas.height) { + ctx.clearRect(px, py, 1, 1); + } + } + } + } + + function drawLine(ctx, x1, y1, x2, y2, tool) { + // Bresenham's line algorithm with tool support + const dx = Math.abs(x2 - x1); + const dy = Math.abs(y2 - y1); + const sx = x1 < x2 ? 1 : -1; + const sy = y1 < y2 ? 1 : -1; + let err = dx - dy; + + let x = x1; + let y = y1; + + while (true) { + drawBrush(ctx, x, y, tool); + + if (x === x2 && y === y2) break; + + const e2 = 2 * err; + if (e2 > -dy) { + err -= dy; + x += sx; + } + if (e2 < dx) { + err += dx; + y += sy; + } + } + } + + function eraseLine(x1, y1, x2, y2) { + // Same as drawLine but with erase + const dx = Math.abs(x2 - x1); + const dy = Math.abs(y2 - y1); + const sx = x1 < x2 ? 1 : -1; + const sy = y1 < y2 ? 1 : -1; + let err = dx - dy; + + let x = x1; + let y = y1; + + while (true) { + erasePixel(x, y); + + if (x === x2 && y === y2) break; + + const e2 = 2 * err; + if (e2 > -dy) { + err -= dy; + x += sx; + } + if (e2 < dx) { + err += dx; + y += sy; + } + } + } + + function selectTool(tool) { + editState.currentTool = tool; + + document.querySelectorAll('.edit-tool').forEach(btn => { + btn.classList.remove('active'); + }); + + const editCanvas = document.getElementById('editCanvas'); + + switch (tool) { + case 'paint': + document.getElementById('paintBrush').classList.add('active'); + editCanvas.style.cursor = 'crosshair'; + break; + case 'erase': + document.getElementById('eraseTool').classList.add('active'); + editCanvas.style.cursor = 'not-allowed'; + break; + case 'eyedropper': + document.getElementById('eyedropperTool').classList.add('active'); + editCanvas.style.cursor = 'copy'; + break; + case 'fill': + document.getElementById('fillTool').classList.add('active'); + editCanvas.style.cursor = 'pointer'; + break; + } + } + + function updateBrushSize(size) { + editState.brushSize = size; + } + + function initializeEditColorPalette() { + const colorGrid = document.getElementById('editColorGrid'); + const currentColorDisplay = document.getElementById('currentColorDisplay'); + + colorGrid.innerHTML = ''; + + let availableColors = []; + + // Try to get colors from state first + if (state && state.availableColors && state.availableColors.length > 0) { + availableColors = state.availableColors.map(color => ({ + id: color.id, + name: color.name, + rgb: color.rgb, + hex: `#${color.rgb[0].toString(16).padStart(2, '0')}${color.rgb[1].toString(16).padStart(2, '0')}${color.rgb[2].toString(16).padStart(2, '0')}` + })); + } else { + // Fallback to CONFIG.COLOR_MAP + availableColors = Object.values(CONFIG.COLOR_MAP) + .filter(color => color.rgb !== null) + .map(color => ({ + id: color.id, + name: color.name, + rgb: [color.rgb.r, color.rgb.g, color.rgb.b], + hex: `#${color.rgb.r.toString(16).padStart(2, '0')}${color.rgb.g.toString(16).padStart(2, '0')}${color.rgb.b.toString(16).padStart(2, '0')}` + })); + } + + // Fallback to basic colors if nothing available + if (availableColors.length === 0) { + availableColors = [ + { id: 0, name: 'Black', rgb: [0, 0, 0], hex: '#000000' }, + { id: 1, name: 'White', rgb: [255, 255, 255], hex: '#ffffff' }, + { id: 2, name: 'Red', rgb: [255, 0, 0], hex: '#ff0000' }, + { id: 3, name: 'Green', rgb: [0, 255, 0], hex: '#00ff00' }, + { id: 4, name: 'Blue', rgb: [0, 0, 255], hex: '#0000ff' } + ]; + } + + // Update color count + const colorCount = document.getElementById('editColorCount'); + if (colorCount) { + colorCount.textContent = availableColors.length; + } + + availableColors.forEach(color => { + const colorBtn = document.createElement('button'); + colorBtn.className = 'color-btn'; + colorBtn.style.backgroundColor = color.hex; + colorBtn.title = `${color.name} (${color.hex})`; + colorBtn.dataset.colorId = color.id; + colorBtn.dataset.colorHex = color.hex; + + colorBtn.onclick = () => { + editState.currentColor = color.hex; + editState.currentColorId = color.id; + currentColorDisplay.style.backgroundColor = color.hex; + + // Update active color + document.querySelectorAll('.color-btn').forEach(btn => { + btn.classList.remove('selected'); + }); + colorBtn.classList.add('selected'); + + updateStatusBar(editState.mouseX, editState.mouseY); + }; + + colorGrid.appendChild(colorBtn); + }); + + // Set first color as default + if (availableColors.length > 0) { + editState.currentColor = availableColors[0].hex; + editState.currentColorId = availableColors[0].id; + currentColorDisplay.style.backgroundColor = availableColors[0].hex; + colorGrid.firstChild.classList.add('selected'); + } + } + + function saveEditState() { + const editCanvas = document.getElementById('editCanvas'); + const imageData = editCanvas.toDataURL(); + + editState.undoStack.push(imageData); + + // Limit undo stack size + if (editState.undoStack.length > 50) { + editState.undoStack.shift(); + } + + // Clear redo stack when new action is performed + editState.redoStack = []; + + updateUndoRedoButtons(); + } + + function undoEdit() { + if (editState.undoStack.length <= 1) return; + + const currentState = editState.undoStack.pop(); + editState.redoStack.push(currentState); + + const previousState = editState.undoStack[editState.undoStack.length - 1]; + loadEditState(previousState); + + updateUndoRedoButtons(); + } + + function redoEdit() { + if (editState.redoStack.length === 0) return; + + const nextState = editState.redoStack.pop(); + editState.undoStack.push(nextState); + + loadEditState(nextState); + + updateUndoRedoButtons(); + } + + function loadEditState(imageData) { + const editCanvas = document.getElementById('editCanvas'); + const ctx = editCanvas.getContext('2d'); + + const img = new Image(); + img.onload = () => { + ctx.clearRect(0, 0, editCanvas.width, editCanvas.height); + // REMOVED: drawCheckerboardBackground - prevents checkerboard from being saved with template + ctx.drawImage(img, 0, 0); + }; + img.src = imageData; + } + + function updateUndoRedoButtons() { + const undoBtn = document.getElementById('undoBtn'); + const redoBtn = document.getElementById('redoBtn'); + + if (undoBtn) { + undoBtn.disabled = editState.undoStack.length <= 1; + } + + if (redoBtn) { + redoBtn.disabled = editState.redoStack.length === 0; + } + } + + function updateCanvasTransform() { + const wrapper = document.getElementById('editCanvasWrapper'); + if (wrapper) { + wrapper.style.transform = `translate(${editState.panX}px, ${editState.panY}px) scale(${editState.zoom})`; + } + + // Update zoom select + const zoomSelect = document.getElementById('zoomSelect'); + if (zoomSelect) { + // If exact value exists, use it, otherwise show nearest but update displayed text + const exact = ZOOM_LEVELS.find(z => z === editState.zoom); + zoomSelect.value = exact != null ? exact : ZOOM_LEVELS.reduce((prev, curr) => Math.abs(curr - editState.zoom) < Math.abs(prev - editState.zoom) ? curr : prev); + // Also update status bar zoom text + updateStatusBar(editState.mouseX || 0, editState.mouseY || 0); + } + } + + function setupCanvasContainer() { + const canvasContainer = document.getElementById('editCanvasContainer'); + const editCanvas = document.getElementById('editCanvas'); + const wrapper = document.getElementById('editCanvasWrapper'); + + if (!canvasContainer || !wrapper) return; + + // Setup container styles + canvasContainer.style.cssText = ` + position: relative; + overflow: hidden; + display: flex; + align-items: center; + justify-content: center; + width: 100%; + height: 100%; + background: + linear-gradient(45deg, #f0f0f0 25%, transparent 25%), + linear-gradient(-45deg, #f0f0f0 25%, transparent 25%), + linear-gradient(45deg, transparent 75%, #f0f0f0 75%), + linear-gradient(-45deg, transparent 75%, #f0f0f0 75%); + background-size: 20px 20px; + background-position: 0 0, 0 10px, 10px -10px, -10px 0px; + background-color: #ddd; + `; + + // Setup wrapper styles + wrapper.style.cssText = ` + position: relative; + transform-origin: center center; + display: inline-block; + box-shadow: 0 4px 20px rgba(0,0,0,0.3); + border: 2px solid #333; + background: white; + `; + } + + function zoomToPoint(newZoom, clientX, clientY) { + const container = document.getElementById('editCanvasContainer'); + const wrapper = document.getElementById('editCanvasWrapper'); + + if (!container || !wrapper) return; + + newZoom = Math.max(0.1, Math.min(32, newZoom)); + + if (clientX !== undefined && clientY !== undefined) { + // Get current transform values + const oldZoom = editState.zoom; + const rect = container.getBoundingClientRect(); + + // Calculate zoom center point relative to container + const containerCenterX = rect.left + rect.width / 2; + const containerCenterY = rect.top + rect.height / 2; + + // Calculate offset to keep zoom point centered + const offsetX = (clientX - containerCenterX) * (1 - newZoom / oldZoom); + const offsetY = (clientY - containerCenterY) * (1 - newZoom / oldZoom); + + editState.panX += offsetX; + editState.panY += offsetY; + } + + editState.zoom = newZoom; + constrainPan(); + updateCanvasTransform(); + updateMinimap(); + updateStatusBar(editState.mouseX || 0, editState.mouseY || 0); + } + + function constrainPan() { + const container = document.getElementById('editCanvasContainer'); + const canvas = document.getElementById('editCanvas'); + + if (!container || !canvas) return; + + const containerRect = container.getBoundingClientRect(); + const scaledWidth = canvas.width * editState.zoom; + const scaledHeight = canvas.height * editState.zoom; + + // Small padding around edges to avoid snapping against borders + const padding = 10; + + // Calculate limits to keep canvas somewhat visible + const maxPanX = Math.max(0, (scaledWidth - containerRect.width) / 2 + padding); + const maxPanY = Math.max(0, (scaledHeight - containerRect.height) / 2 + padding); + + editState.panX = Math.max(-maxPanX, Math.min(maxPanX, editState.panX)); + editState.panY = Math.max(-maxPanY, Math.min(maxPanY, editState.panY)); + } + + function setZoom(zoom) { + zoomToPoint(zoom); + } + + function zoomIn() { + const currentIndex = ZOOM_LEVELS.findIndex(z => z >= editState.zoom); + const nextIndex = Math.min(currentIndex + 1, ZOOM_LEVELS.length - 1); + setZoom(ZOOM_LEVELS[nextIndex]); + } + + function zoomOut() { + const currentIndex = ZOOM_LEVELS.findIndex(z => z >= editState.zoom); + const prevIndex = Math.max(currentIndex - 1, 0); + setZoom(ZOOM_LEVELS[prevIndex]); + } + + function fitToWindow() { + const container = document.getElementById('editCanvasContainer'); + const canvas = document.getElementById('editCanvas'); + + if (!container || !canvas) return; + + const containerRect = container.getBoundingClientRect(); + const padding = 40; + + const scaleX = (containerRect.width - padding) / canvas.width; + const scaleY = (containerRect.height - padding) / canvas.height; + const fitZoom = Math.max(0.1, Math.min(scaleX, scaleY)); + + // Center the canvas + editState.zoom = fitZoom; + editState.panX = 0; + editState.panY = 0; + updateCanvasTransform(); + updateMinimap(); + updateStatusBar(editState.mouseX || 0, editState.mouseY || 0); + } + + function centerCanvas() { + editState.panX = 0; + editState.panY = 0; + updateCanvasTransform(); + updateMinimap(); + } + + function resetEditView() { + editState.zoom = 1; + editState.panX = 0; + editState.panY = 0; + + updateCanvasTransform(); + updateMinimap(); + } + + function setupMinimap() { + const minimapCanvas = document.getElementById('minimapCanvas'); + const editCanvas = document.getElementById('editCanvas'); + const minimapContainer = document.getElementById('minimapContainer'); + + if (!minimapCanvas || !editCanvas) return; + + // Show minimap only for larger images + if (editCanvas.width < 100 || editCanvas.height < 100) { + if (minimapContainer) minimapContainer.style.display = 'none'; + return; + } + + // Calculate minimap size + const maxSize = 150; + const scale = Math.min(maxSize / editCanvas.width, maxSize / editCanvas.height); + + minimapCanvas.width = editCanvas.width * scale; + minimapCanvas.height = editCanvas.height * scale; + + // Draw thumbnail + const minimapCtx = minimapCanvas.getContext('2d'); + minimapCtx.imageSmoothingEnabled = false; + // REMOVED: drawCheckerboardBackground - prevents checkerboard from being saved with template + minimapCtx.drawImage(editCanvas, 0, 0, minimapCanvas.width, minimapCanvas.height); + + // Update viewport indicator + updateMinimap(); + } + + function updateMinimap() { + const viewport = document.getElementById('minimapViewport'); + const minimapCanvas = document.getElementById('minimapCanvas'); + const container = document.getElementById('editCanvasContainer'); + const editCanvas = document.getElementById('editCanvas'); + + if (!viewport || !minimapCanvas || !container || !editCanvas) return; + + const containerRect = container.getBoundingClientRect(); + const scale = minimapCanvas.width / editCanvas.width; + + // Calculate visible area in minimap coordinates + const visibleWidth = Math.min(containerRect.width / editState.zoom * scale, minimapCanvas.width); + const visibleHeight = Math.min(containerRect.height / editState.zoom * scale, minimapCanvas.height); + + const viewportX = (minimapCanvas.width / 2) - (editState.panX / editState.zoom * scale) - (visibleWidth / 2); + const viewportY = (minimapCanvas.height / 2) - (editState.panY / editState.zoom * scale) - (visibleHeight / 2); + + viewport.style.width = `${visibleWidth}px`; + viewport.style.height = `${visibleHeight}px`; + viewport.style.left = `${Math.max(0, Math.min(minimapCanvas.width - visibleWidth, viewportX))}px`; + viewport.style.top = `${Math.max(0, Math.min(minimapCanvas.height - visibleHeight, viewportY))}px`; + } + + function navigateToMinimapPosition(e) { + const minimapCanvas = document.getElementById('minimapCanvas'); + const editCanvas = document.getElementById('editCanvas'); + + if (!minimapCanvas || !editCanvas) return; + + const rect = minimapCanvas.getBoundingClientRect(); + const x = (e.clientX - rect.left) / minimapCanvas.width; + const y = (e.clientY - rect.top) / minimapCanvas.height; + + // Convert to canvas coordinates and center + const targetX = (x - 0.5) * editCanvas.width * editState.zoom; + const targetY = (y - 0.5) * editCanvas.height * editState.zoom; + + editState.panX = -targetX; + editState.panY = -targetY; + + constrainPan(); + updateCanvasTransform(); + updateMinimap(); + } + + function updateStatusBar(x, y) { + const statusBar = document.getElementById('editStatusBar'); + if (statusBar) { + const zoomPercent = Math.round(editState.zoom * 100); + statusBar.textContent = `Position: (${x}, ${y}) | Color: ${editState.currentColor || '#000000'} | Zoom: ${zoomPercent}%`; + } + } + + function handleEyedropper(x, y) { + const editCanvas = document.getElementById('editCanvas'); + const ctx = editCanvas.getContext('2d'); + + if (x >= 0 && x < editCanvas.width && y >= 0 && y < editCanvas.height) { + const imageData = ctx.getImageData(x, y, 1, 1); + const [r, g, b] = imageData.data; + const pickedColor = `#${r.toString(16).padStart(2, '0')}${g.toString(16).padStart(2, '0')}${b.toString(16).padStart(2, '0')}`; + + editState.currentColor = pickedColor; + const currentColorDisplay = document.getElementById('currentColorDisplay'); + if (currentColorDisplay) { + currentColorDisplay.style.backgroundColor = pickedColor; + } + + // Try to find matching color in palette + document.querySelectorAll('.edit-color-btn').forEach(btn => { + btn.classList.remove('active'); + if (btn.dataset.colorHex === pickedColor) { + btn.classList.add('active'); + editState.currentColorId = parseInt(btn.dataset.colorId); + } + }); + + updateStatusBar(x, y); + } + } + + function floodFill(startX, startY, fillColor) { + const editCanvas = document.getElementById('editCanvas'); + const ctx = editCanvas.getContext('2d'); + const imageData = ctx.getImageData(0, 0, editCanvas.width, editCanvas.height); + const data = imageData.data; + + if (startX < 0 || startX >= editCanvas.width || startY < 0 || startY >= editCanvas.height) return; + + const startIndex = (startY * editCanvas.width + startX) * 4; + const startR = data[startIndex]; + const startG = data[startIndex + 1]; + const startB = data[startIndex + 2]; + const startA = data[startIndex + 3]; + + // Convert fill color to RGB + const fillR = parseInt(fillColor.slice(1, 3), 16); + const fillG = parseInt(fillColor.slice(3, 5), 16); + const fillB = parseInt(fillColor.slice(5, 7), 16); + + // Don't fill if the color is already the same + if (startR === fillR && startG === fillG && startB === fillB) return; + + const pixelsToCheck = [{ x: startX, y: startY }]; + const checkedPixels = new Set(); + + while (pixelsToCheck.length > 0) { + const { x, y } = pixelsToCheck.pop(); + const key = `${x},${y}`; + + if (checkedPixels.has(key)) continue; + checkedPixels.add(key); + + if (x < 0 || x >= editCanvas.width || y < 0 || y >= editCanvas.height) continue; + + const index = (y * editCanvas.width + x) * 4; + const r = data[index]; + const g = data[index + 1]; + const b = data[index + 2]; + const a = data[index + 3]; + + if (r === startR && g === startG && b === startB && a === startA) { + data[index] = fillR; + data[index + 1] = fillG; + data[index + 2] = fillB; + data[index + 3] = 255; + + pixelsToCheck.push({ x: x + 1, y }); + pixelsToCheck.push({ x: x - 1, y }); + pixelsToCheck.push({ x, y: y + 1 }); + pixelsToCheck.push({ x, y: y - 1 }); + } + } + + ctx.putImageData(imageData, 0, 0); + } + + // Brush preview removed per user request + function hideBrushPreview() { + // No-op - brush preview disabled + } + + function drawGrid(ctx, width, height) { + if (!editState.showGrid || editState.zoom < 4) return; + + ctx.save(); + ctx.strokeStyle = 'rgba(128, 128, 128, 0.3)'; + ctx.lineWidth = 1 / editState.zoom; + + for (let x = 0; x <= width; x++) { + ctx.beginPath(); + ctx.moveTo(x, 0); + ctx.lineTo(x, height); + ctx.stroke(); + } + + for (let y = 0; y <= height; y++) { + ctx.beginPath(); + ctx.moveTo(0, y); + ctx.lineTo(width, y); + ctx.stroke(); + } + + ctx.restore(); + } + + function drawCheckerboardBackground(ctx, width, height) { + const checkerSize = 8; // Size of each checker square + ctx.fillStyle = '#f0f0f0'; // Light gray + ctx.fillRect(0, 0, width, height); + + ctx.fillStyle = '#e0e0e0'; // Slightly darker gray + for (let x = 0; x < width; x += checkerSize) { + for (let y = 0; y < height; y += checkerSize) { + if ((Math.floor(x / checkerSize) + Math.floor(y / checkerSize)) % 2 === 1) { + ctx.fillRect(x, y, checkerSize, checkerSize); + } + } + } + } + + function redrawCanvas() { + if (editState.undoStack.length === 0) return; + + const currentState = editState.undoStack[editState.undoStack.length - 1]; + const editCanvas = document.getElementById('editCanvas'); + const ctx = editCanvas.getContext('2d'); + + const img = new Image(); + img.onload = () => { + ctx.clearRect(0, 0, editCanvas.width, editCanvas.height); + // REMOVED: drawCheckerboardBackground - prevents checkerboard from being saved with template + ctx.drawImage(img, 0, 0); + drawGrid(ctx, editCanvas.width, editCanvas.height); + }; + img.src = currentState; + } + + function setupTouchSupport() { + const canvas = document.getElementById('editCanvas'); + if (!canvas) return; + + let touchStartTime = 0; + + canvas.addEventListener('touchstart', handleTouchStart, { passive: false }); + canvas.addEventListener('touchmove', handleTouchMove, { passive: false }); + canvas.addEventListener('touchend', handleTouchEnd, { passive: false }); + + function handleTouchStart(e) { + e.preventDefault(); + touchStartTime = Date.now(); + + if (e.touches.length === 1) { + // Single touch - start painting + const touch = e.touches[0]; + const coords = mapClientToCanvas(touch.clientX, touch.clientY); + if (coords) { + editState.isDrawing = true; + editState.lastPaintPos = { x: coords.x, y: coords.y }; + paintAtPosition(coords.x, coords.y, true); + } + } else if (e.touches.length === 2) { + // Two finger - prepare for zoom/pan + const touch1 = e.touches[0]; + const touch2 = e.touches[1]; + + editState.lastTouchDistance = Math.hypot( + touch2.clientX - touch1.clientX, + touch2.clientY - touch1.clientY + ); + + editState.lastTouchCenter = { + x: (touch1.clientX + touch2.clientX) / 2, + y: (touch1.clientY + touch2.clientY) / 2 + }; + } + } + + function handleTouchMove(e) { + e.preventDefault(); + + if (e.touches.length === 1 && editState.isDrawing) { + // Continue painting + const touch = e.touches[0]; + const coords = mapClientToCanvas(touch.clientX, touch.clientY); + if (coords) { + paintAtPosition(coords.x, coords.y, true); + } + } else if (e.touches.length === 2) { + // Pinch zoom and pan + const touch1 = e.touches[0]; + const touch2 = e.touches[1]; + + const currentDistance = Math.hypot( + touch2.clientX - touch1.clientX, + touch2.clientY - touch1.clientY + ); + + const currentCenter = { + x: (touch1.clientX + touch2.clientX) / 2, + y: (touch1.clientY + touch2.clientY) / 2 + }; + + // Zoom based on distance change + if (editState.lastTouchDistance > 0) { + const zoomFactor = currentDistance / editState.lastTouchDistance; + const newZoom = Math.max(0.1, Math.min(32, editState.zoom * zoomFactor)); + zoomToPoint(newZoom, currentCenter.x, currentCenter.y); + } + + // Pan based on center movement + editState.panX += currentCenter.x - editState.lastTouchCenter.x; + editState.panY += currentCenter.y - editState.lastTouchCenter.y; + + editState.lastTouchDistance = currentDistance; + editState.lastTouchCenter = currentCenter; + + constrainPan(); + updateCanvasTransform(); + updateMinimap(); + } + } + + function handleTouchEnd(e) { + e.preventDefault(); + + if (editState.isDrawing) { + editState.isDrawing = false; + editState.lastPaintPos = null; + saveEditState(); + } + + if (e.touches.length === 0) { + editState.lastTouchDistance = 0; + } + } + } + + function setupKeyboardShortcuts() { + document.addEventListener('keydown', (e) => { + // Only handle shortcuts when edit panel is visible + const editOverlay = document.getElementById('editOverlay'); + if (!editOverlay || editOverlay.style.display === 'none') return; + + // Prevent default for handled keys + const handledKeys = ['b', 'e', 'i', 'f', 'g', 'z', '[', ']']; + if (handledKeys.includes(e.key.toLowerCase()) || (e.ctrlKey && e.key.toLowerCase() === 'z')) { + e.preventDefault(); + } + + switch (e.key.toLowerCase()) { + case 'b': // Brush + selectTool('paint'); + break; + case 'e': // Erase + selectTool('erase'); + break; + case 'i': // Eyedropper + selectTool('eyedropper'); + break; + case 'f': // Fill + selectTool('fill'); + break; + case 'g': // Grid + const showGrid = document.getElementById('showGrid'); + if (showGrid) showGrid.click(); + break; + case 'z': + if (e.ctrlKey || e.metaKey) { + if (e.shiftKey) { + redoEdit(); + } else { + undoEdit(); + } + } + break; + case '=': + case '+': + if (e.ctrlKey || e.metaKey) { + zoomIn(); + } + break; + case '-': + if (e.ctrlKey || e.metaKey) { + zoomOut(); + } + break; + case '0': + if (e.ctrlKey || e.metaKey) { + fitToWindow(); + } + break; + case '1': + if (e.ctrlKey || e.metaKey) { + setZoom(1); + } + break; + case '[': // Decrease brush size + const brushSize = document.getElementById('brushSize'); + const currentSize = parseInt(brushSize.value); + if (currentSize > 1) { + brushSize.value = currentSize - 1; + document.getElementById('brushSizeValue').textContent = currentSize - 1; + updateBrushSize(currentSize - 1); + } + break; + case ']': // Increase brush size + const brushSize2 = document.getElementById('brushSize'); + const currentSize2 = parseInt(brushSize2.value); + if (currentSize2 < 20) { + brushSize2.value = currentSize2 + 1; + document.getElementById('brushSizeValue').textContent = currentSize2 + 1; + updateBrushSize(currentSize2 + 1); + } + break; + } + }); + } + + function applyEditChanges() { + try { + const editCanvas = document.getElementById('editCanvas'); + if (!editCanvas) { + throw new Error('Edit canvas not found'); + } + + // Find the resize canvas in the resize panel + const resizeCanvas = document.getElementById('resizeCanvas'); + if (!resizeCanvas) { + throw new Error('Resize canvas not found'); + } + + const baseCtx = resizeCanvas.getContext('2d'); + if (!baseCtx) { + throw new Error('Resize canvas context not available'); + } + + // Make sure the resize canvas has the same dimensions as the edit canvas + if (resizeCanvas.width !== editCanvas.width || resizeCanvas.height !== editCanvas.height) { + resizeCanvas.width = editCanvas.width; + resizeCanvas.height = editCanvas.height; + } + + // Clear resize canvas + baseCtx.clearRect(0, 0, resizeCanvas.width, resizeCanvas.height); + + // Copy edited image to resize canvas (no checkerboard since we removed it entirely) + baseCtx.imageSmoothingEnabled = false; + baseCtx.drawImage(editCanvas, 0, 0); + + // CRITICAL: Completely replace the template system with edited artwork + const editedImageData = editCanvas.toDataURL(); + + // Create new processor with edited image as the template + if (window.WPlaceImageProcessor) { + const newProcessor = new window.WPlaceImageProcessor(editedImageData); + newProcessor.load().then(() => { + // Get the processed pixel data from the edited canvas + const editCtx = editCanvas.getContext('2d'); + const editImageData = editCtx.getImageData(0, 0, editCanvas.width, editCanvas.height); + const pixels = editImageData.data; + + // Count valid pixels in the edited image + let totalValidPixels = 0; + for (let i = 0; i < pixels.length; i += 4) { + const a = pixels[i + 3]; + const r = pixels[i]; + const g = pixels[i + 1]; + const b = pixels[i + 2]; + + const isTransparent = !state.paintTransparentPixels && a < state.customTransparencyThreshold; + const isWhiteAndSkipped = !state.paintWhitePixels && Utils.isWhitePixel(r, g, b); + + if (!isTransparent && !isWhiteAndSkipped) { + totalValidPixels++; + } + } + + // COMPLETELY REBUILD state.imageData with the edited artwork + state.imageData = { + width: editCanvas.width, + height: editCanvas.height, + pixels: pixels, + totalPixels: totalValidPixels, + processor: newProcessor, + }; + + // CRITICAL: Update state.originalImage so resize panel uses edited artwork as base template + state.originalImage = { + dataUrl: editedImageData, + width: editCanvas.width, + height: editCanvas.height + }; + + // Update state with new totals + state.totalPixels = totalValidPixels; + state.paintedPixels = 0; // Reset progress since this is a new template + state.currentPaintingColor = null; // Reset color tracking + state.imageLoaded = true; + + // Update local processors + if (typeof processor !== 'undefined') { + processor = newProcessor; + console.log('🔄 Updated local processor with edited artwork'); + } + if (typeof baseProcessor !== 'undefined') { + baseProcessor = newProcessor; + console.log('🔄 Updated baseProcessor with edited artwork'); + } + + // Force regeneration of overlays by clearing cached mask data + if (typeof window._maskImageData !== 'undefined') { + delete window._maskImageData; + } + + // Update UI to reflect the new template + if (typeof updateUI === 'function') { + updateUI(); + } + + // Show loading and properly reload resize panel with new template + Utils.showAlert('Updating template... Please wait.', 'info'); + + // Give more time for template to fully update and force resize panel reload + setTimeout(() => { + // Hide edit overlay first + document.getElementById('editOverlay').style.display = 'none'; + + // Completely reload resize dialog with updated processor + setTimeout(() => { + // Clean up existing dialog + if (typeof _resizeDialogCleanup === 'function') { + _resizeDialogCleanup(); + } + + // Force complete reload of resize dialog with new processor + setTimeout(() => { + showResizeDialog(newProcessor); + + console.log('✅ Template COMPLETELY replaced with edited artwork'); + console.log(`📊 New template stats: ${editCanvas.width}x${editCanvas.height}, ${totalValidPixels} pixels`); + Utils.showAlert('Template successfully replaced with your edited artwork!', 'success'); + }, 100); + }, 300); + }, 100); + }).catch((error) => { + console.error('Failed to load new processor:', error); + Utils.showAlert('Failed to update template. Please try again.', 'error'); + }); + } else { + console.error('WPlaceImageProcessor not available'); + Utils.showAlert('Image processor not available. Please reload the page.', 'error'); + } + + console.log('✅ Edit changes applied - template replacement in progress'); + } catch (error) { + console.error('Error applying edit changes:', error); + Utils.showAlert('Failed to apply changes. Please try again.', 'error'); + } + } + + if (uploadBtn) { + uploadBtn.addEventListener('click', async () => { + // Auto-open color palette if not already open + Utils.openColorPalette(); + + const availableColors = Utils.extractAvailableColors(); + if (availableColors === null || availableColors.length < 10) { + updateUI('noColorsFound', 'error'); + Utils.showAlert(Utils.t('noColorsFound'), 'error'); + return; + } + + if (!state.colorsChecked) { + state.availableColors = availableColors; + state.colorsChecked = true; + updateUI('colorsFound', 'success', { count: availableColors.length }); + await updateStats(); + selectPosBtn.disabled = false; + // Only enable resize button if image is also loaded + if (state.imageLoaded) { + resizeBtn.disabled = false; + } + } + + try { + updateUI('loadingImage', 'default'); + const imageSrc = await Utils.createImageUploader(); + if (!imageSrc) { + updateUI('colorsFound', 'success', { + count: state.availableColors.length, + }); + return; + } + + const processor = new ImageProcessor(imageSrc); + await processor.load(); + + const { width, height } = processor.getDimensions(); + const pixels = processor.getPixelData(); + + let totalValidPixels = 0; + for (let i = 0; i < pixels.length; i += 4) { + const isTransparent = + !state.paintTransparentPixels && + pixels[i + 3] < (state.customTransparencyThreshold || CONFIG.TRANSPARENCY_THRESHOLD); + const isWhiteAndSkipped = + !state.paintWhitePixels && + Utils.isWhitePixel(pixels[i], pixels[i + 1], pixels[i + 2]); + if (!isTransparent && !isWhiteAndSkipped) { + totalValidPixels++; + } + } + + state.imageData = { + width, + height, + pixels, + totalPixels: totalValidPixels, + processor, + }; + + state.totalPixels = totalValidPixels; + state.paintedPixels = 0; + state.currentPaintingColor = null; // Reset color tracking + state.imageLoaded = true; + state.imageProcessed = false; // New upload - not processed yet + + // Reset session-specific flags when a new image is loaded + state.preFilteringDone = false; + state.progressResetDone = false; + console.log('🔄 Reset session flags for new image load'); + + // Keep existing lastPosition to continue from where we left off + // state.lastPosition = { x: 0, y: 0 }; // REMOVED: Don't reset position + + // Initialize painted map for tracking + Utils.initializePaintedMap(width, height); + + // New image: clear previous resize settings + state.resizeSettings = null; + // Also clear any previous ignore mask + state.resizeIgnoreMask = null; + // Save original image for this browser (dataUrl + dims) + state.originalImage = { dataUrl: imageSrc, width, height }; + saveBotSettings(); + + // ⚠️ Use the ORIGINAL image for the overlay initially (will be replaced after processing) + const imageBitmap = await createImageBitmap(processor.img); + await overlayManager.setImage(imageBitmap); + overlayManager.enable(); + toggleOverlayBtn.disabled = false; + toggleOverlayBtn.classList.add('active'); + toggleOverlayBtn.setAttribute('aria-pressed', 'true'); + + // Only enable resize button if colors have also been captured + if (state.colorsChecked) { + resizeBtn.disabled = false; + } + saveBtn.disabled = false; + + // ⚠️ IMPORTANT: Disable position selection until image is processed + selectPosBtn.disabled = true; + selectPosBtn.title = Utils.t('imageNeedsProcessing'); + + // Disable start button since position can't be set yet + if (state.startPosition) { + startBtn.disabled = true; // Disable even if position was set before + } + + await updateStats(); + updateDataButtons(); + + // Show warning alert to process image first + Utils.showAlert(Utils.t('processImageFirst'), 'warning'); + updateUI('imageLoaded', 'success', { count: totalValidPixels }); + } catch { + updateUI('imageError', 'error'); + } + }); + } + + // Load Extracted button event listener - for loading Art-Extractor JSON files + const loadExtractedBtn = document.getElementById('loadExtractedBtn'); + if (loadExtractedBtn) { + loadExtractedBtn.addEventListener('click', async () => { + try { + // Auto-open color palette if not already open + Utils.openColorPalette(); + + 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 = []; + } + + // Reset session-specific flags when loading extracted file data + state.preFilteringDone = false; + state.progressResetDone = false; + console.log('🔄 Reset session flags for extracted file load'); + + // 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 files, force enable all necessary flags for full functionality + state.colorsChecked = true; + state.currentPaintingColor = null; // Reset color tracking + state.imageLoaded = true; + + // Ensure image processor is available for extracted files + if (state.imageData && !state.imageData.processor) { + try { + // Create a temporary canvas to generate a data URL for the processor + const canvas = document.createElement('canvas'); + canvas.width = state.imageData.width; + canvas.height = state.imageData.height; + const ctx = canvas.getContext('2d'); + + // Create image data from pixels + const imageData = new ImageData( + new Uint8ClampedArray(state.imageData.pixels), + state.imageData.width, + state.imageData.height + ); + ctx.putImageData(imageData, 0, 0); + + // Create processor with canvas data URL using the correct class + const dataUrl = canvas.toDataURL(); + state.imageData.processor = new window.WPlaceImageProcessor(dataUrl); + + // CRITICAL: Load the processor before using it + await state.imageData.processor.load(); + console.log('🔧 Created and loaded WPlaceImageProcessor for extracted artwork'); + } catch (error) { + console.error('❌ Failed to create image processor for extracted artwork:', error); + // Fallback: use global processor if available + if (window.globalImageProcessor) { + console.log('🔄 Using global image processor as fallback'); + state.imageData.processor = window.globalImageProcessor; + } + } + } + + console.log('🎨 Force-enabled all flags for extracted JSON to access resize panel'); + + // For extracted images, ensure overlay is enabled but don't set position yet + try { + if (overlayManager && state.imageData) { + console.log('🖼️ Creating overlay from loaded pixel data:', { + width: state.imageData.width, + height: state.imageData.height, + pixelCount: state.imageData.pixels?.length, + firstPixels: state.imageData.pixels?.slice(0, 12), // First 3 pixels (RGBA) + isUint8ClampedArray: state.imageData.pixels instanceof Uint8ClampedArray, + isArray: Array.isArray(state.imageData.pixels) + }); + + // 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 - should show PROCESSED pixels'); + + // ✅ Loaded files already contain processed pixels - no need to reprocess + // Mark as processed since the overlay is showing the correct processed data + state.imageProcessed = true; + + // Enable position selection immediately + selectPosBtn.disabled = false; + selectPosBtn.title = Utils.t('selectPosition') || 'Select starting position on canvas'; + } + } 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 with custom message for extracted artwork + Utils.showAlert('🎨 Extracted artwork loaded! Please click on the canvas to select where to place it.', 'info'); + updateUI('ready', 'default'); // Use 'ready' instead of 'waitingPosition' to avoid translation issues + + // Enable relevant buttons + selectPosBtn.disabled = false; + // Always enable resize button for extracted files since they have image processor + resizeBtn.disabled = false; + console.log('🔧 Resize button enabled for extracted artwork'); + + // Enable move artwork button (will be fully enabled after position is set) + const moveArtworkBtn = document.getElementById('moveArtworkBtn'); + if (moveArtworkBtn) { + moveArtworkBtn.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'); + + // Enable Move Artwork button when position is set + const moveArtworkBtn = document.getElementById('moveArtworkBtn'); + if (moveArtworkBtn) { + moveArtworkBtn.disabled = false; + } + + 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', () => { + console.log('🔍 [DEBUG] Resize button clicked. State check:', { + imageLoaded: state.imageLoaded, + hasProcessor: !!(state.imageData && state.imageData.processor), + colorsChecked: state.colorsChecked, + hasAvailableColors: !!(state.availableColors && state.availableColors.length > 0) + }); + + if (state.imageLoaded && state.imageData && state.imageData.processor) { + showResizeDialog(state.imageData.processor); + } else { + let message = 'Please upload an image and check colors first.'; + if (!state.imageLoaded) message = 'Please upload an image first.'; + else if (!state.imageData || !state.imageData.processor) message = 'Image processor not available. Please reload the image.'; + Utils.showAlert(message, 'warning'); + } + }); + } + + // Move Artwork button event listener + const moveArtworkBtn = document.getElementById('moveArtworkBtn'); + if (moveArtworkBtn) { + moveArtworkBtn.addEventListener('click', () => { + if (state.imageLoaded && (state.startPosition || state.selectingPosition)) { + showMoveArtworkPanel(); + } else if (!state.imageLoaded) { + Utils.showAlert('Please upload or load an image first', 'warning'); + } else { + Utils.showAlert('Please select a position for the artwork first', 'warning'); + } + }); + } + + if (selectPosBtn) { + selectPosBtn.addEventListener('click', async () => { + if (state.selectingPosition) return; + + state.selectingPosition = true; + state.startPosition = null; + state.region = null; + startBtn.disabled = true; + + // Disable Move Artwork button when selecting new position + const moveArtworkBtn = document.getElementById('moveArtworkBtn'); + if (moveArtworkBtn) { + moveArtworkBtn.disabled = true; + } + + Utils.showAlert(Utils.t('selectPositionAlert'), 'info'); + updateUI('waitingPosition', 'default'); + + const tempFetch = async (url, options) => { + if ( + typeof url === 'string' && + url.includes('https://backend.wplace.live/s0/pixel/') && + options?.method?.toUpperCase() === 'POST' + ) { + try { + const response = await originalFetch(url, options); + const clonedResponse = response.clone(); + const data = await clonedResponse.json(); + + if (data?.painted === 1) { + const regionMatch = url.match(/\/pixel\/(\d+)\/(\d+)/); + if (regionMatch && regionMatch.length >= 3) { + state.region = { + x: Number.parseInt(regionMatch[1]), + y: Number.parseInt(regionMatch[2]), + }; + } + + const payload = JSON.parse(options.body); + if (payload?.coords && Array.isArray(payload.coords)) { + state.startPosition = { + x: payload.coords[0], + y: payload.coords[1], + }; + // Keep existing lastPosition to continue from where we left off + // state.lastPosition = { x: 0, y: 0 }; // REMOVED: Don't reset position + + // 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; + + // Enable Move Artwork button when position is set + const moveArtworkBtn = document.getElementById('moveArtworkBtn'); + if (moveArtworkBtn) { + moveArtworkBtn.disabled = false; + } + } + + window.fetch = originalFetch; + state.selectingPosition = false; + updateUI('positionSet', 'success'); + } + } + + return response; + } catch { + return originalFetch(url, options); + } + } + return originalFetch(url, options); + }; + + const originalFetch = window.fetch; + window.fetch = tempFetch; + + setTimeout(() => { + if (state.selectingPosition) { + window.fetch = originalFetch; + state.selectingPosition = false; + updateUI('positionTimeout', 'error'); + Utils.showAlert(Utils.t('positionTimeout'), 'error'); + } + }, 120000); + }); + } + + async function startPainting() { + if (!state.imageLoaded || !state.startPosition || !state.region) { + updateUI('missingRequirements', 'error'); + return; + } + await ensureToken(); + if (!getTurnstileToken()) return; + + // Only perform progressive pixel detection on first start of session + if (!state.preFilteringDone) { + // Perform progressive pixel detection from top-left to bottom-right + console.log('🔍 Starting progressive pixel detection from top-left to bottom-right...'); + await performProgressivePixelDetection(); + + // Mark pre-filtering as done to prevent duplicate scanning + state.preFilteringDone = true; + console.log('✅ Pre-filtering marked as complete - will not scan again this session'); + } else { + console.log('🔄 Continuing session - pre-filtering already done'); + } + + state.running = true; + state.stopFlag = false; + startBtn.disabled = true; + stopBtn.disabled = false; + uploadBtn.disabled = true; + selectPosBtn.disabled = true; + resizeBtn.disabled = true; + saveBtn.disabled = true; + toggleOverlayBtn.disabled = true; + + updateUI('startPaintingMsg', 'success'); + + try { + await getAccounts(); + await processImage(); + } catch (e) { + console.error('Unexpected error:', e); + updateUI('paintingError', 'error'); + } finally { + state.running = false; + stopBtn.disabled = true; + saveBtn.disabled = false; + + if (state.stopFlag) { + startBtn.disabled = false; + } else { + startBtn.disabled = true; + uploadBtn.disabled = false; + const loadExtractedBtn = document.getElementById('loadExtractedBtn'); + if (loadExtractedBtn) loadExtractedBtn.disabled = false; + selectPosBtn.disabled = false; + resizeBtn.disabled = false; + } + toggleOverlayBtn.disabled = false; + } + } + + // Helper function to update start button state + function updateStartButtonState() { + if (!startBtn) return; + + if (state.paintingMode === 'assist') { + startBtn.disabled = true; + startBtn.title = 'Start button is disabled in Assist mode. Manually place pixels with overlay guidance.'; + } else { + startBtn.disabled = !state.imageLoaded || state.running || !state.startPosition; + startBtn.title = ''; + } + } + + if (startBtn) { + startBtn.addEventListener('click', startPainting); + } + + // Painting Mode Toggle + const paintingModeToggle = container.querySelector('#paintingModeToggle'); + if (paintingModeToggle) { + // Initialize toggle state - MUST be unchecked for auto mode (default) + paintingModeToggle.checked = state.paintingMode === 'assist'; + console.log(`🎨 [Painting Mode] Initial state: ${state.paintingMode}, Toggle checked: ${paintingModeToggle.checked}`); + + paintingModeToggle.addEventListener('change', () => { + state.paintingMode = paintingModeToggle.checked ? 'assist' : 'auto'; + + // Update start button state + updateStartButtonState(); + + // NOTE: We don't save paintingMode - it always resets to 'auto' on page load + + // Show notification + const modeName = state.paintingMode === 'assist' ? 'Assist' : 'Auto'; + const modeDesc = state.paintingMode === 'assist' + ? 'Overlay will guide your manual pixel placement' + : 'Bot will automatically paint pixels'; + Utils.showAlert(`Painting Mode: ${modeName}\n${modeDesc}`, 'info'); + + console.log(`🎨 [Painting Mode] Switched to ${modeName.toUpperCase()} mode`); + }); + + // Initial state update + updateStartButtonState(); + } + + if (stopBtn) { + stopBtn.addEventListener('click', () => { + state.stopFlag = true; + state.running = false; + stopBtn.disabled = true; + updateUI('paintingStoppedByUser', 'warning'); + + if (state.imageLoaded && state.paintedPixels > 0) { + Utils.saveProgress(); + Utils.showAlert(Utils.t('autoSaved'), 'success'); + } + }); + } + + const checkSavedProgress = () => { + const savedData = Utils.loadProgress(); + if (savedData && savedData.state.paintedPixels > 0) { + const savedDate = new Date(savedData.timestamp).toLocaleString(); + const progress = Math.round( + (savedData.state.paintedPixels / savedData.state.totalPixels) * 100 + ); + + Utils.showAlert( + `${Utils.t('savedDataFound')}\n\n` + + `Saved: ${savedDate}\n` + + `Progress: ${savedData.state.paintedPixels}/${savedData.state.totalPixels} pixels (${progress}%)\n` + + `${Utils.t('clickLoadToContinue')}`, + 'info' + ); + } + }; + + setTimeout(checkSavedProgress, 1000); + + if (cooldownSlider && cooldownInput && cooldownValue && cooldownDecrease && cooldownIncrease) { + const updateCooldown = (newValue) => { + const threshold = Math.max(1, Math.min(state.maxCharges || 999, parseInt(newValue))); + state.cooldownChargeThreshold = threshold; + + // Update both controls (value shows in input, label shows unit only) + cooldownSlider.value = threshold; + cooldownInput.value = threshold; + cooldownValue.textContent = `${Utils.t('charges')}`; + + saveBotSettings(); + NotificationManager.resetEdgeTracking(); // prevent spurious notify after threshold change + }; + + // Slider event listener + cooldownSlider.addEventListener('input', (e) => { + updateCooldown(e.target.value); + }); + + // Number input event listener + cooldownInput.addEventListener('input', (e) => { + updateCooldown(e.target.value); + }); + + // Decrease button + cooldownDecrease.addEventListener('click', () => { + updateCooldown(parseInt(cooldownInput.value) - 1); + }); + + // Increase button + cooldownIncrease.addEventListener('click', () => { + updateCooldown(parseInt(cooldownInput.value) + 1); + }); + + // Add scroll-to-adjust for cooldown slider + Utils.createScrollToAdjust(cooldownSlider, updateCooldown, 1, state.maxCharges, 1); + + // Skip cooldown button event handler + const skipCooldownBtn = document.getElementById('skipCooldownBtn'); + if (skipCooldownBtn) { + skipCooldownBtn.addEventListener('click', () => { + if (state.preciseCurrentCharges < state.cooldownChargeThreshold) { + console.log(`[Auto-Image] Skip cooldown requested - resetting to account index 0`); + + // Reset to account index 0 (start new cycle) + state.currentActiveIndex = 0; + console.log(`🔄 Reset currentActiveIndex to 0 for new cycle`); + + // Reset charges to threshold to bypass cooldown + state.preciseCurrentCharges = state.cooldownChargeThreshold; + console.log(`[Auto-Image] Cooldown skipped! Charges set to threshold: ${state.cooldownChargeThreshold}`); + + // Switch to first account if we have multiple accounts + if (accountManager.getAccountCount() > 1) { + const firstAccountInfo = accountManager.getAccountByIndex(0); + if (firstAccountInfo && firstAccountInfo.token) { + console.log(`🔄 Switching to first account: index 0 (${firstAccountInfo.displayName})`); + // Get accounts array for the switch + const accounts = JSON.parse(localStorage.getItem("accounts")) || []; + if (accounts.length > 0) { + switchToSpecificAccount(firstAccountInfo.token, firstAccountInfo.displayName).then(() => { + console.log(`✅ Successfully switched to account index 0 after cooldown skip`); + // Update UI immediately + updateChargesThresholdUI(0); + // Trigger immediate check for painting + if (state.isEnabled && !state.stopFlag) { + setTimeout(() => { + checkAndPaint(); + }, 100); + } + }).catch(err => { + console.error(`❌ Failed to switch to account index 0:`, err); + }); + } else { + console.warn(`⚠️ No accounts available for switching`); + } + } else { + console.warn(`⚠️ First account info not found or missing token`); + } + } else { + console.log(`📝 Single account mode - no switching needed`); + // Update UI immediately + updateChargesThresholdUI(0); + // Trigger immediate check for painting + if (state.isEnabled && !state.stopFlag) { + setTimeout(() => { + checkAndPaint(); + }, 100); + } + } + } + }); + } + } + + loadBotSettings(); + // Ensure notification poller reflects current settings + NotificationManager.syncFromState(); + + // Sync painting mode toggle with loaded state + if (paintingModeToggle) { + paintingModeToggle.checked = state.paintingMode === 'assist'; + updateStartButtonState(); + console.log(`🔄 [Painting Mode] Synced after load: ${state.paintingMode}, Toggle checked: ${paintingModeToggle.checked}`); + } + } + + function getMsToTargetCharges(current, target, cooldown, intervalMs = 0) { + const remainingCharges = target - current; + return Math.max(0, remainingCharges * cooldown - intervalMs); + } + + function updateChargesThresholdUI(intervalMs) { + if (state.stopFlag) return; + + const threshold = state.cooldownChargeThreshold; + const remainingMs = getMsToTargetCharges( + state.preciseCurrentCharges, + threshold, + state.cooldown, + intervalMs + ); + const timeText = Utils.msToTimeText(remainingMs); + + updateUI( + 'noChargesThreshold', + 'warning', + { + threshold, + current: state.displayCharges, + time: timeText, + }, + true + ); + + // Update skip cooldown button state + const skipCooldownBtn = document.getElementById('skipCooldownBtn'); + if (skipCooldownBtn) { + const isInCooldown = state.preciseCurrentCharges < threshold && remainingMs > 0; + skipCooldownBtn.disabled = !isInCooldown; + skipCooldownBtn.title = isInCooldown + ? `Skip cooldown (${timeText} remaining)` + : 'Skip cooldown (only available during cooldown)'; + } + } + + // Fast tile-based pixel detection from top-left to bottom-right + async function performProgressivePixelDetection() { + if (!state.imageLoaded || !state.imageData) { + console.log('⚠️ No image loaded, skipping pixel detection'); + return; + } + + const startTime = performance.now(); + const { width, height } = state.imageData; + const startX = state.startPosition.x; + const startY = state.startPosition.y; + const regionX = state.region.x; + const regionY = state.region.y; + + let detectedPixels = 0; + let totalChecked = 0; + + console.log(`🚀 Fast scanning ${width}x${height} image from top-left (0,0) to bottom-right (${width - 1},${height - 1})...`); + + updateUI('pixelDetection', 'info', { message: 'Fast detecting already painted pixels...' }); + + // Calculate affected tiles + const worldX1 = startX; + const worldY1 = startY; + const worldX2 = startX + width - 1; + const worldY2 = startY + height - 1; + + const startTileX = Math.floor(worldX1 / 1000); + const startTileY = Math.floor(worldY1 / 1000); + const endTileX = Math.floor(worldX2 / 1000); + const endTileY = Math.floor(worldY2 / 1000); + + console.log(`📄 Processing tiles from (${startTileX},${startTileY}) to (${endTileX},${endTileY})`); + + // Cache for downloaded tile data + const tileDataCache = new Map(); + + // Download and cache all required tiles in parallel + const tilePromises = []; + for (let tileY = startTileY; tileY <= endTileY; tileY++) { + for (let tileX = startTileX; tileX <= endTileX; tileX++) { + const absoluteTileX = regionX + tileX; + const absoluteTileY = regionY + tileY; + const tileKey = `${absoluteTileX},${absoluteTileY}`; + + tilePromises.push( + downloadTileImageData(absoluteTileX, absoluteTileY).then(imageData => { + if (imageData) { + tileDataCache.set(tileKey, imageData); + } + }).catch(e => { + console.warn(`⚠️ Failed to download tile ${absoluteTileX},${absoluteTileY}:`, e.message); + }) + ); + } + } + + // Wait for all tiles to download + await Promise.all(tilePromises); + console.log(`📦 Downloaded ${tileDataCache.size} tiles for fast pixel checking`); + + // Fast pixel detection using cached tile data + for (let y = 0; y < height; y++) { + for (let x = 0; x < width; x++) { + totalChecked++; + + // Check if pixel is eligible for painting + const targetPixelInfo = checkPixelEligibility(x, y); + if (!targetPixelInfo.eligible) { + continue; // Skip non-eligible pixels + } + + // Calculate absolute world coordinates + const absX = startX + x; + const absY = startY + y; + const adderX = Math.floor(absX / 1000); + const adderY = Math.floor(absY / 1000); + const pixelX = absX % 1000; + const pixelY = absY % 1000; + const absoluteTileX = regionX + adderX; + const absoluteTileY = regionY + adderY; + + // Check if already marked as painted in local map + if (Utils.isPixelPainted(x, y, absoluteTileX, absoluteTileY)) { + detectedPixels++; + continue; + } + + // Fast pixel color check using cached tile data + const tileKey = `${absoluteTileX},${absoluteTileY}`; + const tileImageData = tileDataCache.get(tileKey); + + if (tileImageData) { + try { + // Direct array access - much faster than canvas operations + const tileWidth = tileImageData.width; + const tileHeight = tileImageData.height; + const data = tileImageData.data; + + // Ensure pixel coordinates are within tile bounds + if (pixelX >= 0 && pixelX < tileWidth && pixelY >= 0 && pixelY < tileHeight) { + const pixelIndex = (pixelY * tileWidth + pixelX) * 4; + const r = data[pixelIndex]; + const g = data[pixelIndex + 1]; + const b = data[pixelIndex + 2]; + const a = data[pixelIndex + 3]; + + // Check alpha threshold + const alphaThresh = state.customTransparencyThreshold || CONFIG.TRANSPARENCY_THRESHOLD; + if (a >= alphaThresh) { + const existingMappedColor = Utils.resolveColor( + [r, g, b], + state.availableColors, + !state.paintUnavailablePixels + ); + const isAlreadyPainted = existingMappedColor.id === targetPixelInfo.mappedColorId; + + if (isAlreadyPainted) { + // Check if pixel is already marked as painted to avoid double counting + if (!Utils.isPixelPainted(x, y, absoluteTileX, absoluteTileY)) { + // Mark as painted in the map but DO NOT increment progress counter + // Progress counter should only reflect actual painting sequence position + Utils.markPixelPainted(x, y, absoluteTileX, absoluteTileY); + detectedPixels++; + } else { + // Pixel already tracked, just count it for detection stats + detectedPixels++; + } + } + } + } + } catch (e) { + // Skip pixels we can't check + console.warn(`⚠️ Could not check pixel (${x}, ${y}):`, e.message); + } + } + + // Update progress periodically + if (totalChecked % 2500 === 0) { + const progress = Math.round((totalChecked / (width * height)) * 100); + updateUI('pixelDetection', 'info', { + message: `Fast detecting... ${progress}% (Found: ${detectedPixels})` + }); + // Yield control briefly to prevent UI blocking + await new Promise(resolve => setTimeout(resolve, 0)); + } + } + } + + const processingTime = Math.round(performance.now() - startTime); + console.log(`🏁 Fast pixel detection complete in ${processingTime}ms:`); + console.log(` - Total pixels checked: ${totalChecked}`); + console.log(` - Already painted pixels found: ${detectedPixels}`); + console.log(` - Updated progress: ${state.paintedPixels}/${state.totalPixels}`); + console.log(` - Performance: ${Math.round(totalChecked / (processingTime / 1000))} pixels/second`); + + // Update progress display + await updateStats(); + updateUI('pixelDetectionComplete', 'success', { + message: `Found ${detectedPixels} already painted pixels in ${processingTime}ms` + }); + } + + // Fast tile download and ImageData extraction (similar to Art-Extractor approach) + async function downloadTileImageData(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}`); + } + + const blob = await response.blob(); + return await processTileBlob(blob); + } catch (error) { + console.warn(`Failed to download tile ${tileX},${tileY}:`, error.message); + return null; + } + } + + // Process tile blob into ImageData (adapted from Art-Extractor) + 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.imageSmoothingEnabled = false; + ctx.drawImage(img, 0, 0); + const imageData = ctx.getImageData(0, 0, canvas.width, canvas.height); + resolve(imageData); + }; + img.onerror = () => reject(new Error('Failed to load image')); + img.src = URL.createObjectURL(blob); + }); + } catch (error) { + console.error('Error processing tile blob:', error); + return null; + } + } + + function generateCoordinates(width, height, mode, direction, snake, blockWidth, blockHeight, startFromX = 0, startFromY = 0) { + const coords = []; + console.log( + 'Generating coordinates with \n mode:', + mode, + '\n direction:', + direction, + '\n snake:', + snake, + '\n blockWidth:', + blockWidth, + '\n blockHeight:', + blockHeight, + '\n startFromX:', + startFromX, + '\n startFromY:', + startFromY + ); + // --------- Standard 4 corners traversal ---------- + let xStart, xEnd, xStep; + let yStart, yEnd, yStep; + switch (direction) { + case 'top-left': + xStart = 0; + xEnd = width; + xStep = 1; + yStart = 0; + yEnd = height; + yStep = 1; + break; + case 'top-right': + xStart = width - 1; + xEnd = -1; + xStep = -1; + yStart = 0; + yEnd = height; + yStep = 1; + break; + case 'bottom-left': + xStart = 0; + xEnd = width; + xStep = 1; + yStart = height - 1; + yEnd = -1; + yStep = -1; + break; + case 'bottom-right': + xStart = width - 1; + xEnd = -1; + xStep = -1; + yStart = height - 1; + yEnd = -1; + yStep = -1; + break; + default: + throw new Error(`Unknown direction: ${direction}`); + } + + // --------- Traversal modes ---------- + if (mode === 'rows') { + let rowIndex = 0; + for (let y = yStart; y !== yEnd; y += yStep) { + if (snake && rowIndex % 2 !== 0) { + for (let x = xEnd - xStep; x !== xStart - xStep; x -= xStep) { + coords.push([x, y]); + } + } else { + for (let x = xStart; x !== xEnd; x += xStep) { + coords.push([x, y]); + } + } + rowIndex++; + } + } else if (mode === 'columns') { + let colIndex = 0; + for (let x = xStart; x !== xEnd; x += xStep) { + if (snake && colIndex % 2 !== 0) { + for (let y = yEnd - yStep; y !== yStart - yStep; y -= yStep) { + coords.push([x, y]); + } + } else { + for (let y = yStart; y !== yEnd; y += yStep) { + coords.push([x, y]); + } + } + colIndex++; + } + } else if (mode === 'circle-out') { + const cx = Math.floor(width / 2); + const cy = Math.floor(height / 2); + const maxRadius = Math.ceil(Math.sqrt(cx * cx + cy * cy)); + + for (let r = 0; r <= maxRadius; r++) { + for (let y = cy - r; y <= cy + r; y++) { + for (let x = cx - r; x <= cx + r; x++) { + if (x >= 0 && x < width && y >= 0 && y < height) { + const dist = Math.max(Math.abs(x - cx), Math.abs(y - cy)); + if (dist === r) coords.push([x, y]); + } + } + } + } + } else if (mode === 'circle-in') { + const cx = Math.floor(width / 2); + const cy = Math.floor(height / 2); + const maxRadius = Math.ceil(Math.sqrt(cx * cx + cy * cy)); + + for (let r = maxRadius; r >= 0; r--) { + for (let y = cy - r; y <= cy + r; y++) { + for (let x = cx - r; x <= cx + r; x++) { + if (x >= 0 && x < width && y >= 0 && y < height) { + const dist = Math.max(Math.abs(x - cx), Math.abs(y - cy)); + if (dist === r) coords.push([x, y]); + } + } + } + } + } else if (mode === 'blocks' || mode === 'shuffle-blocks') { + const blocks = []; + for (let by = 0; by < height; by += blockHeight) { + for (let bx = 0; bx < width; bx += blockWidth) { + const block = []; + for (let y = by; y < Math.min(by + blockHeight, height); y++) { + for (let x = bx; x < Math.min(bx + blockWidth, width); x++) { + block.push([x, y]); + } + } + blocks.push(block); + } + } + + if (mode === 'shuffle-blocks') { + // Simple Fisher-Yates shuffle + for (let i = blocks.length - 1; i > 0; i--) { + const j = Math.floor(Math.random() * (i + 1)); + [blocks[i], blocks[j]] = [blocks[j], blocks[i]]; + } + } + + // Append all blocks without spread/concat to avoid large allocations and argument explosion + for (const block of blocks) { + for (let i = 0; i < block.length; i++) { + coords.push(block[i]); + } + } + } else { + throw new Error(`Unknown mode: ${mode}`); + } + + // Filter coordinates to start from the specified position + 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]; + if (x === startFromX && y === startFromY) { + startIndex = i; + break; + } + } + + if (startIndex >= 0) { + // Resume from the found position (skip all previous coordinates) + const filteredCoords = coords.slice(startIndex); + console.log(`✂️ Resuming: skipped ${startIndex} coordinates, continuing with ${filteredCoords.length} remaining`); + return filteredCoords; + } else { + console.warn(`⚠️ Resume position (${startFromX}, ${startFromY}) not found in coordinate list, starting from beginning`); + } + } + + return coords; + } + + async function flushPixelBatch(pixelBatch) { + if (!pixelBatch || pixelBatch.pixels.length === 0) return true; + + const batchSize = pixelBatch.pixels.length; + console.log( + `📦 Sending batch with ${batchSize} pixels (region: ${pixelBatch.regionX},${pixelBatch.regionY})` + ); + const success = await sendBatchWithRetry( + pixelBatch.pixels, + pixelBatch.regionX, + pixelBatch.regionY + ); + if (success) { + // Only increment progress for actually painted pixels to prevent multiplication + const actuallyPaintedCount = pixelBatch.pixels.length; + state.paintedPixels += actuallyPaintedCount; + console.log(`📊 Added ${actuallyPaintedCount} painted pixels to progress (total: ${state.paintedPixels})`); + + pixelBatch.pixels.forEach((p) => { + Utils.markPixelPainted(p.x, p.y, pixelBatch.regionX, pixelBatch.regionY); + }); + + // Update last painted position to the last pixel in the successful batch + if (pixelBatch.pixels.length > 0) { + const lastPixel = pixelBatch.pixels[pixelBatch.pixels.length - 1]; + // FIXED: Use localX/localY (image-relative coordinates) instead of x/y (absolute canvas coordinates) + state.lastPaintedPosition = { x: lastPixel.localX, y: lastPixel.localY }; + } + + // 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, + }; + await updateStats(); + // Update account list with new charges + await updateCurrentAccountInList(); + // Progress tracking removed from UI to reduce visual clutter + Utils.performSmartSave(); + + if (CONFIG.PAINTING_SPEED_ENABLED && state.paintingSpeed > 0 && batchSize > 0) { + const delayPerPixel = 1000 / state.paintingSpeed; + const totalDelay = Math.max(100, delayPerPixel * batchSize); + await Utils.sleep(totalDelay); + } + } else { + console.error(`❌ Batch failed permanently after retries. Stopping painting.`); + state.stopFlag = true; + updateUI('paintingBatchFailed', 'error'); + } + + pixelBatch.pixels = []; + return success; + } + + 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!'); + state.currentPaintingColor = null; // Reset color tracking + break; + } + + if (paintingResult === 'stopped') { + console.log('⏹️ Painting stopped by user'); + break; + } + + if (paintingResult === 'charges_depleted') { + if (CONFIG.autoBuyToggle && CONFIG.autoBuy != 'none') { + console.log('Trying to buy more charges before swapping account...'); + const purchaseResult = await purchase(CONFIG.autoBuy); + if (purchaseResult == 2) { + console.log('✅ Purchase successful, continuing painting'); + await updateStats(); + await updateCurrentAccountInList(); + if (CONFIG.autoBuy == 'paint_charges') continue; + } + else if (purchaseResult == 1) { + console.log('😭 Not enough droplets to buy more charges, swapping account.'); + } + else { + console.log('🤔 Purchase failed.'); + } + } + else console.log('🤫 Auto buy is disabled, wait for cooldown or swapping account.'); + 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; + break; + } + } 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; + break; + } + } else { + // Debug current state + const totalAccounts = accountManager.getAccountCount(); + console.log(`📊 Account Status - Current index: ${accountManager.currentIndex}, Total accounts: ${totalAccounts}`); + + // Check if we're at the last account in the sequence + const isLastAccount = accountManager.currentIndex >= totalAccounts - 1; + console.log(`🔍 Is last account? ${isLastAccount} (index ${accountManager.currentIndex} of ${totalAccounts})`); + + if (!isLastAccount && totalAccounts > 1) { + // Update current account status before switching + console.log('📊 Updating current account status before switch...'); + await updateCurrentAccountInList(); + + // Switch to next account immediately (no cooldown) - only if we have multiple accounts + const nextAccount = accountManager.getNextAccount(); + console.log(`🔄 Switching to next account: ${nextAccount?.displayName} (${accountManager.currentIndex + 2}/${totalAccounts})`); + const switchResult = await switchToNextAccount(accounts); + if (!switchResult) { + console.log('❌ Account switch failed, stopping'); + state.stopFlag = true; + break; + } + // Continue painting with new account immediately + continue; + } else if (totalAccounts === 1) { + // Only one account available - use cooldown and continue with same account + console.log('ℹ️ Only one account available, entering cooldown period'); + const cooldownResult = await executeCooldownPeriod(); + if (cooldownResult === 'stopped') break; + console.log('✅ Cooldown complete, continuing with same account'); + continue; + } else { + // Last account reached - use cooldown then switch to first account + console.log('⏱️ Last account reached, entering cooldown period'); + console.log(`📊 Current account: index ${accountManager.currentIndex}, Last account: index ${totalAccounts - 1}`); + + const cooldownResult = await executeCooldownPeriod(); + if (cooldownResult === 'stopped') break; + + // After cooldown, switch to first account + console.log('🔁 Cooldown complete, switching to first account'); + console.log(`🔄 Before switch - Current index: ${accountManager.currentIndex}, Target: index 0`); + + const firstAccountInfo = accountManager.getAccountByIndex(0); + const firstAccountToken = firstAccountInfo?.token; + + if (!firstAccountToken) { + console.log('❌ First account token not found, stopping'); + state.stopFlag = true; + break; + } + + // Reset to first position + accountManager.setCurrentIndex(0); + + const switchResult = await switchToSpecificAccount(firstAccountToken, firstAccountInfo.displayName); + 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 { + await finalizePaintingProcess(); + } + } + + // Phase 1: Execute a complete painting session using all available charges + async function executePaintingSession() { + console.log('🎨 Starting painting session - using all charges until 0'); + const { width, height, pixels } = state.imageData; + const { x: startX, y: startY } = state.startPosition; + const { x: regionX, y: regionY } = state.region; + + // 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( + regionX, + regionY, + width, + height, + startX, + startY, + 10000 // timeout 10s + ); + + if (!tilesReady) { + updateUI('overlayTilesNotLoaded', 'error'); + state.stopFlag = true; + return 'stopped'; + } + } + + let pixelBatch = null; + let skippedPixels = { + transparent: 0, + white: 0, + alreadyPainted: 0, + colorUnavailable: 0, + }; + + // IMPORTANT: Check charges once at start, then paint until depleted + console.log('🔋 Checking initial charges for painting session'); + const initialChargeCheck = await WPlaceService.getCharges(); + 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'; + } + + console.log(`🔋 Starting with ${state.displayCharges} charges - painting until depleted`); + + // Paint pixels until we run out of charges or complete the image + try { + + // Generate the actual filtered coordinates for painting (resuming from last position) + const coords = await generateCoordinatesAsync( + width, + height, + state.coordinateMode, + state.coordinateDirection, + state.coordinateSnake, + state.blockWidth, + state.blockHeight, + state.lastPosition.x, + state.lastPosition.y + ); + + // OPTIMIZATION: Pre-filter already painted pixels (happens only once per session) + let eligibleCoords = []; + let alreadyPaintedCount = 0; + + 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; + let adderX = Math.floor(absX / 1000); + let adderY = Math.floor(absY / 1000); + let pixelX = absX % 1000; + let pixelY = absY % 1000; + + try { + const tilePixelRGBA = await overlayManager.getTilePixelColor( + regionX + adderX, + regionY + adderY, + pixelX, + pixelY + ); + + if (tilePixelRGBA && Array.isArray(tilePixelRGBA)) { + const mappedCanvasColor = Utils.resolveColor( + tilePixelRGBA.slice(0, 3), + state.availableColors, + !state.paintUnavailablePixels // Use same parameter as target pixel + ); + const isMatch = mappedCanvasColor.id === targetPixelInfo.mappedColorId; + if (isMatch) { + alreadyPaintedCount++; + // Mark as painted in map but DO NOT increment progress counter + // Progress should only reflect actual painting sequence position + Utils.markPixelPainted(x, y, regionX + adderX, regionY + adderY); + continue; // Skip already painted pixels + } + } + } 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 (not added to progress counter)`); + console.log('ℹ️ This detection will not happen again until a new image/save is loaded'); + console.log('📊 Progress counter only reflects actual painting sequence position'); + // No need to update stats since progress wasn't changed + } + skippedPixels.alreadyPainted = alreadyPaintedCount; + } else { + // Pre-filtering already done this session, just filter for basic eligibility + 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 + let absX = startX + x; + let absY = startY + y; + let adderX = Math.floor(absX / 1000); + let adderY = Math.floor(absY / 1000); + + if (!Utils.isPixelPainted(x, y, regionX + adderX, regionY + adderY)) { + eligibleCoords.push([x, y, targetPixelInfo]); + } + } + } + + // Group pixels by color if color-by-color mode is enabled + let pixelsToProcess = eligibleCoords; + if (state.paintingOrder === 'color-by-color') { + console.log('🎨 Color-by-color mode enabled - grouping pixels by color'); + + // Group pixels by color ID + const colorGroups = new Map(); + for (const [x, y, targetPixelInfo] of eligibleCoords) { + const colorId = targetPixelInfo.mappedColorId; + if (!colorGroups.has(colorId)) { + colorGroups.set(colorId, []); + } + colorGroups.get(colorId).push([x, y, targetPixelInfo]); + } + + // Log color groups + console.log(`📊 Found ${colorGroups.size} different colors to paint:`); + for (const [colorId, pixels] of colorGroups.entries()) { + const colorInfo = Object.values(CONFIG.COLOR_MAP).find(c => c.id === colorId); + const colorName = colorInfo ? colorInfo.name : `Color ${colorId}`; + console.log(` 🎨 ${colorName} (ID: ${colorId}): ${pixels.length} pixels`); + } + + // Process colors one by one + const sortedColorGroups = Array.from(colorGroups.entries()).sort((a, b) => a[0] - b[0]); + + // If we have a current painting color, resume from that color + let startIndex = 0; + if (state.currentPaintingColor !== null) { + startIndex = sortedColorGroups.findIndex(([colorId]) => colorId === state.currentPaintingColor); + if (startIndex === -1) startIndex = 0; + console.log(`🔄 Resuming from color ID ${state.currentPaintingColor} (index ${startIndex})`); + } + + // Flatten the groups starting from the current color + pixelsToProcess = []; + for (let i = startIndex; i < sortedColorGroups.length; i++) { + const [colorId, pixels] = sortedColorGroups[i]; + // Avoid using spread or concat with very large arrays to prevent call stack/alloc overhead + for (let j = 0; j < pixels.length; j++) { + pixelsToProcess.push(pixels[j]); + } + } + + console.log(`✅ Prepared ${pixelsToProcess.length} pixels for color-by-color painting`); + } + + // Paint eligible pixels (already pre-filtered, no duplicate checks) + outerLoop: for (const [x, y, targetPixelInfo] of pixelsToProcess) { + // Track current color being painted in color-by-color mode + if (state.paintingOrder === 'color-by-color') { + if (state.currentPaintingColor !== targetPixelInfo.mappedColorId) { + state.currentPaintingColor = targetPixelInfo.mappedColorId; + const colorInfo = Object.values(CONFIG.COLOR_MAP).find(c => c.id === state.currentPaintingColor); + const colorName = colorInfo ? colorInfo.name : `Color ${state.currentPaintingColor}`; + console.log(`🎨 Now painting: ${colorName} (ID: ${state.currentPaintingColor})`); + + // Update UI to show current color + const statusDiv = document.getElementById('statusDiv'); + if (statusDiv) { + const colorMessage = Utils.t('currentlyPaintingColor', { colorName }); + const colorIndicator = document.getElementById('currentColorIndicator'); + if (colorIndicator) { + colorIndicator.textContent = colorMessage; + } else { + const indicator = document.createElement('div'); + indicator.id = 'currentColorIndicator'; + indicator.textContent = colorMessage; + indicator.style.cssText = 'margin-top: 8px; padding: 8px; background: rgba(255,255,255,0.1); border-radius: 6px; font-weight: bold;'; + statusDiv.appendChild(indicator); + } + } + } + } + + if (state.stopFlag) { + if (pixelBatch && pixelBatch.pixels.length > 0) { + console.log(`🎯 Sending last batch before stop with ${pixelBatch.pixels.length} pixels`); + await flushPixelBatch(pixelBatch); + } + state.lastPosition = { x, y }; + // Show paused coordinates in UI with proper translation template + // Use last painted position if available, otherwise use current position + const pausedX = state.lastPaintedPosition.x || x; + const pausedY = state.lastPaintedPosition.y || y; + updateUI('paintingPaused', 'warning', { x: pausedX, y: pausedY }); + console.log(`⏸️ Painting paused after last painted coordinates (${pausedX}, ${pausedY})`); + return 'stopped'; + } + + // Check if we have charges left (local count, no API call) + if (state.displayCharges <= 0) { + // 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'; + } + // else { + // console.log(`🔋 Charges after purchase: ${state.displayCharges}, continuing painting`); + // } + } + + let absX = startX + x; + let absY = startY + y; + + let adderX = Math.floor(absX / 1000); + let adderY = Math.floor(absY / 1000); + let pixelX = absX % 1000; + let pixelY = absY % 1000; + + // CRITICAL FIX: Always check if pixel is already painted (both locally and on canvas) + if (Utils.isPixelPainted(x, y, regionX + adderX, regionY + adderY)) { + console.log(`⏭️ Skipping already painted pixel at (${x}, ${y}) - marked in local map`); + continue; // Skip already painted pixels + } + + // REAL-TIME CANVAS CHECK: Verify against actual canvas state to prevent overpainting + try { + const existingColorRGBA = await overlayManager.getTilePixelColor( + regionX + adderX, + regionY + adderY, + pixelX, + pixelY + ).catch(() => null); + + if (existingColorRGBA && Array.isArray(existingColorRGBA)) { + const [er, eg, eb] = existingColorRGBA; + const existingMappedColor = Utils.resolveColor( + [er, eg, eb], + state.availableColors, + !state.paintUnavailablePixels + ); + const isAlreadyCorrect = existingMappedColor.id === targetPixelInfo.mappedColorId; + + if (isAlreadyCorrect) { + console.log(`✅ Pixel at (${x}, ${y}) already has correct color (${existingMappedColor.id}) - marking as painted`); + // Mark it as painted in local map but DO NOT increment progress counter + // Progress should only reflect actual painting sequence position + Utils.markPixelPainted(x, y, regionX + adderX, regionY + adderY); + continue; // Skip painting this pixel + } + } + } catch (e) { + // If we can't check the canvas, proceed with painting (better to attempt than skip) + console.warn(`⚠️ Could not verify canvas state for pixel (${x}, ${y}), proceeding with paint:`, e.message); + } + + const targetMappedColorId = targetPixelInfo.mappedColorId; + + // Set up pixel batch for new region if needed + if ( + !pixelBatch || + pixelBatch.regionX !== regionX + adderX || + pixelBatch.regionY !== regionY + adderY + ) { + if (pixelBatch && pixelBatch.pixels.length > 0) { + console.log(`🌍 Sending region-change batch with ${pixelBatch.pixels.length} pixels`); + const success = await flushPixelBatch(pixelBatch); + if (!success) { + console.error(`❌ Batch failed permanently after retries. Stopping painting.`); + state.stopFlag = true; + return 'stopped'; + } + await updateStats(); + } + + pixelBatch = { + regionX: regionX + adderX, + regionY: regionY + adderY, + pixels: [], + }; + } + + // Add pixel to batch (no need to check again - already pre-filtered) + pixelBatch.pixels.push({ + x: pixelX, + y: pixelY, + color: targetMappedColorId, + localX: x, + localY: y, + }); + + // Send batch if it's full + const maxBatchSize = calculateBatchSize(); + if (pixelBatch.pixels.length >= maxBatchSize) { + console.log(`📦 Sending batch with ${pixelBatch.pixels.length} pixels`); + const success = await flushPixelBatch(pixelBatch); + if (!success) { + console.error(`❌ Batch failed permanently after retries. Stopping painting.`); + state.stopFlag = true; + return 'stopped'; + } + pixelBatch.pixels = []; + await updateStats(); + } + } + + // Send final batch if any pixels remain + if (pixelBatch && pixelBatch.pixels.length > 0 && !state.stopFlag) { + console.log(`🏁 Sending final batch with ${pixelBatch.pixels.length} pixels`); + const success = await flushPixelBatch(pixelBatch); + if (!success) { + console.warn(`⚠️ Final batch failed with ${pixelBatch.pixels.length} pixels`); + } + } + + // If we completed the entire coordinate loop, image is complete + return state.stopFlag ? 'stopped' : 'completed'; + + } finally { + // Log skip statistics for this session + console.log(`📊 Session Statistics:`); + console.log(` New pixels painted: ${state.paintedPixels - (skippedPixels.alreadyPainted || 0)}`); + console.log(` Already painted detected: ${skippedPixels.alreadyPainted}`); + console.log(` Total progress: ${state.paintedPixels}`); + console.log(` Pre-filtered - Transparent: ${skippedPixels.transparent}`); + console.log(` Pre-filtered - White: ${skippedPixels.white}`); + console.log(` Pre-filtered - Color Unavailable: ${skippedPixels.colorUnavailable}`); + } + } + + // Phase 2: Execute cooldown period - wait for target charges (NO token regeneration) + async function executeCooldownPeriod() { + console.log('⏱️ Entering cooldown period - waiting for target charges'); + console.log('🚫 NO token regeneration during cooldown (even if expired/invalid)'); + + // Check initial charges to calculate wait time + let chargeCheckCount = 0; + // const maxChargeChecks = 10; // REMOVED: No limit on API calls during cooldown + + while (!state.stopFlag) { + chargeCheckCount++; + + const { charges, cooldown } = await WPlaceService.getCharges(); + state.displayCharges = Math.floor(charges); + state.preciseCurrentCharges = charges; + state.cooldown = cooldown; + + if (state.displayCharges >= state.cooldownChargeThreshold) { + console.log(`✅ Cooldown target reached: ${state.displayCharges}/${state.cooldownChargeThreshold}`); + NotificationManager.maybeNotifyChargesReached(true); + await updateStats(); + return 'target_reached'; + } + + updateUI('noChargesThreshold', 'warning', { + time: Utils.msToTimeText(state.cooldown), + threshold: state.cooldownChargeThreshold, + current: state.displayCharges, + }); + await updateStats(); + + // 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) { + // First few checks - shorter delay + delayTime = Math.max(3000, state.cooldown); // 3 seconds minimum + } else if (estimatedWaitTime > 60000) { + // Long wait expected - check every 15 seconds + delayTime = 10000; + } else if (estimatedWaitTime > 30000) { + // Medium wait - check every 15 seconds + delayTime = 15000; + } else { + // Close to target - check every 5 seconds + delayTime = 5000; + } + + console.log(`⏱️ Cooldown check ${chargeCheckCount}: ${state.displayCharges}/${state.cooldownChargeThreshold} charges, waiting 10s before next check`); + await Utils.sleep(10000); + + // REMOVED: No limit on charge checks - bot will wait infinitely until charges are available + // if (chargeCheckCount >= maxChargeChecks) { + // console.warn('⚠️ Max charge checks reached during cooldown, continuing anyway'); + // break; + // } + } + + return 'stopped'; + } + + // 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) { + console.error('❌ Token regeneration failed:', error); + return false; + } + } + + // Finalize painting process cleanup + async function finalizePaintingProcess() { + console.log('🧹 Finalizing painting process'); + + if (window._chargesInterval) { + clearInterval(window._chargesInterval); + window._chargesInterval = null; + } + + if (state.stopFlag) { + // Save progress when stopped to preserve painted map + Utils.saveProgress(); + } else { + updateUI('paintingComplete', 'success', { count: state.paintedPixels }); + state.lastPosition = { x: 0, y: 0 }; // Only reset when truly complete + Utils.saveProgress(); // Save final complete state + overlayManager.clear(); + const toggleOverlayBtn = document.getElementById('toggleOverlayBtn'); + if (toggleOverlayBtn) { + toggleOverlayBtn.classList.remove('active'); + toggleOverlayBtn.disabled = true; + } + } + } + + // Helper function to check pixel eligibility (shared by painting functions) + function checkPixelEligibility(x, y) { + const { width, height, pixels } = state.imageData; + const transparencyThreshold = state.customTransparencyThreshold || CONFIG.TRANSPARENCY_THRESHOLD; + + // CRITICAL FIX: Check module availability before processing + if (!Utils || typeof Utils.isWhitePixel !== 'function') { + console.error('❌ Utils module not available for pixel eligibility check'); + return { + eligible: false, + reason: 'moduleUnavailable', + }; + } + + const idx = (y * width + x) * 4; + const r = pixels[idx], + g = pixels[idx + 1], + b = pixels[idx + 2], + a = pixels[idx + 3]; + + if (!state.paintTransparentPixels && a < transparencyThreshold) + return { + eligible: false, + reason: 'transparent', + }; + if (!state.paintWhitePixels && Utils.isWhitePixel(r, g, b)) + return { + eligible: false, + reason: 'white', + }; + + let targetRgb = Utils.isWhitePixel(r, g, b) + ? [255, 255, 255] + : Utils.findClosestPaletteColor(r, g, b, state.activeColorPalette); + + const mappedTargetColorId = Utils.resolveColor( + targetRgb, + state.availableColors, + !state.paintUnavailablePixels + ); + + if (!state.paintUnavailablePixels && !mappedTargetColorId.id) { + return { + eligible: false, + reason: 'colorUnavailable', + r, + g, + b, + a, + mappedColorId: mappedTargetColorId.id, + }; + } + return { eligible: true, r, g, b, a, mappedColorId: mappedTargetColorId.id }; + } + + // Helper function to skip pixel and log the reason (minimized logging) + function skipPixel(reason, id, rgb, x, y, skippedPixels) { + // Minimize logging to prevent console flooding - only log non-routine skips + if (reason !== 'transparent' && reason !== 'alreadyPainted') { + console.log(`Skipped pixel for ${reason} (id: ${id}, (${rgb.join(', ')})) at (${x}, ${y})`); + } + skippedPixels[reason]++; + } + function calculateBatchSize() { + let targetBatchSize; + + // If speed control is disabled, use all available charges + if (!CONFIG.PAINTING_SPEED_ENABLED) { + targetBatchSize = state.displayCharges; + console.log(`🚀 Speed control disabled: using all ${targetBatchSize} available charges`); + return Math.max(1, targetBatchSize); + } + + // CRITICAL FIX: When speed control is ENABLED, use actual available charges as the batch size + // This ensures bot sends exactly the number of pixels matching available charges + // Example: 75 charges → send 75 pixels, 14 charges → send 14 pixels + const availableCharges = state.displayCharges; + + if (state.batchMode === 'random') { + // Generate random batch size within the specified range + const min = Math.max(1, state.randomBatchMin); + const max = Math.max(min, state.randomBatchMax); + targetBatchSize = Math.floor(Math.random() * (max - min + 1)) + min; + console.log(`🎲 Random batch size generated: ${targetBatchSize} (range: ${min}-${max})`); + } else { + // Normal mode - use the fixed paintingSpeed value + targetBatchSize = state.paintingSpeed; + } + + // FIXED: Use actual available charges as the maximum batch size + // This matches the available pixels exactly (e.g., 75 charges = 75 pixel batch) + const finalBatchSize = Math.min(targetBatchSize, availableCharges); + + console.log(`📊 Batch size: ${finalBatchSize} (target: ${targetBatchSize}, available: ${availableCharges})`); + + return Math.max(1, finalBatchSize); + } + + // Helper function to retry batch until success with exponential backoff + async function sendBatchWithRetry(pixels, regionX, regionY, maxRetries = MAX_BATCH_RETRIES) { + let attempt = 0; + while (attempt < maxRetries && !state.stopFlag) { + attempt++; + console.log( + `🔄 Attempting to send batch (attempt ${attempt}/${maxRetries}) for region ${regionX},${regionY} with ${pixels.length} pixels` + ); + + const result = await sendPixelBatch(pixels, regionX, regionY); + + if (result === true) { + console.log(`✅ Batch succeeded on attempt ${attempt}`); + return true; + } else if (result === 'token_error') { + console.log(`🔑 Token error on attempt ${attempt} - no token available during processing`); + console.log(`❌ Stopping batch processing - tokens must be generated at startup/start button only`); + updateUI('captchaFailed', 'error'); + await Utils.sleep(2000); // Wait longer before retrying after token failure + continue; // Continue to retry until maxRetries reached + } else if (result === 'token_regenerated') { + console.log(`🔄 Token regenerated on attempt ${attempt} after 403 error - retrying batch`); + const pausedX = state.lastPaintedPosition.x; + const pausedY = state.lastPaintedPosition.y; + updateUI('paintingPaused', 'warning', { x: pausedX, y: pausedY }); + // Don't count token regeneration as a failed attempt, retry immediately + attempt--; + await Utils.sleep(500); // Brief pause before retry + continue; + } else if (result === 'token_regeneration_failed') { + console.log(`❌ Token regeneration failed on attempt ${attempt} after 403 error`); + updateUI('captchaFailed', 'error'); + return false; // Stop processing if we can't get a valid token + } else if (result === 'invalid_token_error') { + console.log(`🔑 Invalid token detected on attempt ${attempt}, regenerating...`); + updateUI('captchaSolving', 'warning'); + try { + await handleCaptcha(true); // Allow generation for invalid token cases + // Don't count token regeneration as a failed attempt + attempt--; + continue; + } catch (e) { + console.error(`❌ Token regeneration failed after invalid token on attempt ${attempt}:`, e); + updateUI('captchaFailed', 'error'); + // Wait longer before retrying after token failure + await Utils.sleep(5000); + } + } else { + console.warn(`⚠️ Batch failed on attempt ${attempt}, retrying...`); + // Exponential backoff with jitter + const baseDelay = Math.min(1000 * Math.pow(2, attempt - 1), 30000); // Max 30s + const jitter = Math.random() * 1000; // Add up to 1s random delay + await Utils.sleep(baseDelay + jitter); + } + } + + if (attempt >= maxRetries) { + console.error( + `❌ Batch failed after ${maxRetries} attempts (MAX_BATCH_RETRIES=${MAX_BATCH_RETRIES}). This will stop painting to prevent infinite loops.` + ); + updateUI('paintingError', 'error'); + return false; + } + + return false; + } + + async function sendPixelBatch(pixelBatch, regionX, regionY) { + let token = getTurnstileToken(); + + // Don't auto-generate tokens during processing - return error if no token available + if (!token) { + console.warn('⚠️ No token available and auto-generation disabled during processing'); + return 'token_error'; + } + + const coords = new Array(pixelBatch.length * 2); + const colors = new Array(pixelBatch.length); + for (let i = 0; i < pixelBatch.length; i++) { + const pixel = pixelBatch[i]; + coords[i * 2] = pixel.x; + coords[i * 2 + 1] = pixel.y; + colors[i] = pixel.color; + } + + try { + const payload = { coords, colors, t: token, fp: fpStr32 }; + var wasmtoken = await createWasmToken(regionX, regionY, payload); + const res = await fetch(`https://backend.wplace.live/s0/pixel/${regionX}/${regionY}`, { + method: 'POST', + headers: { 'Content-Type': 'text/plain;charset=UTF-8', "x-pawtect-token": wasmtoken }, + credentials: 'include', + body: JSON.stringify(payload), + }); + + if (res.status === 403) { + let data = null; + try { + 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) { + console.log('✅ Token regenerated after 403 error, returning regenerate signal'); + return 'token_regenerated'; + } else { + console.error('❌ Failed to regenerate token after 403 error'); + return 'token_regeneration_failed'; + } + } + + const data = await res.json(); + return data?.painted === pixelBatch.length; + } catch (e) { + console.error('Batch paint request failed:', e); + return false; + } + } + + function saveBotSettings() { + try { + const settings = { + paintingSpeed: state.paintingSpeed, + paintingSpeedEnabled: document.getElementById('enableSpeedToggle')?.checked, + batchMode: state.batchMode, // "normal" or "random" + paintingOrder: state.paintingOrder, // "sequential" or "color-by-color" + randomBatchMin: state.randomBatchMin, + randomBatchMax: state.randomBatchMax, + cooldownChargeThreshold: state.cooldownChargeThreshold, + tokenSource: state.tokenSource, // "generator", "hybrid", or "manual" + minimized: state.minimized, + overlayOpacity: state.overlayOpacity, + blueMarbleEnabled: document.getElementById('enableBlueMarbleToggle')?.checked, + ditheringEnabled: state.ditheringEnabled, + colorMatchingAlgorithm: state.colorMatchingAlgorithm, + enableChromaPenalty: state.enableChromaPenalty, + chromaPenaltyWeight: state.chromaPenaltyWeight, + customTransparencyThreshold: state.customTransparencyThreshold, + customWhiteThreshold: state.customWhiteThreshold, + paintWhitePixels: state.paintWhitePixels, + paintTransparentPixels: state.paintTransparentPixels, + resizeSettings: state.resizeSettings, + paintUnavailablePixels: state.paintUnavailablePixels, + coordinateMode: state.coordinateMode, + coordinateDirection: state.coordinateDirection, + coordinateSnake: state.coordinateSnake, + blockWidth: state.blockWidth, + blockHeight: state.blockHeight, // Save ignore mask (as base64) with its dimensions + resizeIgnoreMask: + state.resizeIgnoreMask && + state.resizeSettings && + state.resizeSettings.width * state.resizeSettings.height === state.resizeIgnoreMask.length + ? { + w: state.resizeSettings.width, + h: state.resizeSettings.height, + data: btoa(String.fromCharCode(...state.resizeIgnoreMask)), + } + : null, // Notifications + notificationsEnabled: state.notificationsEnabled, + notifyOnChargesReached: state.notifyOnChargesReached, + notifyOnlyWhenUnfocused: state.notifyOnlyWhenUnfocused, + notificationIntervalMinutes: state.notificationIntervalMinutes, + originalImage: state.originalImage, + ditheringMigrated: true, // Migration flag for dithering default change + blueMarbleMigrated: true, // Migration flag for blue marble default change + // NOTE: paintingMode is intentionally NOT saved - always defaults to 'auto' on page load + // Save region and startPosition for assist mode coordinate calculations + region: state.region, + startPosition: state.startPosition, + }; + CONFIG.PAINTING_SPEED_ENABLED = settings.paintingSpeedEnabled; + // AUTO_CAPTCHA_ENABLED is always true - no need to save/load + + localStorage.setItem('wplace-bot-settings', JSON.stringify(settings)); + } catch (e) { + console.warn('Could not save bot settings:', e); + } + } + + function loadBotSettings() { + try { + const saved = localStorage.getItem('wplace-bot-settings'); + if (!saved) return; + const settings = JSON.parse(saved); + + state.paintingSpeed = settings.paintingSpeed || CONFIG.PAINTING_SPEED.DEFAULT; + state.batchMode = settings.batchMode || CONFIG.BATCH_MODE; // Default to "normal" + state.paintingOrder = settings.paintingOrder || CONFIG.PAINTING_ORDER; // Default to "sequential" + state.randomBatchMin = settings.randomBatchMin || CONFIG.RANDOM_BATCH_RANGE.MIN; + state.randomBatchMax = settings.randomBatchMax || CONFIG.RANDOM_BATCH_RANGE.MAX; + state.cooldownChargeThreshold = + settings.cooldownChargeThreshold || CONFIG.COOLDOWN_CHARGE_THRESHOLD; + state.tokenSource = settings.tokenSource || CONFIG.TOKEN_SOURCE; // Default to "generator" + state.minimized = settings.minimized ?? false; + CONFIG.PAINTING_SPEED_ENABLED = settings.paintingSpeedEnabled ?? false; + CONFIG.AUTO_CAPTCHA_ENABLED = settings.autoCaptchaEnabled ?? false; + state.overlayOpacity = settings.overlayOpacity ?? CONFIG.OVERLAY.OPACITY_DEFAULT; + + // MIGRATION: Force enable blue marble for existing users if not explicitly set + // This ensures the new default (blue marble ON) is applied to existing users + if (settings.blueMarbleMigrated !== true) { + state.blueMarbleEnabled = true; + console.log('🔄 Migrated: Blue Marble enabled (new default)'); + // Save the migration flag + settings.blueMarbleMigrated = true; + localStorage.setItem('wplace-bot-settings', JSON.stringify(settings)); + } else { + state.blueMarbleEnabled = settings.blueMarbleEnabled ?? CONFIG.OVERLAY.BLUE_MARBLE_DEFAULT; + } + + // MIGRATION: Force reset dithering to false if not explicitly set in saved settings + // This ensures the new default (dithering OFF) is applied to existing users + if (settings.ditheringMigrated !== true) { + state.ditheringEnabled = false; + console.log('🔄 Migrated: Dithering reset to OFF (new default)'); + // Save the migration flag + settings.ditheringMigrated = true; + localStorage.setItem('wplace-bot-settings', JSON.stringify(settings)); + } else { + state.ditheringEnabled = settings.ditheringEnabled ?? false; + } + + state.colorMatchingAlgorithm = settings.colorMatchingAlgorithm || 'lab'; + state.enableChromaPenalty = settings.enableChromaPenalty ?? true; + state.chromaPenaltyWeight = settings.chromaPenaltyWeight ?? 0.15; + state.customTransparencyThreshold = + settings.customTransparencyThreshold ?? CONFIG.TRANSPARENCY_THRESHOLD; + state.customWhiteThreshold = settings.customWhiteThreshold ?? CONFIG.WHITE_THRESHOLD; + state.paintWhitePixels = settings.paintWhitePixels ?? true; + state.paintTransparentPixels = settings.paintTransparentPixels ?? false; + state.resizeSettings = settings.resizeSettings ?? null; + state.originalImage = settings.originalImage ?? null; + state.paintUnavailablePixels = settings.paintUnavailablePixels ?? CONFIG.PAINT_UNAVAILABLE; + state.coordinateMode = settings.coordinateMode ?? CONFIG.COORDINATE_MODE; + state.coordinateDirection = settings.coordinateDirection ?? CONFIG.COORDINATE_DIRECTION; + state.coordinateSnake = settings.coordinateSnake ?? CONFIG.COORDINATE_SNAKE; + state.blockWidth = settings.blockWidth ?? CONFIG.COORDINATE_BLOCK_WIDTH; + state.blockHeight = settings.blockHeight ?? CONFIG.COORDINATE_BLOCK_HEIGHT; + // Notifications + state.notificationsEnabled = settings.notificationsEnabled ?? CONFIG.NOTIFICATIONS.ENABLED; + state.notifyOnChargesReached = + settings.notifyOnChargesReached ?? CONFIG.NOTIFICATIONS.ON_CHARGES_REACHED; + state.notifyOnlyWhenUnfocused = + settings.notifyOnlyWhenUnfocused ?? CONFIG.NOTIFICATIONS.ONLY_WHEN_UNFOCUSED; + state.notificationIntervalMinutes = + settings.notificationIntervalMinutes ?? CONFIG.NOTIFICATIONS.REPEAT_MINUTES; + // NOTE: paintingMode is NOT restored - always defaults to 'auto' on page load + + // Restore region and startPosition for assist mode coordinate calculations + if (settings.region) { + state.region = settings.region; + console.log('✅ Restored region from save file:', state.region); + } + if (settings.startPosition) { + state.startPosition = settings.startPosition; + console.log('✅ Restored startPosition from save file:', state.startPosition); + } + + // Restore ignore mask if dims match current resizeSettings + if ( + settings.resizeIgnoreMask && + settings.resizeIgnoreMask.data && + state.resizeSettings && + settings.resizeIgnoreMask.w === state.resizeSettings.width && + settings.resizeIgnoreMask.h === state.resizeSettings.height + ) { + try { + const bin = atob(settings.resizeIgnoreMask.data); + const arr = new Uint8Array(bin.length); + for (let i = 0; i < bin.length; i++) arr[i] = bin.charCodeAt(i); + state.resizeIgnoreMask = arr; + } catch { + state.resizeIgnoreMask = null; + } + } else { + state.resizeIgnoreMask = null; + } + // Initialize coordinate generation UI + const coordinateModeSelect = document.getElementById('coordinateModeSelect'); + if (coordinateModeSelect) coordinateModeSelect.value = state.coordinateMode; + + const coordinateDirectionSelect = document.getElementById('coordinateDirectionSelect'); + if (coordinateDirectionSelect) coordinateDirectionSelect.value = state.coordinateDirection; + + const coordinateSnakeToggle = document.getElementById('coordinateSnakeToggle'); + if (coordinateSnakeToggle) coordinateSnakeToggle.checked = state.coordinateSnake; + + const settingsContainer = document.getElementById('wplace-settings-container'); + const directionControls = settingsContainer.querySelector('#directionControls'); + const snakeControls = settingsContainer.querySelector('#snakeControls'); + const blockControls = settingsContainer.querySelector('#blockControls'); + Utils.updateCoordinateUI({ + mode: state.coordinateMode, + directionControls, + snakeControls, + blockControls, + }); + + const paintUnavailablePixelsToggle = document.getElementById('paintUnavailablePixelsToggle'); + if (paintUnavailablePixelsToggle) { + paintUnavailablePixelsToggle.checked = state.paintUnavailablePixels; + } + + const settingsPaintWhiteToggle = settingsContainer.querySelector('#settingsPaintWhiteToggle'); + if (settingsPaintWhiteToggle) { + settingsPaintWhiteToggle.checked = state.paintWhitePixels; + } + + const settingsPaintTransparentToggle = settingsContainer.querySelector( + '#settingsPaintTransparentToggle' + ); + if (settingsPaintTransparentToggle) { + settingsPaintTransparentToggle.checked = state.paintTransparentPixels; + } + + const speedSlider = document.getElementById('speedSlider'); + const speedInput = document.getElementById('speedInput'); + const speedValue = document.getElementById('speedValue'); + if (speedSlider) speedSlider.value = state.paintingSpeed; + if (speedInput) speedInput.value = state.paintingSpeed; + if (speedValue) speedValue.textContent = `pixels`; + + const enableSpeedToggle = document.getElementById('enableSpeedToggle'); + if (enableSpeedToggle) enableSpeedToggle.checked = CONFIG.PAINTING_SPEED_ENABLED; + + // Painting order UI initialization + const paintingOrderSelect = document.getElementById('paintingOrderSelect'); + if (paintingOrderSelect) paintingOrderSelect.value = state.paintingOrder; + + // Batch mode UI initialization + const batchModeSelect = document.getElementById('batchModeSelect'); + if (batchModeSelect) batchModeSelect.value = state.batchMode; + + const normalBatchControls = document.getElementById('normalBatchControls'); + const randomBatchControls = document.getElementById('randomBatchControls'); + + // Show/hide appropriate controls based on batch mode + if (normalBatchControls && randomBatchControls) { + if (state.batchMode === 'random') { + normalBatchControls.style.display = 'none'; + randomBatchControls.style.display = 'block'; + } else { + normalBatchControls.style.display = 'block'; + randomBatchControls.style.display = 'none'; + } + } + + const randomBatchMin = document.getElementById('randomBatchMin'); + if (randomBatchMin) randomBatchMin.value = state.randomBatchMin; + + const randomBatchMax = document.getElementById('randomBatchMax'); + if (randomBatchMax) randomBatchMax.value = state.randomBatchMax; + + // AUTO_CAPTCHA_ENABLED is always true - no toggle to set + + const cooldownSlider = document.getElementById('cooldownSlider'); + const cooldownInput = document.getElementById('cooldownInput'); + const cooldownValue = document.getElementById('cooldownValue'); + if (cooldownSlider) cooldownSlider.value = state.cooldownChargeThreshold; + if (cooldownInput) cooldownInput.value = state.cooldownChargeThreshold; + if (cooldownValue) cooldownValue.textContent = `${Utils.t('charges')}`; + + const overlayOpacitySlider = document.getElementById('overlayOpacitySlider'); + if (overlayOpacitySlider) overlayOpacitySlider.value = state.overlayOpacity; + const overlayOpacityValue = document.getElementById('overlayOpacityValue'); + if (overlayOpacityValue) + overlayOpacityValue.textContent = `${Math.round(state.overlayOpacity * 100)}%`; + const enableBlueMarbleToggle = document.getElementById('enableBlueMarbleToggle'); + if (enableBlueMarbleToggle) enableBlueMarbleToggle.checked = state.blueMarbleEnabled; + + const tokenSourceSelect = document.getElementById('tokenSourceSelect'); + if (tokenSourceSelect) tokenSourceSelect.value = state.tokenSource; + + const colorAlgorithmSelect = document.getElementById('colorAlgorithmSelect'); + if (colorAlgorithmSelect) colorAlgorithmSelect.value = state.colorMatchingAlgorithm; + const enableChromaPenaltyToggle = document.getElementById('enableChromaPenaltyToggle'); + if (enableChromaPenaltyToggle) enableChromaPenaltyToggle.checked = state.enableChromaPenalty; + const chromaPenaltyWeightSlider = document.getElementById('chromaPenaltyWeightSlider'); + if (chromaPenaltyWeightSlider) chromaPenaltyWeightSlider.value = state.chromaPenaltyWeight; + const chromaWeightValue = document.getElementById('chromaWeightValue'); + if (chromaWeightValue) chromaWeightValue.textContent = state.chromaPenaltyWeight; + const transparencyThresholdInput = document.getElementById('transparencyThresholdInput'); + if (transparencyThresholdInput) + transparencyThresholdInput.value = state.customTransparencyThreshold; + const whiteThresholdInput = document.getElementById('whiteThresholdInput'); + if (whiteThresholdInput) whiteThresholdInput.value = state.customWhiteThreshold; + // Notifications UI + const notifEnabledToggle = document.getElementById('notifEnabledToggle'); + if (notifEnabledToggle) notifEnabledToggle.checked = state.notificationsEnabled; + const notifOnChargesToggle = document.getElementById('notifOnChargesToggle'); + if (notifOnChargesToggle) notifOnChargesToggle.checked = state.notifyOnChargesReached; + const notifOnlyUnfocusedToggle = document.getElementById('notifOnlyUnfocusedToggle'); + if (notifOnlyUnfocusedToggle) + notifOnlyUnfocusedToggle.checked = state.notifyOnlyWhenUnfocused; + const notifIntervalInput = document.getElementById('notifIntervalInput'); + if (notifIntervalInput) notifIntervalInput.value = state.notificationIntervalMinutes; + NotificationManager.resetEdgeTracking(); + } catch (e) { + console.warn('Could not load bot settings:', e); + } + } + + // Initialize Turnstile generator integration + console.log('🚀 WPlace Auto-Image with Turnstile Token Generator loaded'); + console.log('🔑 Turnstile token generator: ALWAYS ENABLED (Background mode)'); + console.log('🎯 Manual pixel captcha solving: Available as fallback/alternative'); + console.log('📱 Turnstile widgets: DISABLED - pure background token generation only!'); + + // Generate fingerprint string for requests + if (!window.WPlaceTokenManager) { + console.error('❌ WPlaceTokenManager not available - dependency loading issue'); + return; + } + const fpStr32 = tokenManager._randStr(32); + console.log('🔑 Generated fingerprint string for API requests'); + + // Function to enable file operations after initial startup setup is complete + function enableFileOperations() { + state.initialSetupComplete = true; + + const loadBtn = document.querySelector('#loadBtn'); + const loadFromFileBtn = document.querySelector('#loadFromFileBtn'); + const uploadBtn = document.querySelector('#uploadBtn'); + + if (loadBtn) { + loadBtn.disabled = false; + loadBtn.title = ''; + // Add a subtle animation to indicate the button is now available + loadBtn.style.animation = 'pulse 0.6s ease-in-out'; + setTimeout(() => { + if (loadBtn) loadBtn.style.animation = ''; + }, 600); + console.log('✅ Load Progress button enabled after initial setup'); + } + + if (loadFromFileBtn) { + loadFromFileBtn.disabled = false; + loadFromFileBtn.title = ''; + // Add a subtle animation to indicate the button is now available + loadFromFileBtn.style.animation = 'pulse 0.6s ease-in-out'; + setTimeout(() => { + if (loadFromFileBtn) loadFromFileBtn.style.animation = ''; + }, 600); + console.log('✅ Load from File button enabled after initial setup'); + } + + if (uploadBtn) { + uploadBtn.disabled = false; + uploadBtn.title = ''; + // Add a subtle animation to indicate the button is now available + uploadBtn.style.animation = 'pulse 0.6s ease-in-out'; + setTimeout(() => { + if (uploadBtn) uploadBtn.style.animation = ''; + }, 600); + 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'); + } + + // Optimized token initialization with better timing and error handling + async function initializeTokenGenerator() { + // Skip if already have valid token + if (isTokenValid()) { + console.log('✅ Valid token already available, skipping initialization'); + updateUI('tokenReady', 'success'); + enableFileOperations(); // Enable file operations since initial setup is complete + return; + } + + try { + console.log('🔧 Initializing Turnstile token generator...'); + updateUI('initializingToken', 'default'); + + console.log('Attempting to load Turnstile script...'); + await Utils.loadTurnstile(); + console.log('Turnstile script loaded. Attempting to generate token...'); + + // Use TokenManager's handleCaptchaWithRetry method instead + const token = await tokenManager.handleCaptchaWithRetry(); + if (token) { + setTurnstileToken(token); + console.log('✅ Startup token generated successfully'); + updateUI('tokenReady', 'success'); + Utils.showAlert(Utils.t('tokenGeneratorReady'), 'success'); + enableFileOperations(); // Enable file operations since initial setup is complete + } else { + console.warn( + '⚠️ Startup token generation failed (no token received), will retry when needed' + ); + updateUI('tokenRetryLater', 'warning'); + // Still enable file operations even if initial token generation fails + // Users can load progress and use manual/hybrid modes + enableFileOperations(); + } + } catch (error) { + console.error('❌ Critical error during Turnstile initialization:', error); // More specific error + updateUI('tokenRetryLater', 'warning'); + // Still enable file operations even if initial setup fails + // Users can load progress and use manual/hybrid modes + enableFileOperations(); + // Don't show error alert for initialization failures, just log them + } + } + + // Load theme preference immediately on startup before creating UI + loadThemePreference(); + applyTheme(); + + var pawtect_chunk = null; + + //find module if pawtect_chunk is null + pawtect_chunk ??= await findTokenModule("pawtect_wasm_bg.wasm"); + + async function createWasmToken(regionX, regionY, payload) { + try { + // Load the Pawtect module and WASM + const mod = await import(new URL('/_app/immutable/chunks/' + pawtect_chunk, location.origin).href); + let wasm; + try { + wasm = await mod._(); + console.log('✅ WASM initialized successfully'); + } catch (wasmError) { + console.error('❌ WASM initialization failed:', wasmError); + return null; + } + try { + try { + const me = await fetch(`https://backend.wplace.live/me`, { credentials: 'include' }).then(r => r.ok ? r.json() : null); + if (me?.id) { + mod.i(me.id); + console.log('✅ user ID set:', me.id); + } + } catch { } + } catch (userIdError) { + console.log('⚠️ Error setting user ID:', userIdError.message); + } + try { + const testUrl = `https://backend.wplace.live/s0/pixel/${regionX}/${regionY}`; + if (mod.r) { + mod.r(testUrl); + console.log('✅ Request URL set:', testUrl); + } else { + console.log('⚠️ request_url function (mod.r) not available'); + } + } catch (urlError) { + console.log('⚠️ Error setting request URL:', urlError.message); + } + + // Create test payload + + console.log('📝 payload:', payload); + + // Encode payload + const enc = new TextEncoder(); + const dec = new TextDecoder(); + const bodyStr = JSON.stringify(payload); + const bytes = enc.encode(bodyStr); + console.log('📏 Payload size:', bytes.length, 'bytes'); + console.log('📄 Payload string:', bodyStr); + + // Allocate WASM memory with validation + let inPtr; + try { + if (!wasm.__wbindgen_malloc) { + console.error('❌ __wbindgen_malloc function not found'); + return null; + } + + inPtr = wasm.__wbindgen_malloc(bytes.length, 1); + console.log('✅ WASM memory allocated, pointer:', inPtr); + + // Copy data to WASM memory + const wasmBuffer = new Uint8Array(wasm.memory.buffer, inPtr, bytes.length); + wasmBuffer.set(bytes); + console.log('✅ Data copied to WASM memory'); + } catch (memError) { + console.error('❌ Memory allocation error:', memError); + return null; + } + + // Call the WASM function + console.log('🚀 Calling get_pawtected_endpoint_payload...'); + let outPtr, outLen, token; + try { + const result = wasm.get_pawtected_endpoint_payload(inPtr, bytes.length); + console.log('✅ Function called, result type:', typeof result, result); + + if (Array.isArray(result) && result.length === 2) { + [outPtr, outLen] = result; + console.log('✅ Got output pointer:', outPtr, 'length:', outLen); + + // Decode the result + const outputBuffer = new Uint8Array(wasm.memory.buffer, outPtr, outLen); + token = dec.decode(outputBuffer); + console.log('✅ Token decoded successfully'); + } else { + console.error('❌ Unexpected function result format:', result); + return null; + } + } catch (funcError) { + console.error('❌ Function call error:', funcError); + console.error('Stack trace:', funcError.stack); + return null; + } + + // Cleanup memory + try { + if (wasm.__wbindgen_free && outPtr && outLen) { + wasm.__wbindgen_free(outPtr, outLen, 1); + console.log('✅ Output memory freed'); + } + if (wasm.__wbindgen_free && inPtr) { + wasm.__wbindgen_free(inPtr, bytes.length, 1); + console.log('✅ Input memory freed'); + } + } catch (cleanupError) { + console.log('⚠️ Cleanup warning:', cleanupError.message); + } + + // Display results + console.log(''); + console.log('🎉 SUCCESS!'); + console.log('🔑 Full token:'); + console.log(token); + return token; + } catch (error) { + console.error('❌ Failed to generate fp parameter:', error); + return null; + } + } + + async function findTokenModule(str) { + console.log('🔎 Searching for wasm Module...'); + const links = Array.from(document.querySelectorAll('link[rel="modulepreload"][href$=".js"]')); + + for (const link of links) { + try { + const url = new URL(link.getAttribute("href"), location.origin).href; + const code = await fetch(url).then(r => r.text()); + if (code.includes(str)) { + console.log('✅ Found wasm Module...'); + return url.split('/').pop(); + } + } catch { } + } + console.error(`❌ Could not find Pawtect chunk: `, error); + } + + async function purchase(type) { + // loadThemePreference() + let id; + let chargeMultiplier; + if (type === "max_charges") { + id = 70; + chargeMultiplier = 5; + } else if (type === "paint_charges") { + id = 80; + chargeMultiplier = 30; + } else { + console.error("Error: Invalid purchase type provided."); + return; + } + const { droplets } = await WPlaceService.getCharges(); + console.log("There are currently : ", droplets, "droplets."); + try { + const amounts = Math.floor(droplets / 500); + if (amounts < 1) { + console.log("Not enough droplets to purchase."); + return; + } + const payload = { + "product": { + "id": id, + "amount": amounts + } + }; + const res = await fetch("https://backend.wplace.live/purchase", { + method: "POST", + headers: { + "Content-Type": "text/plain;charset=UTF-8" + }, + body: JSON.stringify(payload), + credentials: "include" + }); + // POST request completed + const { droplets: newDroplets } = await WPlaceService.getCharges(); + if (droplets != newDroplets) { + console.log("Successfully bought", amounts * chargeMultiplier, type.replace('_', ' '), "."); + return 2; + } else { + console.log("Failed to buy charges"); + return 1; + } + } catch (e) { + console.error("An error occurred during the purchase:", e); + return 0; + } + } + 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) { + console.error('❌ Failed to send setCookie message:', error); + return false; + } + } + async function getAccounts() { + return new Promise((resolve, reject) => { + console.log("Requesting accounts from extension..."); + + // Ask extension for accounts + window.postMessage({ + source: "my-userscript", + type: "getAccounts" + }, "*"); + + function handler(event) { + if (event.source !== window) return; + if (event.data.source !== "extension") return; + if (event.data.type === "accountsData") { + // Remove listener when we get the response + window.removeEventListener("message", handler); + + try { + localStorage.setItem("accounts", JSON.stringify(event.data.accounts)); + console.log("✅ Accounts saved to localStorage:", event.data.accounts); + } catch (e) { + console.error("❌ Failed to save accounts:", e); + } + resolve(event.data.accounts); + } + } + window.addEventListener("message", handler); + }); + } + + async function fetchAccount() { + const { ID, Charges, Max, Droplets } = await WPlaceService.fetchCheck(); + console.log("User's ID :", ID); + console.log("User's Charges :", Charges, "/", Max); + console.log("User's Droplets :", Droplets); + } + + async function fetchAllAccountDetails() { + if (state.isFetchingAllAccounts) { + Utils.showAlert("Already fetching account details.", "warning"); + return; + } + + state.isFetchingAllAccounts = true; + + const refreshBtn = document.getElementById('refreshAllAccountsBtn'); + if (refreshBtn) { + refreshBtn.innerHTML = ''; + refreshBtn.disabled = true; + } + + const accountsListArea = document.getElementById('accountsListArea'); + if (accountsListArea) { + accountsListArea.innerHTML = `
Loading accounts...
`; + } + + try { + // First, get accounts from the extension + console.log(`🔄 [FETCH] Requesting accounts from extension...`); + try { + await getAccounts(); + console.log(`✅ [FETCH] Successfully retrieved accounts from extension`); + } catch (error) { + console.warn(`⚠️ [FETCH] Failed to get accounts from extension:`, error); + // Continue anyway in case we have cached accounts + } + + // Load accounts using the new AccountManager + await accountManager.loadAccounts(); + + console.log(`✅ [FETCH] Loaded ${accountManager.getAccountCount()} accounts from storage`); + + // Debug: Check if we actually have accounts + if (accountManager.getAccountCount() === 0) { + console.warn(`⚠️ [FETCH] No accounts found in storage. Check localStorage 'accounts' and chrome.storage 'infoAccounts'`); + + // Check localStorage for accounts + const localStorageAccounts = JSON.parse(localStorage.getItem("accounts")) || []; + console.log(`📋 [DEBUG] localStorage accounts:`, localStorageAccounts); + + // Render empty state and return early + renderAccountsList(); + return; + } + + // Now fetch fresh data for each account + const accounts = accountManager.getAllAccounts(); + if (accounts.length > 0) { + console.log(`🔄 [FETCH] Fetching fresh data for ${accounts.length} accounts...`); + + // Remember the current account so we can switch back + const originalCurrentAccount = accounts.find(acc => acc.isCurrent); + + for (let i = 0; i < accounts.length; i++) { + const account = accounts[i]; + console.log(`📊 [FETCH] Fetching data for account ${i + 1}: ${account.displayName}`); + + try { + // Switch to this account temporarily to fetch its data + console.log(`🔄 [FETCH] Switching to ${account.displayName} to fetch fresh data...`); + await switchToSpecificAccount(account.token, account.displayName); + // await Utils.sleep(500); // Small delay to ensure switch takes effect + + // Fetch fresh account details + const accountData = await WPlaceService.getCharges(); + const accountInfo = await WPlaceService.fetchCheck(); + + // Update account with fresh data + accountManager.updateAccountData(account.token, { + ID: accountData.id || accountInfo.ID, + Charges: Math.floor(accountData.charges || 0), + Max: Math.floor(accountData.max || 0), + Droplets: Math.floor(accountData.droplets || 0), + displayName: accountInfo.Username || accountInfo.name || account.displayName + }); + + console.log(`✅ [FETCH] Updated ${account.displayName}: ⚡${Math.floor(accountData.charges)}/${Math.floor(accountData.max)} 💧${Math.floor(accountData.droplets)}`); + + } catch (error) { + console.warn(`⚠️ [FETCH] Failed to fetch data for ${account.displayName}:`, error); + } + } + + // Switch back to the original current account if there was one + if (originalCurrentAccount) { + console.log(`🔙 [FETCH] Switching back to original current account: ${originalCurrentAccount.displayName}`); + try { + await switchToSpecificAccount(originalCurrentAccount.token, originalCurrentAccount.displayName); + //await Utils.sleep(300); + + // Mark it as current again + accountManager.updateAccountData(originalCurrentAccount.token, { + isCurrent: true + }); + } catch (error) { + console.warn(`⚠️ [FETCH] Failed to switch back to original account:`, error); + } + } + + console.log(`🎯 [FETCH] Completed fetching fresh data for all accounts`); + } + + // Render the accounts list with fresh data + renderAccountsList(); + + } catch (error) { + console.error('❌ [FETCH] Error fetching account details:', error); + if (accountsListArea) { + accountsListArea.innerHTML = `
Error loading accounts.
`; + } + } finally { + state.isFetchingAllAccounts = false; + if (refreshBtn) { + refreshBtn.innerHTML = ''; + refreshBtn.disabled = false; + } + } + } + + // Function to update current account charges in the account list + async function updateCurrentAccountInList() { + if (accountManager.getAccountCount() === 0) return; + + // Find current account in the list and update its charges + const currentAccount = accountManager.getCurrentAccount(); + if (currentAccount) { + const { charges, cooldown, droplets } = await WPlaceService.getCharges(); + state.displayCharges = Math.floor(charges); + state.preciseCurrentCharges = charges; + await updateStats(); + + // Update the current account data in AccountManager + accountManager.updateAccountData(currentAccount.token, { + Charges: Math.floor(state.displayCharges || state.preciseCurrentCharges || 0), + Max: state.maxCharges, + Droplets: Math.floor(droplets) + }); + + // Re-render the account list to show updated charges + renderAccountsList(); + } + } + + // Function to update current account spotlight when switching during painting + async function updateCurrentAccountSpotlight() { + if (accountManager.getAccountCount() === 0) return; + // await Utils.sleep(500); // Wait a bit for the switch to take effect + try { + const currentAccountData = await WPlaceService.getCharges(); + console.log("Current account after switch:", currentAccountData); + console.log(`🔍 Switched to account with ID: ${currentAccountData.id}`); + + // Find the current account in AccountManager and update it + const accounts = accountManager.getAllAccounts(); + const currentAccount = accounts.find(acc => acc.ID === currentAccountData.id); + + if (currentAccount) { + const currentAccountInfo = await WPlaceService.fetchCheck(); + + // Update account data in AccountManager + accountManager.updateAccountData(currentAccount.token, { + isCurrent: true, + Charges: Math.floor(currentAccountData.charges), + Max: Math.floor(currentAccountData.max), + Droplets: Math.floor(currentAccountData.droplets), + displayName: currentAccountInfo.Username || currentAccountInfo.name || currentAccount.displayName + }); + + console.log(`🎯 Updated current account spotlight: ${currentAccount.displayName}`); + + // Check for autobuy after account is loaded + if (CONFIG.autoBuyToggle && Math.floor(currentAccountData.droplets) > 500) { + console.log(`💰 Account has ${Math.floor(currentAccountData.droplets)} droplets (>500), triggering autobuy...`); + try { + const purchaseResult = await purchase(CONFIG.autoBuy); + if (purchaseResult == 2) { + console.log('✅ Autobuy successful after account switch'); + // Update charges after purchase + const updatedCharges = await WPlaceService.getCharges(); + accountManager.updateAccountData(currentAccount.token, { + Charges: Math.floor(updatedCharges.charges), + Droplets: Math.floor(updatedCharges.droplets) + }); + // Re-render account list to show updated status after purchase + renderAccountsList(); + } else { + console.log('❌ Autobuy failed after account switch'); + } + } catch (error) { + console.error('❌ Error during autobuy after account switch:', error); + } + } else if (CONFIG.autoBuyToggle) { + console.log(`💰 Account has ${Math.floor(currentAccountData.droplets)} droplets (≤500), skipping autobuy`); + } + + // Re-render the account list to show new current account + renderAccountsList(); + + console.log(`🔒 PRESERVING currentActiveIndex: ${state.currentActiveIndex} (do not recalculate from isCurrent flag)`); + console.log(`� Account at currentActiveIndex ${state.currentActiveIndex}: ${state.originalAccountOrder[state.currentActiveIndex]?.displayName} (ID ${state.originalAccountOrder[state.currentActiveIndex]?.orderId})`); + + // Update accountIndex to match original array position + const originalArrayIndex = state.allAccountsInfo.findIndex(acc => acc.isCurrent); + if (originalArrayIndex !== -1) { + state.accountIndex = originalArrayIndex; + } + + console.log(`📊 Final state: activeIndex=${state.currentActiveIndex}, accountIndex=${state.accountIndex}, orderId=${newCurrentAccount.orderId}`); + + console.warn(`⚠️ Could not find account ID ${newCurrentAccount.orderId} in switching order`); + + console.warn(`⚠️ Could not find switched account with ID ${currentAccountData.id} in account list`); + } + + // Re-render the account list to show new current account + renderAccountsList(); + + } catch (error) { + console.warn('⚠️ Failed to update account spotlight:', error); + } + } + + function renderAccountsList() { + const accountsListArea = document.getElementById('accountsListArea'); + if (!accountsListArea) return; + + accountsListArea.innerHTML = ''; + + const accounts = accountManager.getAllAccounts(); + console.log(`🔍 [RENDER] Rendering ${accounts.length} accounts`); + + // Debug: Log account data to see what we're working with + accounts.forEach((account, index) => { + console.log(`📊 [RENDER] Account ${index + 1}: ${account.displayName} - ⚡${account.Charges}/${account.Max} 💧${account.Droplets} ${account.isCurrent ? '(CURRENT)' : ''}`); + }); + + if (accounts.length === 0) { + // Don't show any placeholder - just leave empty + console.log(`📝 [RENDER] No accounts to display`); + return; + } + + accounts.forEach((account, index) => { + const item = createAccountItem(account, index); + accountsListArea.appendChild(item); + }); + + console.log(`✅ [RENDER] Successfully rendered ${accounts.length} accounts`); + } + + function createAccountItem(account, index) { + const item = document.createElement('div'); + + // Determine if this is the current account + const isCurrentAccount = account.isCurrent; + const isNextInSequence = accountManager.currentIndex !== -1 && + ((accountManager.currentIndex + 1) % accountManager.getAccountCount()) === index; + + let itemClasses = 'wplace-account-item'; + if (isCurrentAccount) { + itemClasses += ' current'; + } else if (isNextInSequence) { + itemClasses += ' next-in-sequence'; + } + + item.className = itemClasses; + + // Create ordering number element with 1-based index for user display + const orderNumber = document.createElement('div'); + orderNumber.className = 'wplace-account-number'; + orderNumber.textContent = index + 1; // Show 1-based index for user-friendly display + + // 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)'; + } else if (isNextInSequence) { + orderNumber.style.background = '#f39c12'; // Orange color for next + orderNumber.style.color = 'white'; + orderNumber.style.boxShadow = '0 0 8px rgba(243, 156, 18, 0.6)'; + } + + // Create account details + const details = document.createElement('div'); + details.className = 'wplace-account-details'; + + const accountName = document.createElement('div'); + accountName.className = 'wplace-account-name'; + const displayName = account.displayName || account.name || `Account ${index + 1}`; + accountName.textContent = displayName; + accountName.title = displayName; + + const accountStats = document.createElement('div'); + accountStats.className = 'wplace-account-stats'; + + // Safely handle undefined values + const charges = account.Charges !== undefined ? Math.floor(account.Charges) : 0; + const max = account.Max !== undefined ? Math.floor(account.Max) : 0; + const droplets = account.Droplets !== undefined ? Math.floor(account.Droplets) : 0; + + accountStats.innerHTML = ` + ${charges}/${max} + ${droplets} + `; + + details.appendChild(accountName); + details.appendChild(accountStats); + + // Add status indicators + const status = document.createElement('div'); + status.className = 'wplace-account-status'; + if (isCurrentAccount) { + status.innerHTML = ''; + } else if (isNextInSequence) { + status.innerHTML = ''; + } + + item.appendChild(orderNumber); + item.appendChild(details); + item.appendChild(status); + + return item; + } + + // SIMPLIFIED ACCOUNT SWITCHING + async function switchToNextAccount(accounts) { + console.log(`🔄 [SWITCH] Starting account switch`); + + // Validate we have accounts + if (accountManager.getAccountCount() === 0) { + console.error('❌ No accounts available for switching'); + return false; + } + + // Get next account using simplified manager + const nextAccount = accountManager.switchToNext(); + if (!nextAccount) { + console.error('❌ Failed to get next account'); + return false; + } + + console.log(`🎯 [SWITCH] Switching to: ${nextAccount.displayName} (index ${accountManager.currentIndex})`); + + // Ensure token exists + if (!nextAccount.token) { + console.error(`❌ Missing token for account: ${nextAccount.displayName}`); + return false; + } + + console.log(`� [SWITCH] Using token: ${nextAccount.token.substring(0, 20)}...`); + + // Perform the account switch + try { + await swapAccountTrigger(nextAccount.token); + + // Update account index for backward compatibility + state.accountIndex = accountManager.currentIndex; + + console.log(`✅ [SWITCH] Successfully switched to ${nextAccount.displayName}`); + + // Wait a moment for the switch to fully complete + // await new Promise(resolve => setTimeout(resolve, 1000)); + + // Update the account status and UI after successful switch + await updateCurrentAccountInList(); + + return true; + } catch (error) { + console.error('❌ [SWITCH] Account switch failed:', error); + return false; + } + } + + // SIMPLIFIED helper function for specific account switching + async function switchToSpecificAccount(token, accountName) { + console.log(`🔄 [SPECIFIC SWITCH] Attempting to switch to account: ${accountName}`); + console.log(`🔑 [SPECIFIC SWITCH] Using token: ${token.substring(0, 20)}...`); + + await swapAccountTrigger(token); + + let maxRetries = 20; + let retryCount = 0; + let swapSuccess = false; + + while (!swapSuccess && retryCount < maxRetries) { + console.log(`⏳ [SPECIFIC SWITCH] Waiting for account swap... (Attempt ${retryCount + 1}/${maxRetries})`); + //await new Promise(resolve => setTimeout(resolve, 1000)); + + try { + // await fetchAccount(); + console.log('✅ [SPECIFIC SWITCH] Account swap confirmed.'); + swapSuccess = true; + } catch (error) { + console.warn('❌ [SPECIFIC SWITCH] Account swap not yet successful. Retrying...', error); + retryCount++; + + if (retryCount % 5 === 0) { + console.log('🔄 [SPECIFIC SWITCH] Re-triggering account swap...'); + 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; + Utils.performSmartSave(); + await updateStats(); + + // Update account data in manager + accountManager.updateAccountData({ charges, cooldown }); + + // Update the account status and UI after successful switch + await updateCurrentAccountSpotlight(); + + console.log(`✅ [SPECIFIC SWITCH] Successfully switched to ${accountName} with ${Math.floor(charges)} charges`); + return true; + } else { + console.error(`❌ [SPECIFIC SWITCH] Failed to swap to ${accountName} after multiple retries.`); + return false; + } + } + + // Wait for dependencies before initializing UI + async function waitForDependenciesAndInitialize() { + // 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, + imageProcessor: window.globalImageProcessor, + 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:', { + utilsManager: !!window.globalUtilsManager, + imageProcessor: !!window.globalImageProcessor, + overlayManager: !!window.globalOverlayManager, + tokenManager: !!window.globalTokenManager + }); + } + + return createUI(); + } + + waitForDependenciesAndInitialize().then(() => { + // Generate token automatically after UI is ready + setTimeout(initializeTokenGenerator, 1000); + + // Quick initial account load from cache + setTimeout(async () => { + console.log('🔄 Initial account load from cache...'); + try { + await accountManager.loadAccounts(); + if (accountManager.getAccountCount() > 0) { + console.log(`✅ Loaded ${accountManager.getAccountCount()} cached accounts`); + renderAccountsList(); + } else { + console.log('📭 No cached accounts found'); + } + } catch (error) { + console.warn('⚠️ Initial account load failed:', error); + } + }, 500); + + // Auto-refresh account list on startup (with extension communication) + setTimeout(() => { + console.log('🔄 Auto-refreshing account list on startup...'); + fetchAllAccountDetails(); + }, 2000); + + // Attach advanced color matching listeners (resize dialog) + const advancedInit = () => { + const chromaSlider = document.getElementById('chromaPenaltyWeightSlider'); + const chromaValue = document.getElementById('chromaWeightValue'); + const resetBtn = document.getElementById('resetAdvancedColorBtn'); + const algoSelect = document.getElementById('colorAlgorithmSelect'); + const chromaToggle = document.getElementById('enableChromaPenaltyToggle'); + const transInput = document.getElementById('transparencyThresholdInput'); + const whiteInput = document.getElementById('whiteThresholdInput'); + const ditherToggle = document.getElementById('enableDitheringToggle'); + + // Ensure dithering checkbox matches state (explicit sync on init) + if (ditherToggle) { + ditherToggle.checked = state.ditheringEnabled; + console.log(`🎨 Dithering initialized: ${state.ditheringEnabled ? 'ON' : 'OFF'}`); + } + + if (algoSelect) + algoSelect.addEventListener('change', (e) => { + state.colorMatchingAlgorithm = e.target.value; + saveBotSettings(); + _updateResizePreview(); + }); + if (chromaToggle) + chromaToggle.addEventListener('change', (e) => { + state.enableChromaPenalty = e.target.checked; + saveBotSettings(); + _updateResizePreview(); + }); + if (chromaSlider && chromaValue) + chromaSlider.addEventListener('input', (e) => { + state.chromaPenaltyWeight = parseFloat(e.target.value) || 0.15; + chromaValue.textContent = state.chromaPenaltyWeight.toFixed(2); + saveBotSettings(); + _updateResizePreview(); + }); + if (transInput) + transInput.addEventListener('change', (e) => { + const v = parseInt(e.target.value, 10); + if (!isNaN(v) && v >= 0 && v <= 255) { + state.customTransparencyThreshold = v; + CONFIG.TRANSPARENCY_THRESHOLD = v; + saveBotSettings(); + _updateResizePreview(); + } + }); + if (whiteInput) + whiteInput.addEventListener('change', (e) => { + const v = parseInt(e.target.value, 10); + if (!isNaN(v) && v >= 200 && v <= 255) { + state.customWhiteThreshold = v; + CONFIG.WHITE_THRESHOLD = v; + saveBotSettings(); + _updateResizePreview(); + } + }); + if (ditherToggle) + ditherToggle.addEventListener('change', (e) => { + state.ditheringEnabled = e.target.checked; + saveBotSettings(); + _updateResizePreview(); + }); + if (resetBtn) + resetBtn.addEventListener('click', () => { + state.colorMatchingAlgorithm = 'lab'; + state.enableChromaPenalty = true; + state.chromaPenaltyWeight = 0.15; + state.customTransparencyThreshold = CONFIG.TRANSPARENCY_THRESHOLD = 100; + state.customWhiteThreshold = CONFIG.WHITE_THRESHOLD = 250; + saveBotSettings(); + const a = document.getElementById('colorAlgorithmSelect'); + if (a) a.value = 'lab'; + const ct = document.getElementById('enableChromaPenaltyToggle'); + if (ct) ct.checked = true; + if (chromaSlider) chromaSlider.value = 0.15; + if (chromaValue) chromaValue.textContent = '0.15'; + if (transInput) transInput.value = 100; + if (whiteInput) whiteInput.value = 250; + _updateResizePreview(); + Utils.showAlert(Utils.t('advancedColorSettingsReset'), 'success'); + }); + }; + // Delay to ensure resize UI built + setTimeout(advancedInit, 500); + + // Add cleanup on page unload + window.addEventListener('beforeunload', () => { + Utils.cleanupTurnstile(); + }); + }); +})(); + +// === Async coordinate generation (off-main-thread) === +async function generateCoordinatesAsync( + width, + height, + mode, + direction, + snake, + blockWidth, + blockHeight, + startFromX = 0, + startFromY = 0 +) { + try { + if (typeof Worker === 'undefined') { + // Environment does not support Web Workers – fall back + return generateCoordinates( + width, + height, + mode, + direction, + snake, + blockWidth, + blockHeight, + startFromX, + startFromY + ); + } + + const workerCode = `self.onmessage = function(e) { + const { width, height, mode, direction, snake, blockWidth, blockHeight, startFromX, startFromY } = e.data || {}; + + function generate() { + let coords = []; + + // Determine traversal direction + let xStart, xEnd, xStep; + let yStart, yEnd, yStep; + switch (direction) { + case 'top-left': + xStart = 0; xEnd = width; xStep = 1; + yStart = 0; yEnd = height; yStep = 1; + break; + case 'top-right': + xStart = width - 1; xEnd = -1; xStep = -1; + yStart = 0; yEnd = height; yStep = 1; + break; + case 'bottom-left': + xStart = 0; xEnd = width; xStep = 1; + yStart = height - 1; yEnd = -1; yStep = -1; + break; + case 'bottom-right': + xStart = width - 1; xEnd = -1; xStep = -1; + yStart = height - 1; yEnd = -1; yStep = -1; + break; + default: + throw new Error('Unknown direction: ' + direction); + } + + if (mode === 'rows') { + let rowIndex = 0; + for (let y = yStart; y !== yEnd; y += yStep) { + if (snake && rowIndex % 2 !== 0) { + for (let x = xEnd - xStep; x !== xStart - xStep; x -= xStep) { + coords.push([x, y]); + } + } else { + for (let x = xStart; x !== xEnd; x += xStep) { + coords.push([x, y]); + } + } + rowIndex++; + } + } else if (mode === 'columns') { + let colIndex = 0; + for (let x = xStart; x !== xEnd; x += xStep) { + if (snake && colIndex % 2 !== 0) { + for (let y = yEnd - yStep; y !== yStart - yStep; y -= yStep) { + coords.push([x, y]); + } + } else { + for (let y = yStart; y !== yEnd; y += yStep) { + coords.push([x, y]); + } + } + colIndex++; + } + } else if (mode === 'circle-out') { + const cx = Math.floor(width / 2); + const cy = Math.floor(height / 2); + const maxRadius = Math.ceil(Math.sqrt(cx * cx + cy * cy)); + for (let r = 0; r <= maxRadius; r++) { + for (let y = cy - r; y <= cy + r; y++) { + for (let x = cx - r; x <= cx + r; x++) { + if (x >= 0 && x < width && y >= 0 && y < height) { + const dist = Math.max(Math.abs(x - cx), Math.abs(y - cy)); + if (dist === r) coords.push([x, y]); + } + } + } + } + } else if (mode === 'circle-in') { + const cx = Math.floor(width / 2); + const cy = Math.floor(height / 2); + const maxRadius = Math.ceil(Math.sqrt(cx * cx + cy * cy)); + for (let r = maxRadius; r >= 0; r--) { + for (let y = cy - r; y <= cy + r; y++) { + for (let x = cx - r; x <= cx + r; x++) { + if (x >= 0 && x < width && y >= 0 && y < height) { + const dist = Math.max(Math.abs(x - cx), Math.abs(y - cy)); + if (dist === r) coords.push([x, y]); + } + } + } + } + } else if (mode === 'blocks' || mode === 'shuffle-blocks') { + const blocks = []; + for (let by = 0; by < height; by += blockHeight) { + for (let bx = 0; bx < width; bx += blockWidth) { + const block = []; + for (let y = by; y < Math.min(by + blockHeight, height); y++) { + for (let x = bx; x < Math.min(bx + blockWidth, width); x++) { + block.push([x, y]); + } + } + blocks.push(block); + } + } + if (mode === 'shuffle-blocks') { + for (let i = blocks.length - 1; i > 0; i--) { + const j = Math.floor(Math.random() * (i + 1)); + const tmp = blocks[i]; + blocks[i] = blocks[j]; + blocks[j] = tmp; + } + } + for (const block of blocks) { + for (let i = 0; i < block.length; i++) { + coords.push(block[i]); + } + } + } else { + throw new Error('Unknown mode: ' + mode); + } + + // Resume from specified position if provided + if (startFromX > 0 || startFromY > 0) { + let startIndex = -1; + for (let i = 0; i < coords.length; i++) { + const c = coords[i]; + if (c[0] === startFromX && c[1] === startFromY) { + startIndex = i; + break; + } + } + if (startIndex >= 0) { + coords = coords.slice(startIndex); + } + } + + return coords; + } + + try { + const coords = generate(); + self.postMessage({ ok: true, coords }); + } catch (err) { + self.postMessage({ ok: false, error: err && (err.message || String(err)) }); + } +};`; + + const blob = new Blob([workerCode], { type: 'application/javascript' }); + const url = URL.createObjectURL(blob); + + return await new Promise((resolve) => { + const worker = new Worker(url); + const cleanup = () => { + URL.revokeObjectURL(url); + try { worker.terminate(); } catch (e) { } + }; + + worker.onmessage = (e) => { + const { ok, coords, error } = e.data || {}; + cleanup(); + if (ok && Array.isArray(coords)) { + resolve(coords); + } else { + console.warn('Coordinate worker failed, falling back to sync:', error); + resolve( + generateCoordinates( + width, + height, + mode, + direction, + snake, + blockWidth, + blockHeight, + startFromX, + startFromY + ) + ); + } + }; + + worker.onerror = (err) => { + console.warn('Coordinate worker error, falling back to sync:', err && (err.message || err)); + cleanup(); + resolve( + generateCoordinates( + width, + height, + mode, + direction, + snake, + blockWidth, + blockHeight, + startFromX, + startFromY + ) + ); + }; + + worker.postMessage({ + width, + height, + mode, + direction, + snake, + blockWidth, + blockHeight, + startFromX, + startFromY, + }); + }); + } catch (e) { + console.warn('Failed to start coordinate worker, using sync generation:', e); + return generateCoordinates( + width, + height, + mode, + direction, + snake, + blockWidth, + blockHeight, + startFromX, + startFromY + ); + } +} diff --git a/Extension/scripts/overlay-manager.js b/Extension/scripts/overlay-manager.js index e63279c..011f41f 100644 --- a/Extension/scripts/overlay-manager.js +++ b/Extension/scripts/overlay-manager.js @@ -149,6 +149,7 @@ class OverlayManager { // Method 2: Force browser to bypass cache by adding cache-busting parameter // We need to intercept and modify tile requests to add cache-busting + // IMPORTANT: Preserve any existing fetch override (e.g., from Auto-Image assist mode) const originalFetch = window.fetch; let fetchOverrideActive = true; @@ -167,6 +168,7 @@ class OverlayManager { } } + // Chain to previous fetch (which may have assist mode logic) return originalFetch.apply(this, args); }; diff --git a/Extension/scripts/utils-manager.js b/Extension/scripts/utils-manager.js index 64581f8..572ea93 100644 --- a/Extension/scripts/utils-manager.js +++ b/Extension/scripts/utils-manager.js @@ -1165,6 +1165,13 @@ class WPlaceUtilsManager { try { console.log('🔄 Restoring overlay from saved data...'); + console.log('📊 Pixel data info:', { + width: window.state.imageData.width, + height: window.state.imageData.height, + pixelCount: window.state.imageData.pixels?.length, + firstPixels: window.state.imageData.pixels?.slice(0, 12), // First 3 pixels (RGBA) + isProcessed: '⚠️ These should be PROCESSED pixels if saved after processing' + }); // Recreate ImageBitmap from loaded pixel data (match old version exactly) const imageData = new ImageData( From 8a52dbd8cc6c3bdf4909d78f5c40bbb769aa71f6 Mon Sep 17 00:00:00 2001 From: MiraclePatrickLumowa Date: Mon, 13 Oct 2025 01:34:50 +0800 Subject: [PATCH 3/6] update lingua --- Extension/lang/de.json | 9 +++++++-- Extension/lang/en.json | 8 ++++++-- Extension/lang/es.json | 11 ++++++++--- Extension/lang/fr.json | 9 +++++++-- Extension/lang/id.json | 9 +++++++-- Extension/lang/ja.json | 9 +++++++-- Extension/lang/ko.json | 9 +++++++-- Extension/lang/lang/es-MX.json | 15 ++++++++++++--- Extension/lang/pt.json | 9 +++++++-- Extension/lang/ru.json | 9 +++++++-- Extension/lang/tr.json | 9 +++++++-- Extension/lang/uk.json | 9 +++++++-- Extension/lang/vi.json | 9 +++++++-- Extension/lang/zh-CN.json | 9 +++++++-- Extension/lang/zh-TW.json | 9 +++++++-- 15 files changed, 110 insertions(+), 32 deletions(-) diff --git a/Extension/lang/de.json b/Extension/lang/de.json index 3af0fbf..7e5dcbb 100644 --- a/Extension/lang/de.json +++ b/Extension/lang/de.json @@ -3,7 +3,7 @@ "toggleOverlay": "Vorschau umschalten", "scanColors": "Farben scannen", "uploadImage": "Bild hochladen", - "resizeImage": "Bildgröße ändern", + "resizeImage": "Bild verarbeiten/bearbeiten", "selectPosition": "Position wählen", "startPainting": "Malen starten", "stopPainting": "Malen stoppen", @@ -11,7 +11,8 @@ "noColorsFound": "❌ Öffne die Farbpalette und versuche es erneut!", "colorsFound": "✅ {count} verfügbare Farben gefunden. Bereit zum Hochladen.", "loadingImage": "🖼️ Bild wird geladen...", - "imageLoaded": "✅ Bild mit {count} gültigen Pixeln geladen", + "imageLoaded": "✅ Bild mit {count} gültigen Pixeln geladen ⚠️ Verarbeiten Sie das Bild zuerst über \"Bild verarbeiten/bearbeiten\", bevor Sie es platzieren!", + "imageNeedsProcessing": "Bild muss verarbeitet werden ⚠️", "imageError": "❌ Fehler beim Laden des Bildes", "selectPositionAlert": "Male den ersten Pixel an der Stelle, an der dein Kunstwerk beginnen soll!", "waitingPosition": "👆 Warte, bis du den Referenzpixel malst...", @@ -149,6 +150,10 @@ "paintTransparentPixelsDescription": "Wenn aktiviert, werden transparente Template-Pixel gemalt", "paintUnavailablePixels": "Nicht verfügbare Pixel malen", "paintUnavailablePixelsDescription": "Wenn aktiviert, werden nicht verfügbare Template-Farben mit der nächsten verfügbaren Farbe gemalt", + "paintingModeAuto": "Automatisch", + "paintingModeAssist": "Assistieren", + "paintingModeAutoDesc": "Bot malt automatisch Pixel", + "paintingModeAssistDesc": "Overlay leitet Ihre manuelle Pixelplatzierung", "colorsUpdated": "✅ Verfügbare Farben erhöht {oldCount} -> {newCount}, {diffCount} neue Farben gefunden", "overlayTilesNotLoaded": "❌ Erforderliche Kartenkacheln nicht geladen. Verbindung prüfen oder erneut versuchen.", "paintingBatchFailed": "❌ Batch-Senden nach mehreren Versuchen fehlgeschlagen. Malen gestoppt.", diff --git a/Extension/lang/en.json b/Extension/lang/en.json index ccf8021..0f7cd46 100644 --- a/Extension/lang/en.json +++ b/Extension/lang/en.json @@ -3,7 +3,7 @@ "toggleOverlay": "Toggle Overlay", "scanColors": "Scan Colors", "uploadImage": "Upload", - "resizeImage": "Resize", + "resizeImage": "Process/Edit Image", "selectPosition": "Select Position", "startPainting": "Start", "stopPainting": "Stop", @@ -158,5 +158,9 @@ "paintTransparentPixels": "Paint Transparent", "paintTransparentPixelsDescription": "If enabled, template transparent pixels will be painted", "paintUnavailablePixels": "Paint Unavailable", - "paintUnavailablePixelsDescription": "If enabled, template colors that are unavailable will be painted using the closest available color" + "paintUnavailablePixelsDescription": "If enabled, template colors that are unavailable will be painted using the closest available color", + "paintingModeAuto": "Auto", + "paintingModeAssist": "Assist", + "paintingModeAutoDesc": "Bot will automatically paint pixels", + "paintingModeAssistDesc": "Overlay will guide your manual pixel placement" } \ No newline at end of file diff --git a/Extension/lang/es.json b/Extension/lang/es.json index 5987aea..e2a8600 100644 --- a/Extension/lang/es.json +++ b/Extension/lang/es.json @@ -3,7 +3,7 @@ "toggleOverlay": "Alternar superposición", "scanColors": "Escanear colores", "uploadImage": "Subir imagen", - "resizeImage": "Cambiar tamaño", + "resizeImage": "Procesar/Editar imagen", "selectPosition": "Seleccionar posición", "startPainting": "Iniciar", "stopPainting": "Detener", @@ -12,7 +12,8 @@ "colorsUpdated": "✅ Colores disponibles aumentados {oldCount} -> {newCount}, {diffCount} nuevos colores encontrados", "colorsFound": "✅ Se encontraron {count} colores disponibles. Listo para subir.", "loadingImage": "🖼️ Cargando imagen...", - "imageLoaded": "✅ Imagen cargada con {count} píxeles válidos", + "imageLoaded": "✅ Imagen cargada con {count} píxeles válidos ⚠️ ¡Procesa la imagen primero mediante \"Procesar/Editar imagen\" antes de colocarla!", + "imageNeedsProcessing": "La imagen necesita procesamiento ⚠️", "imageError": "❌ Error al cargar la imagen", "selectPositionAlert": "¡Pinta el primer píxel en la ubicación donde quieres que comience el arte!", "waitingPosition": "👆 Esperando que pintes el píxel de referencia...", @@ -157,5 +158,9 @@ "paintTransparentPixels": "Pintar píxeles transparentes", "paintTransparentPixelsDescription": "Si está activado, se pintarán los píxeles transparentes de la plantilla", "paintUnavailablePixels": "Pintar píxeles no disponibles", - "paintUnavailablePixelsDescription": "Si está activado, los colores de plantilla no disponibles se pintarán usando el color disponible más cercano" + "paintUnavailablePixelsDescription": "Si está activado, los colores de plantilla no disponibles se pintarán usando el color disponible más cercano", + "paintingModeAuto": "Automático", + "paintingModeAssist": "Asistir", + "paintingModeAutoDesc": "El bot pintará píxeles automáticamente", + "paintingModeAssistDesc": "La superposición guiará tu colocación manual de píxeles" } diff --git a/Extension/lang/fr.json b/Extension/lang/fr.json index 6a83089..7948a65 100644 --- a/Extension/lang/fr.json +++ b/Extension/lang/fr.json @@ -3,7 +3,7 @@ "toggleOverlay": "Basculer l'overlay", "scanColors": "Scanner les couleurs", "uploadImage": "Télécharger l'image", - "resizeImage": "Redimensionner l'image", + "resizeImage": "Traiter/Éditer l'image", "selectPosition": "Sélectionner la position", "startPainting": "Commencer à peindre", "stopPainting": "Arrêter de peindre", @@ -11,7 +11,8 @@ "noColorsFound": "❌ Ouvrez la palette de couleurs sur le site et réessayez!", "colorsFound": "✅ {count} couleurs trouvées. Prêt à télécharger.", "loadingImage": "🖼️ Chargement de l'image...", - "imageLoaded": "✅ Image chargée avec {count} pixels valides", + "imageLoaded": "✅ Image chargée avec {count} pixels valides ⚠️ Traitez d'abord l'image via \"Traiter/Éditer l'image\" avant de la placer!", + "imageNeedsProcessing": "L'image nécessite un traitement ⚠️", "imageError": "❌ Erreur lors du chargement de l'image", "selectPositionAlert": "Peignez le premier pixel à l'endroit où vous voulez que l'art commence!", "waitingPosition": "👆 En attente que vous peigniez le pixel de référence...", @@ -151,6 +152,10 @@ "paintTransparentPixelsDescription": "Si activé, les pixels transparents du modèle seront peints", "paintUnavailablePixels": "Peindre les pixels indisponibles", "paintUnavailablePixelsDescription": "Si activé, les couleurs de modèle non disponibles seront peintes avec la couleur disponible la plus proche", + "paintingModeAuto": "Auto", + "paintingModeAssist": "Assistance", + "paintingModeAutoDesc": "Le bot peindra automatiquement les pixels", + "paintingModeAssistDesc": "La superposition guidera votre placement manuel de pixels", "colorsUpdated": "✅ Couleurs disponibles augmentées {oldCount} -> {newCount}, {diffCount} nouvelles couleurs trouvées", "overlayTilesNotLoaded": "❌ Tuiles de carte requises non chargées. Vérifiez la connexion ou réessayez.", "paintingBatchFailed": "❌ Échec de l'envoi du lot après plusieurs tentatives. Peinture arrêtée.", diff --git a/Extension/lang/id.json b/Extension/lang/id.json index b4f8bc7..7cfde44 100644 --- a/Extension/lang/id.json +++ b/Extension/lang/id.json @@ -3,7 +3,7 @@ "toggleOverlay": "Toggle Overlay", "scanColors": "Pindai Warna", "uploadImage": "Unggah Gambar", - "resizeImage": "Ubah Ukuran Gambar", + "resizeImage": "Proses/Edit Gambar", "selectPosition": "Pilih Posisi", "startPainting": "Mulai Melukis", "stopPainting": "Berhenti Melukis", @@ -11,7 +11,8 @@ "noColorsFound": "❌ Buka palet warna di situs dan coba lagi!", "colorsFound": "✅ {count} warna ditemukan. Siap untuk diunggah.", "loadingImage": "🖼️ Memuat gambar...", - "imageLoaded": "✅ Gambar dimuat dengan {count} piksel valid", + "imageLoaded": "✅ Gambar dimuat dengan {count} piksel valid ⚠️ Proses gambar terlebih dahulu melalui \"Proses/Edit Gambar\" sebelum menempatkan!", + "imageNeedsProcessing": "Gambar perlu diproses ⚠️", "imageError": "❌ Kesalahan saat memuat gambar", "selectPositionAlert": "Lukis piksel pertama di lokasi tempat karya seni akan dimulai!", "waitingPosition": "👆 Menunggu Anda melukis piksel referensi...", @@ -151,6 +152,10 @@ "paintTransparentPixelsDescription": "Jika diaktifkan, piksel transparan template akan dicat", "paintUnavailablePixels": "Cat piksel tidak tersedia", "paintUnavailablePixelsDescription": "Jika diaktifkan, warna template yang tidak tersedia akan dicat dengan warna tersedia terdekat", + "paintingModeAuto": "Otomatis", + "paintingModeAssist": "Bantu", + "paintingModeAutoDesc": "Bot akan melukis piksel secara otomatis", + "paintingModeAssistDesc": "Overlay akan memandu penempatan piksel manual Anda", "colorsUpdated": "✅ Warna tersedia meningkat {oldCount} -> {newCount}, {diffCount} warna baru ditemukan", "overlayTilesNotLoaded": "❌ Ubin peta yang diperlukan tidak dimuat. Periksa koneksi atau coba lagi.", "paintingBatchFailed": "❌ Gagal mengirim batch setelah beberapa percobaan. Melukis dihentikan.", diff --git a/Extension/lang/ja.json b/Extension/lang/ja.json index f247837..8500d4b 100644 --- a/Extension/lang/ja.json +++ b/Extension/lang/ja.json @@ -3,7 +3,7 @@ "toggleOverlay": "オーバーレイ切替", "scanColors": "色をスキャン", "uploadImage": "画像をアップロード", - "resizeImage": "画像サイズ変更", + "resizeImage": "画像の処理/編集", "selectPosition": "位置を選択", "startPainting": "描画開始", "stopPainting": "描画停止", @@ -11,7 +11,8 @@ "noColorsFound": "❌ サイトでカラーパレットを開いて再試行してください!", "colorsFound": "✅ 利用可能な色 {count} 件を検出。アップロード可能。", "loadingImage": "🖼️ 画像を読み込み中...", - "imageLoaded": "✅ 画像を読み込みました。有効なピクセル {count}", + "imageLoaded": "✅ 画像を読み込みました。有効なピクセル {count} ⚠️ 配置する前に「画像の処理/編集」で画像を処理してください!", + "imageNeedsProcessing": "画像の処理が必要です ⚠️", "imageError": "❌ 画像の読み込みエラー", "selectPositionAlert": "作品を開始したい位置に最初のピクセルを置いてください!", "waitingPosition": "👆 参照ピクセルの描画を待っています...", @@ -151,6 +152,10 @@ "paintTransparentPixelsDescription": "有効にすると、テンプレートの透明ピクセルが描画されます", "paintUnavailablePixels": "利用不可のピクセルを描画", "paintUnavailablePixelsDescription": "有効にすると、利用できないテンプレートの色は最も近い利用可能な色で描画されます", + "paintingModeAuto": "自動", + "paintingModeAssist": "アシスト", + "paintingModeAutoDesc": "ボットが自動的にピクセルを描画します", + "paintingModeAssistDesc": "オーバーレイが手動ピクセル配置をガイドします", "colorsUpdated": "✅ 利用可能な色が増加 {oldCount} -> {newCount}、{diffCount} 個の新しい色が見つかりました", "overlayTilesNotLoaded": "❌ 必要なマップタイルがロードされていません。接続を確認するか、再試行してください。", "paintingBatchFailed": "❌ 複数回の試行後、バッチ送信に失敗しました。描画を停止しました。", diff --git a/Extension/lang/ko.json b/Extension/lang/ko.json index 836b18f..7e083b6 100644 --- a/Extension/lang/ko.json +++ b/Extension/lang/ko.json @@ -3,7 +3,7 @@ "toggleOverlay": "오버레이 전환", "scanColors": "색상 스캔", "uploadImage": "이미지 업로드", - "resizeImage": "크기 조정", + "resizeImage": "이미지 처리/편집", "selectPosition": "위치 선택", "startPainting": "그리기 시작", "stopPainting": "그리기 중지", @@ -11,7 +11,8 @@ "noColorsFound": "❌ 색상 팔레트를 열고 다시 시도하세요!", "colorsFound": "✅ {count}개의 사용 가능한 색상을 찾았습니다. 업로드 준비 완료.", "loadingImage": "🖼️ 이미지 로딩 중...", - "imageLoaded": "✅ {count}개의 유효한 픽셀이 포함된 이미지를 로드했습니다", + "imageLoaded": "✅ {count}개의 유효한 픽셀이 포함된 이미지를 로드했습니다 ⚠️ 배치하기 전에 \"이미지 처리/편집\"을 통해 먼저 이미지를 처리하세요!", + "imageNeedsProcessing": "이미지 처리가 필요합니다 ⚠️", "imageError": "❌ 이미지 로딩 오류", "selectPositionAlert": "작품이 시작될 위치에 첫 번째 픽셀을 그리세요!", "waitingPosition": "👆 참조 픽셀 그릴 때까지 대기 중...", @@ -151,6 +152,10 @@ "paintTransparentPixelsDescription": "활성화하면 템플릿 투명 픽셀이 칠해집니다", "paintUnavailablePixels": "사용 불가능한 픽셀 칠하기", "paintUnavailablePixelsDescription": "활성화하면 사용할 수 없는 템플릿 색상을 가장 가까운 사용 가능한 색상으로 칠합니다", + "paintingModeAuto": "자동", + "paintingModeAssist": "보조", + "paintingModeAutoDesc": "봇이 자동으로 픽셀을 칠합니다", + "paintingModeAssistDesc": "오버레이가 수동 픽셀 배치를 안내합니다", "colorsUpdated": "✅ 사용 가능한 색상 증가 {oldCount} -> {newCount}, {diffCount}개의 새 색상 발견", "overlayTilesNotLoaded": "❌ 필요한 맵 타일이 로드되지 않았습니다. 연결을 확인하거나 다시 시도하세요.", "paintingBatchFailed": "❌ 여러 번 시도 후 배치 전송 실패. 그리기가 중지되었습니다.", diff --git a/Extension/lang/lang/es-MX.json b/Extension/lang/lang/es-MX.json index f702f52..96ba564 100644 --- a/Extension/lang/lang/es-MX.json +++ b/Extension/lang/lang/es-MX.json @@ -3,7 +3,7 @@ "toggleOverlay": "Alternar superposición", "scanColors": "Escanear colores", "uploadImage": "Subir imagen", - "resizeImage": "Cambiar tamaño de imagen", + "resizeImage": "Procesar/Editar imagen", "selectPosition": "Seleccionar posición", "startPainting": "Iniciar pintado", "stopPainting": "Detener pintado", @@ -11,7 +11,8 @@ "noColorsFound": "❌ ¡Abre la paleta de colores en el sitio e inténtalo de nuevo!", "colorsFound": "✅ Se encontraron {count} colores disponibles. Listo para subir.", "loadingImage": "🖼️ Cargando imagen...", - "imageLoaded": "✅ Imagen cargada con {count} píxeles válidos", + "imageLoaded": "✅ Imagen cargada con {count} píxeles válidos ⚠️ ¡Procesa la imagen primero mediante \"Procesar/Editar imagen\" antes de colocarla!", + "imageNeedsProcessing": "La imagen necesita procesamiento ⚠️", "imageError": "❌ Error al cargar la imagen", "selectPositionAlert": "¡Pinta el primer píxel en la ubicación donde quieres que comience el arte!", "waitingPosition": "👆 Esperando que pintes el píxel de referencia...", @@ -140,5 +141,13 @@ "hideStats": "Ocultar estadísticas", "paintOptions": "Opciones de pintado", "paintWhitePixels": "Pintar píxeles blancos", - "paintTransparentPixels": "Pintar píxeles transparentes" + "paintWhitePixelsDescription": "Si está activado, se pintarán los píxeles blancos de la plantilla.", + "paintTransparentPixels": "Pintar píxeles transparentes", + "paintTransparentPixelsDescription": "Si está activado, se pintarán los píxeles transparentes de la plantilla", + "paintUnavailablePixels": "Pintar píxeles no disponibles", + "paintUnavailablePixelsDescription": "Si está activado, los colores de plantilla no disponibles se pintarán usando el color disponible más cercano", + "paintingModeAuto": "Automático", + "paintingModeAssist": "Asistir", + "paintingModeAutoDesc": "El bot pintará píxeles automáticamente", + "paintingModeAssistDesc": "La superposición guiará tu colocación manual de píxeles" } diff --git a/Extension/lang/pt.json b/Extension/lang/pt.json index ed6bcb2..253774c 100644 --- a/Extension/lang/pt.json +++ b/Extension/lang/pt.json @@ -3,7 +3,7 @@ "toggleOverlay": "Toggle Overlay", "scanColors": "Escanear Cores", "uploadImage": "Upload da Imagem", - "resizeImage": "Redimensionar Imagem", + "resizeImage": "Processar/Editar Imagem", "selectPosition": "Selecionar Posição", "startPainting": "Iniciar Pintura", "stopPainting": "Parar Pintura", @@ -11,7 +11,8 @@ "noColorsFound": "❌ Abra a paleta de cores no site e tente novamente!", "colorsFound": "✅ {count} cores encontradas. Pronto para upload.", "loadingImage": "🖼️ Carregando imagem...", - "imageLoaded": "✅ Imagem carregada com {count} pixels válidos", + "imageLoaded": "✅ Imagem carregada com {count} pixels válidos ⚠️ Processe a imagem primeiro via \"Processar/Editar Imagem\" antes de colocar!", + "imageNeedsProcessing": "A imagem precisa de processamento ⚠️", "imageError": "❌ Erro ao carregar imagem", "selectPositionAlert": "Pinte o primeiro pixel на localização onde deseja que a arte comece!", "waitingPosition": "👆 Aguardando você pintar o pixel de referência...", @@ -150,6 +151,10 @@ "paintTransparentPixelsDescription": "Se ativado, pixels transparentes do modelo serão pintados", "paintUnavailablePixels": "Pintar pixels indisponíveis", "paintUnavailablePixelsDescription": "Se ativado, cores de modelo indisponíveis serão pintadas usando a cor disponível mais próxima", + "paintingModeAuto": "Automático", + "paintingModeAssist": "Assistir", + "paintingModeAutoDesc": "Bot pintará pixels automaticamente", + "paintingModeAssistDesc": "Sobreposição guiará sua colocação manual de pixels", "colorsUpdated": "✅ Cores disponíveis aumentadas {oldCount} -> {newCount}, {diffCount} novas cores encontradas", "overlayTilesNotLoaded": "❌ Blocos de mapa necessários não carregados. Verifique a conexão ou tente novamente.", "paintingBatchFailed": "❌ Falha ao enviar lote após várias tentativas. Pintura interrompida.", diff --git a/Extension/lang/ru.json b/Extension/lang/ru.json index c1d81c2..1bba518 100644 --- a/Extension/lang/ru.json +++ b/Extension/lang/ru.json @@ -3,7 +3,7 @@ "toggleOverlay": "Toggle Overlay", "scanColors": "Сканировать цвета", "uploadImage": "Загрузить изображение", - "resizeImage": "Изменить размер изображения", + "resizeImage": "Обработать/Редактировать изображение", "selectPosition": "Выбрать позицию", "startPainting": "Начать рисование", "stopPainting": "Остановить рисование", @@ -11,7 +11,8 @@ "noColorsFound": "❌ Откройте палитру цветов на сайте и попробуйте снова!", "colorsFound": "✅ Найдено доступных цветов: {count}. Готово к загрузке.", "loadingImage": "🖼️ Загрузка изображения...", - "imageLoaded": "✅ Изображение загружено, валидных пикселей: {count}", + "imageLoaded": "✅ Изображение загружено, валидных пикселей: {count} ⚠️ Сначала обработайте изображение через \"Обработать/Редактировать изображение\" перед размещением!", + "imageNeedsProcessing": "Изображение требует обработки ⚠️", "imageError": "❌ Ошибка при загрузке изображения", "selectPositionAlert": "Нарисуйте первый пиксель в месте, откуда начнётся рисунок!", "waitingPosition": "👆 Ожидание, пока вы нарисуете опорный пиксель...", @@ -150,6 +151,10 @@ "paintTransparentPixelsDescription": "Если включено, прозрачные пиксели шаблона будут рисоваться", "paintUnavailablePixels": "Рисовать недоступные пиксели", "paintUnavailablePixelsDescription": "Если включено, недоступные цвета шаблона будут рисоваться ближайшим доступным цветом", + "paintingModeAuto": "Авто", + "paintingModeAssist": "Помощь", + "paintingModeAutoDesc": "Бот будет автоматически рисовать пиксели", + "paintingModeAssistDesc": "Оверлей будет направлять вашу ручную расстановку пикселей", "colorsUpdated": "✅ Доступные цвета увеличены {oldCount} -> {newCount}, найдено {diffCount} новых цветов", "overlayTilesNotLoaded": "❌ Необходимые тайлы карты не загружены. Проверьте соединение или попробуйте снова.", "paintingBatchFailed": "❌ Не удалось отправить пакет после нескольких попыток. Рисование остановлено.", diff --git a/Extension/lang/tr.json b/Extension/lang/tr.json index dc990e4..e4e038b 100644 --- a/Extension/lang/tr.json +++ b/Extension/lang/tr.json @@ -3,7 +3,7 @@ "toggleOverlay": "Katmanı Aç/Kapat", "scanColors": "Renkleri Tara", "uploadImage": "Resim Yükle", - "resizeImage": "Resmi Yeniden Boyutlandır", + "resizeImage": "Resmi İşle/Düzenle", "selectPosition": "Konum Seç", "startPainting": "Boyamayı Başlat", "stopPainting": "Boyamayı Durdur", @@ -11,7 +11,8 @@ "noColorsFound": "❌ Sitede renk paletini açın ve tekrar deneyin!", "colorsFound": "✅ {count} uygun renk bulundu. Yüklemeye hazır.", "loadingImage": "🖼️ Resim yükleniyor...", - "imageLoaded": "✅ Resim {count} geçerli piksel ile yüklendi", + "imageLoaded": "✅ Resim {count} geçerli piksel ile yüklendi ⚠️ Yerleştirmeden önce \"Resmi İşle/Düzenle\" yoluyla resmi işleyin!", + "imageNeedsProcessing": "Resim işleme gerektirir ⚠️", "imageError": "❌ Resim yüklenirken hata oluştu", "selectPositionAlert": "Sanatı başlatmak istediğiniz ilk pikseli boyayın!", "waitingPosition": "👆 Referans pikseli boyamanız bekleniyor...", @@ -151,6 +152,10 @@ "paintTransparentPixelsDescription": "Etkinleştirilirse, şablon şeffaf pikselleri boyanacaktır", "paintUnavailablePixels": "Kullanılamayan pikselleri boya", "paintUnavailablePixelsDescription": "Etkinleştirilirse, kullanılamayan şablon renkleri en yakın uygun renkle boyanacaktır", + "paintingModeAuto": "Otomatik", + "paintingModeAssist": "Yardımcı", + "paintingModeAutoDesc": "Bot pikselleri otomatik olarak boyayacak", + "paintingModeAssistDesc": "Katman, manuel piksel yerleştirmenizi yönlendirecek", "colorsUpdated": "✅ Uygun renkler arttı {oldCount} -> {newCount}, {diffCount} yeni renk bulundu", "overlayTilesNotLoaded": "❌ Gerekli harita karoları yüklenmedi. Bağlantıyı kontrol edin veya yeniden deneyin.", "paintingBatchFailed": "❌ Toplu gönderim birkaç denemeden sonra başarısız oldu. Boyama durduruldu.", diff --git a/Extension/lang/uk.json b/Extension/lang/uk.json index 17e1aa1..71338bd 100644 --- a/Extension/lang/uk.json +++ b/Extension/lang/uk.json @@ -3,7 +3,7 @@ "toggleOverlay": "Перемкнути оверлей", "scanColors": "Сканувати кольори", "uploadImage": "Завантажити зображення", - "resizeImage": "Змінити розмір зображення", + "resizeImage": "Обробити/Редагувати зображення", "selectPosition": "Вибрати позицію", "startPainting": "Почати малювання", "stopPainting": "Зупинити малювання", @@ -11,7 +11,8 @@ "noColorsFound": "❌ Відкрий палітру кольорів на сайті та спробуй ще раз!", "colorsFound": "✅ Знайдено {count} доступних кольорів. Готово до завантаження.", "loadingImage": "🖼️ Завантаження зображення...", - "imageLoaded": "✅ Зображення завантажено. Валідних пікселів: {count}", + "imageLoaded": "✅ Зображення завантажено. Валідних пікселів: {count} ⚠️ Спочатку обробіть зображення через \"Обробити/Редагувати зображення\" перед розміщенням!", + "imageNeedsProcessing": "Зображення потребує обробки ⚠️", "imageError": "❌ Помилка завантаження зображення", "selectPositionAlert": "Намалюй перший піксель у місці, де має починатися арт!", "waitingPosition": "👆 Очікування на малювання референсного пікселя...", @@ -151,6 +152,10 @@ "paintTransparentPixelsDescription": "Якщо ввімкнено, прозорі пікселі шаблону будуть намальовані", "paintUnavailablePixels": "Малювати недоступні пікселі", "paintUnavailablePixelsDescription": "Якщо ввімкнено, недоступні кольори шаблону будуть намальовані найближчим доступним кольором", + "paintingModeAuto": "Авто", + "paintingModeAssist": "Допомога", + "paintingModeAutoDesc": "Бот буде автоматично малювати пікселі", + "paintingModeAssistDesc": "Оверлей буде направляти ваше ручне розташування пікселів", "colorsUpdated": "✅ Доступні кольори збільшено {oldCount} -> {newCount}, знайдено {diffCount} нових кольорів", "overlayTilesNotLoaded": "❌ Необхідні тайли карти не завантажені. Перевір з'єднання або спробуй знову.", "paintingBatchFailed": "❌ Не вдалося відправити пакет після кількох спроб. Малювання зупинено.", diff --git a/Extension/lang/vi.json b/Extension/lang/vi.json index d4c964a..d477249 100644 --- a/Extension/lang/vi.json +++ b/Extension/lang/vi.json @@ -3,7 +3,7 @@ "toggleOverlay": "Bật/tắt lớp phủ", "scanColors": "Quét màu", "uploadImage": "Tải lên hình ảnh", - "resizeImage": "Thay đổi kích thước", + "resizeImage": "Xử lý/Chỉnh sửa hình ảnh", "selectPosition": "Chọn vị trí", "startPainting": "Bắt đầu vẽ", "stopPainting": "Dừng vẽ", @@ -11,7 +11,8 @@ "noColorsFound": "❌ Hãy mở bảng màu trên trang web và thử lại!", "colorsFound": "✅ Tìm thấy {count} màu. Sẵn sàng để tải lên.", "loadingImage": "🖼️ Đang tải hình ảnh...", - "imageLoaded": "✅ Đã tải hình ảnh với {count} pixel hợp lệ", + "imageLoaded": "✅ Đã tải hình ảnh với {count} pixel hợp lệ ⚠️ Xử lý hình ảnh trước qua \"Xử lý/Chỉnh sửa hình ảnh\" trước khi đặt!", + "imageNeedsProcessing": "Hình ảnh cần xử lý ⚠️", "imageError": "❌ Lỗi khi tải hình ảnh", "selectPositionAlert": "Vẽ pixel đầu tiên tại vị trí bạn muốn tác phẩm nghệ thuật bắt đầu!", "waitingPosition": "👆 Đang chờ bạn vẽ pixel tham chiếu...", @@ -151,6 +152,10 @@ "paintTransparentPixelsDescription": "Nếu được bật, các pixel trong suốt của mẫu sẽ được vẽ", "paintUnavailablePixels": "Vẽ pixel không khả dụng", "paintUnavailablePixelsDescription": "Nếu được bật, màu mẫu không khả dụng sẽ được vẽ bằng màu khả dụng gần nhất", + "paintingModeAuto": "Tự động", + "paintingModeAssist": "Trợ giúp", + "paintingModeAutoDesc": "Bot sẽ tự động vẽ pixel", + "paintingModeAssistDesc": "Lớp phủ sẽ hướng dẫn việc đặt pixel thủ công của bạn", "colorsUpdated": "✅ Màu khả dụng tăng {oldCount} -> {newCount}, tìm thấy {diffCount} màu mới", "overlayTilesNotLoaded": "❌ Ô bản đồ cần thiết chưa được tải. Kiểm tra kết nối hoặc thử lại.", "paintingBatchFailed": "❌ Gửi lô thất bại sau nhiều lần thử. Dừng vẽ.", diff --git a/Extension/lang/zh-CN.json b/Extension/lang/zh-CN.json index ef67cd7..c0ae5da 100644 --- a/Extension/lang/zh-CN.json +++ b/Extension/lang/zh-CN.json @@ -3,7 +3,7 @@ "toggleOverlay": "切换覆盖层", "scanColors": "扫描颜色", "uploadImage": "上传图像", - "resizeImage": "调整大小", + "resizeImage": "处理/编辑图像", "selectPosition": "选择位置", "startPainting": "开始绘制", "stopPainting": "停止绘制", @@ -11,7 +11,8 @@ "noColorsFound": "❌ 请在网站上打开调色板后再试!", "colorsFound": "✅ 找到 {count} 个可用颜色,准备上传。", "loadingImage": "🖼️ 正在加载图像...", - "imageLoaded": "✅ 图像已加载,包含 {count} 个有效像素", + "imageLoaded": "✅ 图像已加载,包含 {count} 个有效像素 ⚠️ 放置前请先通过\"处理/编辑图像\"处理图像!", + "imageNeedsProcessing": "图像需要处理 ⚠️", "imageError": "❌ 加载图像时出错", "selectPositionAlert": "请在你想让作品开始的位置绘制第一个像素!", "waitingPosition": "👆 正在等待你绘制参考像素...", @@ -151,6 +152,10 @@ "paintTransparentPixelsDescription": "如果启用,将绘制模板透明像素", "paintUnavailablePixels": "绘制不可用像素", "paintUnavailablePixelsDescription": "如果启用,不可用的模板颜色将使用最接近的可用颜色绘制", + "paintingModeAuto": "自动", + "paintingModeAssist": "辅助", + "paintingModeAutoDesc": "机器人将自动绘制像素", + "paintingModeAssistDesc": "叠加层将引导您手动放置像素", "colorsUpdated": "✅ 可用颜色增加 {oldCount} -> {newCount},找到 {diffCount} 个新颜色", "overlayTilesNotLoaded": "❌ 所需的地图图块未加载。检查连接或重试。", "paintingBatchFailed": "❌ 多次尝试后批量发送失败。绘制已停止。", diff --git a/Extension/lang/zh-TW.json b/Extension/lang/zh-TW.json index bbb8792..7dfad9d 100644 --- a/Extension/lang/zh-TW.json +++ b/Extension/lang/zh-TW.json @@ -3,7 +3,7 @@ "toggleOverlay": "切換覆蓋層", "scanColors": "掃描顏色", "uploadImage": "上傳圖像", - "resizeImage": "調整大小", + "resizeImage": "處理/編輯圖像", "selectPosition": "選擇位置", "startPainting": "開始繪製", "stopPainting": "停止繪製", @@ -11,7 +11,8 @@ "noColorsFound": "❌ 請在網站上打開調色板後再試!", "colorsFound": "✅ 找到 {count} 個可用顏色,準備上傳。", "loadingImage": "🖼️ 正在載入圖像...", - "imageLoaded": "✅ 圖像已載入,包含 {count} 個有效像素", + "imageLoaded": "✅ 圖像已載入,包含 {count} 個有效像素 ⚠️ 放置前請先透過\"處理/編輯圖像\"處理圖像!", + "imageNeedsProcessing": "圖像需要處理 ⚠️", "imageError": "❌ 載入圖像時出錯", "selectPositionAlert": "請在你想讓作品開始的位置繪製第一個像素!", "waitingPosition": "👆 正在等待你繪製參考像素...", @@ -151,6 +152,10 @@ "paintTransparentPixelsDescription": "如果啟用,將繪製範本透明像素", "paintUnavailablePixels": "繪製不可用像素", "paintUnavailablePixelsDescription": "如果啟用,不可用的範本顏色將使用最接近的可用顏色繪製", + "paintingModeAuto": "自動", + "paintingModeAssist": "輔助", + "paintingModeAutoDesc": "機器人將自動繪製像素", + "paintingModeAssistDesc": "疊加層將引導您手動放置像素", "colorsUpdated": "✅ 可用顏色增加 {oldCount} -> {newCount},找到 {diffCount} 個新顏色", "overlayTilesNotLoaded": "❌ 所需的地圖圖塊未載入。檢查連線或重試。", "paintingBatchFailed": "❌ 多次嘗試後批次傳送失敗。繪製已停止。", From 2dd0ac22b0d5d655532b55475740df88925fcd64 Mon Sep 17 00:00:00 2001 From: MiraclePatrickLumowa Date: Mon, 13 Oct 2025 02:18:03 +0800 Subject: [PATCH 4/6] Update README.md --- README.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/README.md b/README.md index f0dbc5c..0b2fee6 100644 --- a/README.md +++ b/README.md @@ -68,6 +68,8 @@ Follow the exact same steps listed for Chrome above, using Kiwi Browser instead. - [x] Automatically farms droplets/levels - [X] Artwork Extractor(copy-paste artworks) + +- [x] Painting Assist - [X] Quick Art mover feature From 9866b58268c8da7e5c63b79edcf4ef470eaf9c6c Mon Sep 17 00:00:00 2001 From: MiraclePatrickLumowa Date: Mon, 13 Oct 2025 02:47:35 +0800 Subject: [PATCH 5/6] Update README.md --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 0b2fee6..1d87f65 100644 --- a/README.md +++ b/README.md @@ -94,7 +94,7 @@ Follow the exact same steps listed for Chrome above, using Kiwi Browser instead. - [x] Works on mobile (via browsers like Kiwi) :P

-Script Demonstration +Script Demonstration

📋┃To-do From f799595984b1aeb102977b17c8ab8ce0fb2a5bf7 Mon Sep 17 00:00:00 2001 From: MiraclePatrickLumowa Date: Mon, 13 Oct 2025 02:49:18 +0800 Subject: [PATCH 6/6] Update README.md --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 1d87f65..ad89233 100644 --- a/README.md +++ b/README.md @@ -94,7 +94,7 @@ Follow the exact same steps listed for Chrome above, using Kiwi Browser instead. - [x] Works on mobile (via browsers like Kiwi) :P

-Script Demonstration +Script Demonstration

📋┃To-do