/** * WPlace AutoBOT - Utils Manager * Centralized utility functions for the WPlace automation systemds */ class WPlaceUtilsManager { constructor() { this.translationCache = new Map(); this._labCache = new Map(); // Available languages this.AVAILABLE_LANGUAGES = [ 'en', 'ru', 'pt', 'vi', 'fr', 'id', 'tr', 'zh-CN', 'zh-TW', 'ja', 'ko', 'uk' ]; } // Basic utility functions sleep = (ms) => new Promise((r) => setTimeout(r, ms)); async dynamicSleep(tickAndGetRemainingMs) { let remaining = Math.max(0, await tickAndGetRemainingMs()); while (remaining > 0) { const interval = remaining > 5000 ? 2000 : remaining > 1000 ? 500 : 100; await this.sleep(Math.min(interval, remaining)); remaining = Math.max(0, await tickAndGetRemainingMs()); } } async waitForSelector(selector, interval = 200, timeout = 5000) { const start = Date.now(); while (Date.now() - start < timeout) { const el = document.querySelector(selector); if (el) return el; await this.sleep(interval); } return null; } msToTimeText(ms) { const totalSeconds = Math.ceil(ms / 1000); const hours = Math.floor(totalSeconds / 3600); const minutes = Math.floor((totalSeconds % 3600) / 60); const seconds = totalSeconds % 60; if (hours > 0) return `${hours}h ${minutes}m ${seconds}s`; if (minutes > 0) return `${minutes}m ${seconds}s`; return `${seconds}s`; } // Debounced scroll-to-adjust handler for sliders createScrollToAdjust(element, updateCallback, min, max, step = 1) { let debounceTimer = null; const handleWheel = (e) => { if (e.target !== element) return; e.preventDefault(); e.stopPropagation(); if (debounceTimer) { clearTimeout(debounceTimer); } debounceTimer = setTimeout(() => { const currentValue = parseInt(element.value) || 0; const delta = e.deltaY > 0 ? -step : step; const newValue = Math.max(min, Math.min(max, currentValue + delta)); if (newValue !== currentValue) { element.value = newValue; updateCallback(newValue); } }, 50); }; element.addEventListener('wheel', handleWheel, { passive: false }); return () => { if (debounceTimer) clearTimeout(debounceTimer); element.removeEventListener('wheel', handleWheel); }; } // Tile range calculation delegation calculateTileRange(startRegionX, startRegionY, startPixelX, startPixelY, width, height, tileSize = 1000) { return window.globalOverlayManager.calculateTileRange( startRegionX, startRegionY, startPixelX, startPixelY, width, height, tileSize ); } // Token management delegation loadTurnstile() { return window.globalTokenManager.loadTurnstile(); } ensureTurnstileContainer() { return window.globalTokenManager.ensureTurnstileContainer(); } ensureTurnstileOverlayContainer() { return window.globalTokenManager.ensureTurnstileOverlayContainer(); } executeTurnstile(sitekey, action) { return window.globalTokenManager.executeTurnstile(sitekey, action); } createTurnstileWidget(sitekey, action) { return window.globalTokenManager.createTurnstileWidget(sitekey, action); } createTurnstileWidgetInteractive(sitekey, action) { return window.globalTokenManager.createTurnstileWidgetInteractive(sitekey, action); } cleanupTurnstile() { return window.globalTokenManager.cleanupTurnstile(); } obtainSitekeyAndToken(fallback) { return window.globalTokenManager.obtainSitekeyAndToken(fallback); } // DOM utilities createElement(tag, props = {}, children = []) { const element = document.createElement(tag); Object.entries(props).forEach(([key, value]) => { if (key === 'style' && typeof value === 'object') { Object.assign(element.style, value); } else if (key === 'className') { element.className = value; } else if (key === 'innerHTML') { element.innerHTML = value; } else { element.setAttribute(key, value); } }); if (typeof children === 'string') { element.textContent = children; } else if (Array.isArray(children)) { children.forEach((child) => { if (typeof child === 'string') { element.appendChild(document.createTextNode(child)); } else { element.appendChild(child); } }); } return element; } createButton(id, text, icon, onClick, style = window.CONFIG.CSS_CLASSES.BUTTON_PRIMARY) { const button = this.createElement('button', { id: id, style: style, innerHTML: `${icon ? `` : ''}${text}`, }); if (onClick) button.addEventListener('click', onClick); return button; } // Translation function t(key, params = {}) { const state = window.state; const translationCache = this.translationCache; const loadedTranslations = window.loadedTranslations || {}; // Safety check: ensure state and language exist if (!state || !state.language) { // Fallback: try to get translation directly or return key if (loadedTranslations[key]) { let text = loadedTranslations[key]; Object.keys(params).forEach((param) => { text = text.replace(`{${param}}`, params[param]); }); return text; } return key; // Return the key as fallback } const cacheKey = `${state.language}_${key}`; if (translationCache.has(cacheKey)) { let text = translationCache.get(cacheKey); Object.keys(params).forEach((param) => { text = text.replace(`{${param}}`, params[param]); }); return text; } if (loadedTranslations[state.language]?.[key]) { let text = loadedTranslations[state.language][key]; translationCache.set(cacheKey, text); Object.keys(params).forEach((param) => { text = text.replace(`{${param}}`, params[param]); }); return text; } // Try English fallback if available and not using English if (state.language && state.language !== 'en' && loadedTranslations['en']?.[key]) { let text = loadedTranslations['en'][key]; Object.keys(params).forEach((param) => { text = text.replace(`{${param}}`, params[param]); }); return text; } // Final fallback to FALLBACK_TEXT or key const language = state.language || 'en'; let text = window.FALLBACK_TEXT?.[language]?.[key] || window.FALLBACK_TEXT?.en?.[key] || key; Object.keys(params).forEach((param) => { text = text.replace(new RegExp(`\\{${param}\\}`, 'g'), params[param]); }); if (text === key && key !== 'undefined') { console.warn(`⚠️ Missing translation for key: ${key} (language: ${language})`); } return text; } showAlert(message, type = 'info') { const alertDiv = document.createElement('div'); alertDiv.className = `wplace-alert-base wplace-alert-${type}`; alertDiv.textContent = message; document.body.appendChild(alertDiv); setTimeout(() => { alertDiv.style.animation = 'slide-down 0.3s ease-out reverse'; setTimeout(() => { document.body.removeChild(alertDiv); }, 300); }, 4000); } // Color utilities colorDistance(a, b) { return Math.sqrt(Math.pow(a[0] - b[0], 2) + Math.pow(a[1] - b[1], 2) + Math.pow(a[2] - b[2], 2)); } _rgbToLab(r, g, b) { const srgbToLinear = (v) => { v /= 255; return v <= 0.04045 ? v / 12.92 : Math.pow((v + 0.055) / 1.055, 2.4); }; const rl = srgbToLinear(r); const gl = srgbToLinear(g); const bl = srgbToLinear(b); let X = rl * 0.4124 + gl * 0.3576 + bl * 0.1805; let Y = rl * 0.2126 + gl * 0.7152 + bl * 0.0722; let Z = rl * 0.0193 + gl * 0.1192 + bl * 0.9505; X /= 0.95047; Y /= 1.0; Z /= 1.08883; const f = (t) => (t > 0.008856 ? Math.cbrt(t) : 7.787 * t + 16 / 116); const fX = f(X), fY = f(Y), fZ = f(Z); const L = 116 * fY - 16; const a = 500 * (fX - fY); const b2 = 200 * (fY - fZ); return [L, a, b2]; } _lab(r, g, b) { const key = (r << 16) | (g << 8) | b; let v = this._labCache.get(key); if (!v) { v = this._rgbToLab(r, g, b); this._labCache.set(key, v); } return v; } findClosestPaletteColor(r, g, b, palette) { // Use provided palette or derive from COLOR_MAP if (!palette || palette.length === 0) { palette = Object.values(window.CONFIG.COLOR_MAP) .filter((c) => c.rgb) .map((c) => [c.rgb.r, c.rgb.g, c.rgb.b]); } // Use modular ImageProcessor for color matching if available if (window.globalImageProcessor) { return window.globalImageProcessor.findClosestPaletteColor( r, g, b, palette, window.state.colorMatchingAlgorithm || 'lab', { enableChromaPenalty: window.state.enableChromaPenalty || false, chromaPenaltyWeight: window.state.chromaPenaltyWeight || 0.15 } ); } // Fallback: simple color distance matching let closestColor = null; let minDistance = Infinity; for (const color of palette) { const distance = this.colorDistance([r, g, b], color); if (distance < minDistance) { minDistance = distance; closestColor = color; } } return closestColor || [r, g, b]; } isWhitePixel(r, g, b) { const wt = window.state.customWhiteThreshold || window.CONFIG.WHITE_THRESHOLD; if (window.globalImageProcessor) { return window.globalImageProcessor.isWhitePixel(r, g, b, wt); } // Fallback calculation if image processor not available return r >= wt && g >= wt && b >= wt; } resolveColor(targetRgb, availableColors, exactMatch = false) { if (window.globalImageProcessor) { return window.globalImageProcessor.resolveColor(targetRgb, availableColors, { exactMatch, algorithm: window.state.colorMatchingAlgorithm || 'lab', enableChromaPenalty: window.state.enableChromaPenalty || false, chromaPenaltyWeight: window.state.chromaPenaltyWeight || 0.15, whiteThreshold: window.state.customWhiteThreshold || window.CONFIG.WHITE_THRESHOLD }); } // Fallback: simple distance-based matching if (!availableColors || availableColors.length === 0) return null; let closestColor = null; let minDistance = Infinity; for (const color of availableColors) { const distance = this.colorDistance(targetRgb, color.rgb || [color.rgb.r, color.rgb.g, color.rgb.b]); if (distance < minDistance) { minDistance = distance; closestColor = color; } } return closestColor; } createImageUploader() { if (window.globalImageProcessor) { return window.globalImageProcessor.createImageUploader(); } // Fallback implementation if image processor not available 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(); }); } // File utilities 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); } 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(); }); } extractAvailableColors() { if (window.globalImageProcessor) { return window.globalImageProcessor.extractAvailableColors(window.CONFIG.COLOR_MAP); } // Fallback: extract colors directly from CONFIG if image processor not available if (window.CONFIG && window.CONFIG.COLOR_MAP) { return Object.values(window.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] })); } return []; } formatTime(ms) { // Handle invalid or infinite values if (!Number.isFinite(ms) || ms < 0) { return '--:--:--'; } // Handle very large values (more than 999 days) if (ms > 999 * 24 * 60 * 60 * 1000) { return '999d+'; } const seconds = Math.floor((ms / 1000) % 60); const minutes = Math.floor((ms / (1000 * 60)) % 60); const hours = Math.floor((ms / (1000 * 60 * 60)) % 24); const days = Math.floor(ms / (1000 * 60 * 60 * 24)); let result = ''; if (days > 0) result += `${days}d `; if (hours > 0 || days > 0) result += `${hours}h `; if (minutes > 0 || hours > 0 || days > 0) result += `${minutes}m `; result += `${seconds}s`; return result; } calculateEstimatedTime(remainingPixels, charges, cooldown) { // Safety checks for input parameters if (!Number.isFinite(remainingPixels) || remainingPixels <= 0) return 0; if (!Number.isFinite(charges) || charges <= 0) charges = 1; if (!Number.isFinite(cooldown) || cooldown <= 0) cooldown = 30000; // Default 30s const paintingSpeed = window.state?.paintingSpeed || 5; if (!Number.isFinite(paintingSpeed) || paintingSpeed <= 0) { // Fallback calculation without painting speed const cyclesNeeded = Math.ceil(remainingPixels / Math.max(charges, 1)); const timeFromCharges = cyclesNeeded * cooldown; return Math.min(timeFromCharges, 999 * 24 * 60 * 60 * 1000); // Cap at 999 days } const paintingSpeedDelay = 1000 / paintingSpeed; const timeFromSpeed = remainingPixels * paintingSpeedDelay; const cyclesNeeded = Math.ceil(remainingPixels / Math.max(charges, 1)); const timeFromCharges = cyclesNeeded * cooldown; const totalTime = timeFromSpeed + timeFromCharges; // Safety check to prevent infinity and cap at reasonable maximum if (!Number.isFinite(totalTime) || totalTime < 0) { return 0; } // Cap at 999 days to prevent display issues return Math.min(totalTime, 999 * 24 * 60 * 60 * 1000); } // Painted pixel tracking helpers initializePaintedMap(width, height) { if (!window.state.paintedMap || window.state.paintedMap.length !== height) { window.state.paintedMap = Array(height) .fill() .map(() => Array(width).fill(false)); } } markPixelPainted(x, y, regionX = 0, regionY = 0) { const actualX = x + regionX; const actualY = y + regionY; if ( window.state.paintedMap && window.state.paintedMap[actualY] && actualX >= 0 && actualX < window.state.paintedMap[actualY].length ) { window.state.paintedMap[actualY][actualX] = true; } } isPixelPainted(x, y, regionX = 0, regionY = 0) { const actualX = x + regionX; const actualY = y + regionY; if ( window.state.paintedMap && window.state.paintedMap[actualY] && actualX >= 0 && actualX < window.state.paintedMap[actualY].length ) { return window.state.paintedMap[actualY][actualX]; } return false; } // Alias for isPixelPainted - used in pre-filtering logic isPixelMarkedPainted(x, y, regionX = 0, regionY = 0) { return this.isPixelPainted(x, y, regionX, regionY); } // Smart save shouldAutoSave() { const now = Date.now(); const pixelsSinceLastSave = window.state.paintedPixels - window.state._lastSavePixelCount; const timeSinceLastSave = now - window.state._lastSaveTime; return !window.state._saveInProgress && pixelsSinceLastSave >= 25 && timeSinceLastSave >= 30000; } performSmartSave() { if (!this.shouldAutoSave()) return false; window.state._saveInProgress = true; const success = this.saveProgress(); if (success) { window.state._lastSavePixelCount = window.state.paintedPixels; window.state._lastSaveTime = Date.now(); } window.state._saveInProgress = false; return success; } // Data compression helpers packPaintedMapToBase64(paintedMap, width, height) { if (!paintedMap || !width || !height) return null; const totalBits = width * height; const byteLen = Math.ceil(totalBits / 8); const bytes = new Uint8Array(byteLen); let bitIndex = 0; for (let y = 0; y < height; y++) { const row = paintedMap[y]; for (let x = 0; x < width; x++) { const bit = row && row[x] ? 1 : 0; const b = bitIndex >> 3; const o = bitIndex & 7; if (bit) bytes[b] |= 1 << o; bitIndex++; } } let binary = ''; const chunk = 0x8000; for (let i = 0; i < bytes.length; i += chunk) { binary += String.fromCharCode.apply( null, bytes.subarray(i, Math.min(i + chunk, bytes.length)) ); } return btoa(binary); } unpackPaintedMapFromBase64(base64, width, height) { if (!base64 || !width || !height) return null; const binary = atob(base64); const bytes = new Uint8Array(binary.length); for (let i = 0; i < binary.length; i++) bytes[i] = binary.charCodeAt(i); const map = Array(height) .fill() .map(() => Array(width).fill(false)); let bitIndex = 0; for (let y = 0; y < height; y++) { for (let x = 0; x < width; x++) { const b = bitIndex >> 3; const o = bitIndex & 7; map[y][x] = ((bytes[b] >> o) & 1) === 1; bitIndex++; } } return map; } // Migration helpers migrateProgressToV2(saved) { if (!saved) return saved; const isV1 = !saved.version || saved.version === '1' || saved.version === '1.0' || saved.version === '1.1'; if (!isV1) return saved; try { const migrated = { ...saved }; const width = migrated.imageData?.width; const height = migrated.imageData?.height; if (migrated.paintedMap && width && height) { const data = this.packPaintedMapToBase64(migrated.paintedMap, width, height); migrated.paintedMapPacked = { width, height, data }; } delete migrated.paintedMap; migrated.version = '2'; return migrated; } catch (e) { console.warn('Migration to v2 failed, using original data:', e); return saved; } } migrateProgressToV21(saved) { if (!saved) return saved; if (saved.version === '2.1') return saved; const isV2 = saved.version === '2' || saved.version === '2.0'; const isV1 = !saved.version || saved.version === '1' || saved.version === '1.0' || saved.version === '1.1'; if (!isV2 && !isV1) return saved; try { const migrated = { ...saved }; if (isV1) { const width = migrated.imageData?.width; const height = migrated.imageData?.height; if (migrated.paintedMap && width && height) { const data = this.packPaintedMapToBase64(migrated.paintedMap, width, height); migrated.paintedMapPacked = { width, height, data }; } delete migrated.paintedMap; } migrated.version = '2.1'; return migrated; } catch (e) { console.warn('Migration to v2.1 failed, using original data:', e); return saved; } } migrateProgressToV22(data) { try { const migrated = { ...data }; migrated.version = '2.2'; if (!migrated.state.coordinateMode) { migrated.state.coordinateMode = window.CONFIG.COORDINATE_MODE; } if (!migrated.state.coordinateDirection) { migrated.state.coordinateDirection = window.CONFIG.COORDINATE_DIRECTION; } if (!migrated.state.coordinateSnake) { migrated.state.coordinateSnake = window.CONFIG.COORDINATE_SNAKE; } if (!migrated.state.blockWidth) { migrated.state.blockWidth = window.CONFIG.COORDINATE_BLOCK_WIDTH; } if (!migrated.state.blockHeight) { migrated.state.blockHeight = window.CONFIG.COORDINATE_BLOCK_HEIGHT; } return migrated; } catch (e) { console.warn('Migration to v2.2 failed, using original data:', e); return data; } } buildPaintedMapPacked() { if (window.state.paintedMap && window.state.imageData) { const data = this.packPaintedMapToBase64( window.state.paintedMap, window.state.imageData.width, window.state.imageData.height ); if (data) { return { width: window.state.imageData.width, height: window.state.imageData.height, data: data, }; } } return null; } buildProgressData() { const result = { 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: null, paintedMapPacked: null, }; // Decide how to store image pixels if (window.state.imageData) { const img = window.state.imageData; const width = img.width; const height = img.height; const totalPixels = img.totalPixels; const pixels = img.pixels; const USE_IDB_THRESHOLD = 500000; // ~0.5 MB raw RGBA (before JSON expansion) if (pixels && pixels.length) { const bytes = pixels.length; // Uint8ClampedArray length equals byte size if (bytes > USE_IDB_THRESHOLD) { // Store pixels in IndexedDB and keep only the reference in save payload const ref = this.generatePixelsRef(width, height); try { // Fire-and-forget async persist this.storePixelsInIndexedDB(ref, { width, height, totalPixels, pixels }); } catch (e) { console.warn('Failed to schedule IDB pixel store:', e); } result.imageData = { width, height, totalPixels, pixelsRef: ref, pixelsBackend: 'idb' }; } else { result.imageData = { width, height, pixels: Array.from(pixels), totalPixels }; } } else { result.imageData = { width, height, totalPixels, pixels: null }; } } result.paintedMapPacked = this.buildPaintedMapPacked(); return result; } migrateProgress(saved) { if (!saved) return null; let data = saved; const ver = data.version; if (!ver || ver === '1' || ver === '1.0' || ver === '1.1') { data = this.migrateProgressToV2(data); } if (data.version === '2' || data.version === '2.0') { data = this.migrateProgressToV21(data); } if (data.version === '2.1') { data = this.migrateProgressToV22(data); } return data; } saveProgress() { try { const progressData = this.buildProgressData(); try { localStorage.setItem('wplace-bot-progress', JSON.stringify(progressData)); return true; } catch (e) { // Strip heavy pixel data and retry if quota exceeded if (progressData && progressData.imageData && progressData.imageData.pixels) { const slim = { ...progressData, imageData: { width: progressData.imageData.width, height: progressData.imageData.height, totalPixels: progressData.imageData.totalPixels, pixelsStripped: true, } }; try { localStorage.setItem('wplace-bot-progress', JSON.stringify(slim)); console.warn('Saved progress without raw pixel data due to storage quota limits.'); return true; } catch (e2) { try { sessionStorage.setItem('wplace-bot-progress', JSON.stringify(slim)); console.warn('Saved progress to sessionStorage without pixel data due to storage quota limits.'); return true; } catch (e3) { throw e2; } } } throw e; } } catch (error) { console.error('Error saving progress:', error); return false; } } loadProgress() { try { const saved = localStorage.getItem('wplace-bot-progress'); if (!saved) return null; let data = JSON.parse(saved); const migrated = this.migrateProgress(data); if (migrated && migrated !== data) { try { localStorage.setItem('wplace-bot-progress', JSON.stringify(migrated)); } catch { } } return migrated; } catch (error) { console.error('Error loading progress:', error); return null; } } clearProgress() { try { localStorage.removeItem('wplace-bot-progress'); window.state.paintedMap = null; window.state._lastSavePixelCount = 0; window.state._lastSaveTime = 0; window.state.coordinateMode = window.CONFIG.COORDINATE_MODE; window.state.coordinateDirection = window.CONFIG.COORDINATE_DIRECTION; window.state.coordinateSnake = window.CONFIG.COORDINATE_SNAKE; window.state.blockWidth = window.CONFIG.COORDINATE_BLOCK_WIDTH; window.state.blockHeight = window.CONFIG.COORDINATE_BLOCK_HEIGHT; return true; } catch (error) { console.error('Error clearing progress:', error); return false; } } restoreProgress(savedData) { try { Object.assign(window.state, savedData.state); // Restore coordinate generation settings if (savedData.state.coordinateMode) { window.state.coordinateMode = savedData.state.coordinateMode; } if (savedData.state.coordinateDirection) { window.state.coordinateDirection = savedData.state.coordinateDirection; } if (savedData.state.coordinateSnake !== undefined) { window.state.coordinateSnake = savedData.state.coordinateSnake; } if (savedData.state.blockWidth) { window.state.blockWidth = savedData.state.blockWidth; } if (savedData.state.blockHeight) { window.state.blockHeight = savedData.state.blockHeight; } // Restore available colors from old save files (backward compatibility) if (savedData.state.availableColors && Array.isArray(savedData.state.availableColors)) { window.state.availableColors = savedData.state.availableColors; window.state.colorsChecked = true; // Mark colors as checked } if (savedData.imageData && Array.isArray(savedData.imageData.pixels)) { window.state.imageData = { width: savedData.imageData.width, height: savedData.imageData.height, pixels: savedData.imageData.pixels, // Keep as Array - will convert in ImageData constructor totalPixels: savedData.imageData.totalPixels, }; try { const canvas = document.createElement('canvas'); canvas.width = window.state.imageData.width; canvas.height = window.state.imageData.height; const ctx = canvas.getContext('2d'); const imageData = new ImageData( window.state.imageData.pixels, window.state.imageData.width, window.state.imageData.height ); ctx.putImageData(imageData, 0, 0); // Create ImageProcessor directly like the original - no delegation if (window.ImageProcessor) { const proc = new window.ImageProcessor(''); proc.img = canvas; proc.canvas = canvas; proc.ctx = ctx; window.state.imageData.processor = proc; } else if (window.WPlaceImageProcessor) { const proc = new window.WPlaceImageProcessor(''); proc.img = canvas; proc.canvas = canvas; proc.ctx = ctx; window.state.imageData.processor = proc; } } catch (e) { console.warn('Could not rebuild processor from saved image data:', e); } } else if (savedData.imageData && savedData.imageData.pixelsRef) { // Asynchronously load large pixel data from IndexedDB using the stored reference console.log('🔄 Loading large image 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; this.loadPixelsFromIndexedDB(savedData.imageData.pixelsRef) .then((payload) => { if (!payload || !payload.pixels) { console.warn('⚠️ No pixel payload found in IndexedDB for ref:', savedData.imageData.pixelsRef); this.showAlert?.('⚠️ Saved image pixels not found in local database. Please reload the image file.', 'warning'); return; } try { window.state.imageData.pixels = new Uint8ClampedArray(payload.pixels); window.state.imageLoaded = true; // Rebuild processor and notify modules/UI this._syncModulesAfterStateRestore(); this.showAlert?.('✅ Large image restored from local database.', 'success'); // Attempt overlay restoration if available if (typeof this.restoreOverlayFromData === 'function') { this.restoreOverlayFromData().catch(() => {}); } } catch (err) { console.warn('Failed to apply pixels from IndexedDB:', err); } }) .catch((err) => { console.warn('Failed to load pixels from IndexedDB:', err); }); } else if (savedData.imageData && savedData.imageData.pixelsStripped) { console.warn('⚠️ Saved progress did not include raw pixel data due to quota limits. Please reload the image to resume.'); window.state.imageData = { width: savedData.imageData.width, height: savedData.imageData.height, totalPixels: savedData.imageData.totalPixels, pixels: null, }; window.state.imageLoaded = false; } // Prefer packed form if available; fallback to legacy paintedMap array for backward compatibility if (savedData.paintedMapPacked && savedData.paintedMapPacked.data) { const { width, height, data } = savedData.paintedMapPacked; window.state.paintedMap = this.unpackPaintedMapFromBase64(data, width, height); } else if (savedData.paintedMap) { window.state.paintedMap = savedData.paintedMap.map((row) => Array.from(row)); } // CRITICAL FIX: Notify all modules that state has been restored this._syncModulesAfterStateRestore(); return true; } catch (error) { console.error('Error restoring progress:', error); return false; } } /** * Synchronize all modules after state restore to prevent timing issues * This ensures modules reflect the updated state properly * @private */ _syncModulesAfterStateRestore() { try { console.log('🔄 Synchronizing modules after state restore...'); // Sync image processor if available if (window.globalImageProcessor && window.state.imageData) { // Force refresh image processor state if (typeof window.globalImageProcessor.updateState === 'function') { window.globalImageProcessor.updateState(window.state); } } // Sync overlay manager if available if (window.globalOverlayManager) { // Force refresh overlay state - this ensures overlay knows about the new state if (typeof window.globalOverlayManager.updateState === 'function') { window.globalOverlayManager.updateState(window.state); } } // Extract available colors if not already set (required for color palette functionality) if (!window.state.availableColors || window.state.availableColors.length === 0) { console.log('🎨 Extracting available colors for save file...'); try { const availableColors = this.extractAvailableColors(); if (availableColors && availableColors.length > 0) { window.state.availableColors = availableColors; window.state.colorsChecked = true; console.log(`✅ Extracted ${availableColors.length} available colors`); } else { console.warn('⚠️ No colors available - this might cause issues with overlay display'); } } catch (error) { console.warn('⚠️ Failed to extract colors:', error); } } // Force window.state reference update for any stale references window.state = window.state; // Trigger any watchers console.log('✅ Module synchronization complete'); } catch (error) { console.warn('⚠️ Module synchronization failed:', error); } } /** * Force navigation to saved position to display overlay immediately * @param {Object} region - The region coordinates * @param {Object} position - The pixel position within region * @private */ async _forceNavigateToSavedPosition(region, position) { try { console.log(`🧭 Forcing navigation to region (${region.x}, ${region.y}) pixel (${position.x}, ${position.y})...`); // Calculate world coordinates (region * 1000 + pixel position) const worldX = region.x * 1000 + position.x; const worldY = region.y * 1000 + position.y; console.log(`🌍 World coordinates: (${worldX}, ${worldY})`); // Method 1: Try to find WPlace's camera/viewport controls const attemptCameraControl = () => { // Look for WPlace's camera control functions in global scope const possibleControls = [ 'camera', 'viewport', 'map', 'game', 'wplace', 'app' ]; for (const control of possibleControls) { if (window[control]) { console.log(`🎯 Found potential control: ${control}`, window[control]); // Try common navigation method names const methods = ['setPosition', 'moveTo', 'panTo', 'setView', 'goTo', 'navigate']; for (const method of methods) { if (typeof window[control][method] === 'function') { console.log(`📍 Attempting ${control}.${method}(${worldX}, ${worldY})`); try { window[control][method](worldX, worldY); return true; } catch (e) { console.warn(`❌ ${control}.${method} failed:`, e); } } } } } return false; }; // Method 2: Try updating URL hash (common for canvas-based apps) const attemptUrlNavigation = () => { try { const newHash = `#${worldX},${worldY}`; console.log(`🔗 Attempting URL navigation: ${newHash}`); window.location.hash = newHash; // Also try alternative formats setTimeout(() => { const altHash = `#x=${worldX}&y=${worldY}`; console.log(`🔗 Attempting alternative URL: ${altHash}`); window.location.hash = altHash; }, 100); return true; } catch (e) { console.warn('❌ URL navigation failed:', e); return false; } }; // Method 3: Try triggering scroll/wheel events to force tile loading const attemptScrollTrigger = () => { try { console.log('🖱️ Attempting scroll trigger to force tile loading...'); // Find canvas or main container const canvas = document.querySelector('canvas') || document.body; // Simulate mouse events at target coordinates ['mousedown', 'mousemove', 'mouseup', 'wheel'].forEach(eventType => { const event = new MouseEvent(eventType, { bubbles: true, clientX: worldX % window.innerWidth, clientY: worldY % window.innerHeight, deltaY: eventType === 'wheel' ? 1 : 0 }); canvas.dispatchEvent(event); }); return true; } catch (e) { console.warn('❌ Scroll trigger failed:', e); return false; } }; // Try all methods const success = attemptCameraControl() || attemptUrlNavigation() || attemptScrollTrigger(); if (success) { console.log('✅ Navigation triggered - overlay should display in a moment'); // Wait a bit for tiles to load, then show success message setTimeout(() => { if (typeof this.showAlert === 'function') { this.showAlert('✅ Overlay loaded and positioned automatically!', 'success'); } }, 1000); } else { console.warn('⚠️ Automatic navigation failed - showing manual instruction'); if (typeof this.showAlert === 'function') { this.showAlert(`📍 Navigate to region (${region.x}, ${region.y}) to see your restored overlay`, 'info'); } } } catch (error) { console.error('❌ Failed to force navigation:', error); if (typeof this.showAlert === 'function') { this.showAlert(`📍 Navigate to region (${region.x}, ${region.y}) to see your overlay`, 'info'); } } } saveProgressToFile() { try { const progressData = this.buildProgressData(); const filename = `wplace-bot-progress-${new Date() .toISOString() .slice(0, 19) .replace(/:/g, '-')}.json`; this.createFileDownloader(JSON.stringify(progressData, null, 2), filename); return true; } catch (error) { console.error('Error saving to file:', error); return false; } } async loadProgressFromFile() { try { const data = await this.createFileUploader(); if (!data || !data.state) { throw new Error('Invalid file format'); } const migrated = this.migrateProgress(data); const success = this.restoreProgress(migrated); return success; } catch (error) { console.error('Error loading from file:', error); throw error; } } async restoreOverlayFromData() { if (!window.state.imageLoaded || !window.state.imageData || !window.state.startPosition || !window.state.region) { console.warn('❌ Missing required data for overlay restoration'); return false; } try { console.log('🔄 Restoring overlay from saved data...'); // Recreate ImageBitmap from loaded pixel data (match old version exactly) const imageData = new ImageData( window.state.imageData.pixels, window.state.imageData.width, window.state.imageData.height ); const canvas = new OffscreenCanvas(window.state.imageData.width, window.state.imageData.height); const ctx = canvas.getContext('2d'); ctx.putImageData(imageData, 0, 0); const imageBitmap = await canvas.transferToImageBitmap(); // Set up overlay with restored data (use the Auto-Image.js overlayManager instance) if (window.autoImageOverlayManager) { console.log('📋 Setting image bitmap...'); await window.autoImageOverlayManager.setImage(imageBitmap); console.log('📍 Setting position and region...'); await window.autoImageOverlayManager.setPosition(window.state.startPosition, window.state.region); console.log('� Enabling overlay...'); window.autoImageOverlayManager.enable(); // Update overlay button state const toggleOverlayBtn = document.getElementById('toggleOverlayBtn'); if (toggleOverlayBtn) { toggleOverlayBtn.disabled = false; toggleOverlayBtn.classList.add('active'); } console.log('✅ Overlay restored from data'); return true; } else { console.error('❌ autoImageOverlayManager not available'); return false; } } catch (error) { console.error('❌ Failed to restore overlay from data:', error); return false; } } updateCoordinateUI({ mode, directionControls, snakeControls, blockControls }) { const isLinear = mode === 'rows' || mode === 'columns'; const isBlock = mode === 'blocks' || mode === 'shuffle-blocks'; if (directionControls) directionControls.style.display = isLinear ? 'block' : 'none'; if (snakeControls) snakeControls.style.display = isLinear ? 'block' : 'none'; if (blockControls) blockControls.style.display = isBlock ? 'block' : 'none'; } /** * Force navigation to saved position to display overlay immediately * @param {Object} region - The region coordinates * @param {Object} position - The pixel position within region * @private */ async _forceNavigateToSavedPosition(region, position) { try { console.log(`🧭 Forcing navigation to region (${region.x}, ${region.y}) pixel (${position.x}, ${position.y})...`); // Calculate world coordinates (region * 1000 + pixel position) const worldX = region.x * 1000 + position.x; const worldY = region.y * 1000 + position.y; console.log(`🌍 World coordinates: (${worldX}, ${worldY})`); // Method 1: Try to find WPlace's camera/viewport controls const attemptCameraControl = () => { // Look for WPlace's camera control functions in global scope const possibleControls = [ 'camera', 'viewport', 'map', 'game', 'wplace', 'app' ]; for (const control of possibleControls) { if (window[control]) { console.log(`🎯 Found potential control: ${control}`, window[control]); // Try common navigation method names const methods = ['setPosition', 'moveTo', 'panTo', 'setView', 'goTo', 'navigate']; for (const method of methods) { if (typeof window[control][method] === 'function') { console.log(`📍 Attempting ${control}.${method}(${worldX}, ${worldY})`); try { window[control][method](worldX, worldY); return true; } catch (e) { console.warn(`❌ ${control}.${method} failed:`, e); } } } } } return false; }; // Method 2: Try updating URL hash (common for canvas-based apps) const attemptUrlNavigation = () => { try { const newHash = `#${worldX},${worldY}`; console.log(`🔗 Attempting URL navigation: ${newHash}`); window.location.hash = newHash; // Also try alternative formats setTimeout(() => { const altHash = `#x=${worldX}&y=${worldY}`; console.log(`🔗 Attempting alternative URL: ${altHash}`); window.location.hash = altHash; }, 100); return true; } catch (e) { console.warn('❌ URL navigation failed:', e); return false; } }; // Method 3: Try triggering scroll/wheel events to force tile loading const attemptScrollTrigger = () => { try { console.log('🖱️ Attempting scroll trigger to force tile loading...'); // Find canvas or main container const canvas = document.querySelector('canvas') || document.body; // Simulate mouse events at target coordinates ['mousedown', 'mousemove', 'mouseup', 'wheel'].forEach(eventType => { const event = new MouseEvent(eventType, { bubbles: true, clientX: worldX % window.innerWidth, clientY: worldY % window.innerHeight, deltaY: eventType === 'wheel' ? 1 : 0 }); canvas.dispatchEvent(event); }); return true; } catch (e) { console.warn('❌ Scroll trigger failed:', e); return false; } }; // Try all methods const success = attemptCameraControl() || attemptUrlNavigation() || attemptScrollTrigger(); if (success) { console.log('✅ Navigation triggered - overlay should display in a moment'); // Wait a bit for tiles to load, then show success message setTimeout(() => { if (typeof this.showAlert === 'function') { this.showAlert('✅ Overlay loaded and positioned automatically!', 'success'); } }, 1000); } else { console.warn('⚠️ Automatic navigation failed - showing manual instruction'); if (typeof this.showAlert === 'function') { this.showAlert(`📍 Navigate to region (${region.x}, ${region.y}) to see your restored overlay`, 'info'); } } } catch (error) { console.error('❌ Failed to force navigation:', error); if (typeof this.showAlert === 'function') { this.showAlert(`📍 Navigate to region (${region.x}, ${region.y}) to see your overlay`, 'info'); } } } } // Global instance with safety checks try { window.globalUtilsManager = new WPlaceUtilsManager(); console.log('✅ WPlaceUtilsManager initialized successfully'); } catch (error) { console.error('❌ Failed to initialize WPlaceUtilsManager:', error); } // Additional safety: ensure the instance is available for other scripts if (!window.globalUtilsManager) { console.warn('⚠️ globalUtilsManager not available, attempting to reinitialize...'); setTimeout(() => { try { window.globalUtilsManager = new WPlaceUtilsManager(); console.log('✅ WPlaceUtilsManager reinitialized successfully'); } catch (error) { console.error('❌ Failed to reinitialize WPlaceUtilsManager:', error); } }, 100); } // === IndexedDB helpers for large pixel storage === WPlaceUtilsManager.prototype._openPixelDB = function() { return new Promise((resolve, reject) => { try { const request = indexedDB.open('wplace-bot-db', 1); request.onupgradeneeded = () => { const db = request.result; if (!db.objectStoreNames.contains('pixels')) { const store = db.createObjectStore('pixels', { keyPath: 'ref' }); try { store.createIndex('createdAt', 'createdAt', { unique: false }); } catch {} } }; request.onsuccess = () => resolve(request.result); request.onerror = () => reject(request.error); } catch (e) { reject(e); } }); }; WPlaceUtilsManager.prototype.generatePixelsRef = function(width, height) { const rand = Math.random().toString(36).slice(2, 8); return `img_${width}x${height}_${Date.now().toString(36)}_${rand}`; }; WPlaceUtilsManager.prototype.storePixelsInIndexedDB = async function(ref, payload) { try { const db = await this._openPixelDB(); return await new Promise((resolve, reject) => { const tx = db.transaction('pixels', 'readwrite'); const store = tx.objectStore('pixels'); const data = { ref, width: payload.width, height: payload.height, totalPixels: payload.totalPixels, pixels: payload.pixels, createdAt: Date.now(), }; const req = store.put(data); req.onsuccess = () => resolve(true); req.onerror = () => reject(req.error); }); } catch (e) { console.warn('IDB storePixels failed:', e); return false; } }; WPlaceUtilsManager.prototype.loadPixelsFromIndexedDB = async function(ref) { try { const db = await this._openPixelDB(); return await new Promise((resolve, reject) => { const tx = db.transaction('pixels', 'readonly'); const store = tx.objectStore('pixels'); const req = store.get(ref); req.onsuccess = () => resolve(req.result || null); req.onerror = () => reject(req.error); }); } catch (e) { console.warn('IDB loadPixels failed:', e); return null; } };