diff --git a/Auto-Image.js b/Auto-Image.js index 839f89c..21cddd0 100644 --- a/Auto-Image.js +++ b/Auto-Image.js @@ -13,42 +13,78 @@ PAINTING_SPEED_ENABLED: false, AUTO_CAPTCHA_ENABLED: false, // Disabled by default COOLDOWN_CHARGE_THRESHOLD: 1, // Default wait threshold + OVERLAY: { + OPACITY_DEFAULT: 0.6, + BLUE_MARBLE_DEFAULT: false, + }, // --- START: Color data from colour-converter.js --- - COLOR_PALETTE: [ - [0,0,0],[60,60,60],[120,120,120],[170,170,170],[210,210,210],[255,255,255], - [96,0,24],[165,14,30],[237,28,36],[250,128,114],[228,92,26],[255,127,39],[246,170,9], - [249,221,59],[255,250,188],[156,132,49],[197,173,49],[232,212,95],[74,107,58],[90,148,74],[132,197,115], - [14,185,104],[19,230,123],[135,255,94],[12,129,110],[16,174,166],[19,225,190],[15,121,159],[96,247,242], - [187,250,242],[40,80,158],[64,147,228],[125,199,255],[77,49,184],[107,80,246],[153,177,251], - [74,66,132],[122,113,196],[181,174,241],[170,56,185],[224,159,249], - [203,0,122],[236,31,128],[243,141,169],[155,82,73],[209,128,120],[250,182,164], - [104,70,52],[149,104,42],[219,164,99],[123,99,82],[156,132,107],[214,181,148], - [209,128,81],[248,178,119],[255,197,165],[109,100,63],[148,140,107],[205,197,158], - [51,57,65],[109,117,141],[179,185,209] - ], - COLOR_NAMES: { - "0,0,0": "Black", "60,60,60": "Dark Gray", "120,120,120": "Gray", "210,210,210": "Light Gray", "255,255,255": "White", - "96,0,24": "Deep Red", "237,28,36": "Red", "255,127,39": "Orange", "246,170,9": "Gold", "249,221,59": "Yellow", - "255,250,188": "Light Yellow", "14,185,104": "Dark Green", "19,230,123": "Green", "135,255,94": "Light Green", - "12,129,110": "Dark Teal", "16,174,166": "Teal", "19,225,190": "Light Teal", "96,247,242": "Cyan", "40,80,158": "Dark Blue", - "64,147,228": "Blue", "107,80,246": "Indigo", "153,177,251": "Light Indigo", "120,12,153": "Dark Purple", - "170,56,185": "Purple", "224,159,249": "Light Purple", "203,0,122": "Dark Pink", "236,31,128": "Pink", - "243,141,169": "Light Pink", "104,70,52": "Dark Brown", "149,104,42": "Brown", "248,178,119": "Beige", - "170,170,170": "Medium Gray", "165,14,30": "Dark Red", "250,128,114": "Light Red", "228,92,26": "Dark Orange", - "156,132,49": "Dark Goldenrod", "197,173,49": "Goldenrod", "232,212,95": "Light Goldenrod", "74,107,58": "Dark Olive", - "90,148,74": "Olive", "132,197,115": "Light Olive", "15,121,159": "Dark Cyan", "187,250,242": "Light Cyan", - "125,199,255": "Light Blue", "77,49,184": "Dark Indigo", "74,66,132": "Dark Slate Blue", "122,113,196": "Slate Blue", - "181,174,241": "Light Slate Blue", "155,82,73": "Dark Peach", "209,128,120": "Peach", "250,182,164": "Light Peach", - "219,164,99": "Light Brown", "123,99,82": "Dark Tan", "156,132,107": "Tan", "214,181,148": "Light Tan", - "209,128,81": "Dark Beige", "255,197,165": "Light Beige", "109,100,63": "Dark Stone", "148,140,107": "Stone", - "205,197,158": "Light Stone", "51,57,65": "Dark Slate", "109,117,141": "Slate", "179,185,209": "Light Slate", + // 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: 32, name: 'Medium Gray', rgb: {r: 160, g: 160, b: 160}}, + 4: {id: 4, name: 'Light Gray', rgb: {r: 210, g: 210, b: 210}}, + 5: {id: 5, name: 'White', rgb: {r: 255, g: 255, b: 255}}, + 6: {id: 6, name: 'Deep Red', rgb: {r: 96, g: 0, b: 24}}, + 7: {id: 33, name: 'Dark Red', rgb: {r: 128, g: 0, b: 0}}, + 8: {id: 7, name: 'Red', rgb: {r: 237, g: 28, b: 36}}, + 9: {id: 34, name: 'Light Red', rgb: {r: 255, g: 128, b: 128}}, + 10: {id: 35, name: 'Dark Orange', rgb: {r: 204, g: 85, b: 0}}, + 11: {id: 8, name: 'Orange', rgb: {r: 255, g: 127, b: 39}}, + 12: {id: 9, name: 'Gold', rgb: {r: 246, g: 170, b: 9}}, + 13: {id: 10, name: 'Yellow', rgb: {r: 249, g: 221, b: 59}}, + 14: {id: 11, name: 'Light Yellow', rgb: {r: 255, g: 250, b: 188}}, + 15: {id: 37, name: 'Dark Goldenrod', rgb: {r: 184, g: 134, b: 11}}, + 16: {id: 38, name: 'Goldenrod', rgb: {r: 218, g: 165, b: 32}}, + 17: {id: 39, name: 'Light Goldenrod', rgb: {r: 250, g: 250, b: 210}}, + 18: {id: 40, name: 'Dark Olive', rgb: {r: 85, g: 107, b: 47}}, + 19: {id: 41, name: 'Olive', rgb: {r: 128, g: 128, b: 0}}, + 20: {id: 42, name: 'Light Olive', rgb: {r: 192, g: 192, b: 128}}, + 21: {id: 12, name: 'Dark Green', rgb: {r: 14, g: 185, b: 104}}, + 22: {id: 13, name: 'Green', rgb: {r: 19, g: 230, b: 123}}, + 23: {id: 14, name: 'Light Green', rgb: {r: 135, g: 255, b: 94}}, + 24: {id: 15, name: 'Dark Teal', rgb: {r: 12, g: 129, b: 110}}, + 25: {id: 16, name: 'Teal', rgb: {r: 16, g: 174, b: 166}}, + 26: {id: 17, name: 'Light Teal', rgb: {r: 19, g: 225, b: 190}}, + 27: {id: 43, name: 'Dark Cyan', rgb: {r: 0, g: 139, b: 139}}, + 28: {id: 20, name: 'Cyan', rgb: {r: 96, g: 247, b: 242}}, + 29: {id: 44, name: 'Light Cyan', rgb: {r: 224, g: 255, b: 255}}, + 30: {id: 18, name: 'Dark Blue', rgb: {r: 40, g: 80, b: 158}}, + 31: {id: 19, name: 'Blue', rgb: {r: 64, g: 147, b: 228}}, + 32: {id: 45, name: 'Light Blue', rgb: {r: 173, g: 216, b: 230}}, + 33: {id: 46, name: 'Dark Indigo', rgb: {r: 75, g: 0, b: 130}}, + 34: {id: 21, name: 'Indigo', rgb: {r: 107, g: 80, b: 246}}, + 35: {id: 22, name: 'Light Indigo', rgb: {r: 153, g: 177, b: 251}}, + 36: {id: 47, name: 'Dark Slate Blue', rgb: {r: 72, g: 61, b: 139}}, + 37: {id: 48, name: 'Slate Blue', rgb: {r: 106, g: 90, b: 205}}, + 38: {id: 49, name: 'Light Slate Blue', rgb: {r: 132, g: 112, b: 255}}, + 39: {id: 23, name: 'Dark Purple', rgb: {r: 120, g: 12, b: 153}}, + 40: {id: 24, name: 'Purple', rgb: {r: 170, g: 56, b: 185}}, + 41: {id: 25, name: 'Light Purple', rgb: {r: 224, g: 159, b: 249}}, + 42: {id: 26, name: 'Dark Pink', rgb: {r: 203, g: 0, b: 122}}, + 43: {id: 27, name: 'Pink', rgb: {r: 236, g: 31, b: 128}}, + 44: {id: 28, name: 'Light Pink', rgb: {r: 243, g: 141, b: 169}}, + 45: {id: 53, name: 'Dark Peach', rgb: {r: 255, g: 140, b: 105}}, + 46: {id: 54, name: 'Peach', rgb: {r: 255, g: 218, b: 185}}, + 47: {id: 55, name: 'Light Peach', rgb: {r: 255, g: 239, b: 213}}, + 48: {id: 29, name: 'Dark Brown', rgb: {r: 104, g: 70, b: 52}}, + 49: {id: 30, name: 'Brown', rgb: {r: 149, g: 104, b: 42}}, + 50: {id: 50, name: 'Light Brown', rgb: {r: 205, g: 133, b: 63}}, + 51: {id: 56, name: 'Dark Tan', rgb: {r: 139, g: 69, b: 19}}, + 52: {id: 57, name: 'Tan', rgb: {r: 210, g: 180, b: 140}}, + 53: {id: 36, name: 'Light Tan', rgb: {r: 238, g: 203, b: 173}}, + 54: {id: 51, name: 'Dark Beige', rgb: {r: 160, g: 130, b: 98}}, + 55: {id: 31, name: 'Beige', rgb: {r: 248, g: 178, b: 119}}, + 56: {id: 52, name: 'Light Beige', rgb: {r: 255, g: 197, b: 165}}, + 57: {id: 61, name: 'Dark Stone', rgb: {r: 105, g: 105, b: 105}}, + 58: {id: 62, name: 'Stone', rgb: {r: 128, g: 128, b: 128}}, + 59: {id: 63, name: 'Light Stone', rgb: {r: 176, g: 176, b: 176}}, + 60: {id: 58, name: 'Dark Slate', rgb: {r: 47, g: 79, b: 79}}, + 61: {id: 59, name: 'Slate', rgb: {r: 112, g: 128, b: 144}}, + 62: {id: 60, name: 'Light Slate', rgb: {r: 176, g: 196, b: 222}}, + 63: {id: 0, name: 'Transparent', rgb: null} }, - PAID_COLORS: new Set([ - "170,170,170", "165,14,30", "250,128,114", "228,92,26", "156,132,49", "197,173,49", "232,212,95", "74,107,58", - "90,148,74", "132,197,115", "15,121,159", "187,250,242", "125,199,255", "77,49,184", "74,66,132", "122,113,196", - "181,174,241", "155,82,73", "209,128,120", "250,182,164", "219,164,99", "123,99,82", "156,132,107", "214,181,148", - "209,128,81", "255,197,165", "109,100,63", "148,140,107", "205,197,158", "51,57,65", "109,117,141", "179,185,209", - ]), // --- END: Color data --- // Optimized CSS Classes for reuse CSS_CLASSES: { @@ -176,6 +212,7 @@ const TEXT = { en: { title: "WPlace Auto-Image", + toggleOverlay: "Toggle Overlay", scanColors: "Scan Colors", uploadImage: "Upload Image", resizeImage: "Resize Image", @@ -524,6 +561,76 @@ automation: "Automatisation", noChargesThreshold: "โŒ› En attente que les charges atteignent {threshold}. Actuel: {current}. Prochaine dans {time}...", }, + id: { + title: "WPlace Auto-Image", + scanColors: "Pindai Warna", + uploadImage: "Unggah Gambar", + resizeImage: "Ubah Ukuran Gambar", + selectPosition: "Pilih Posisi", + startPainting: "Mulai Melukis", + stopPainting: "Berhenti Melukis", + checkingColors: "๐Ÿ” Memeriksa warna yang tersedia...", + 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", + imageError: "โŒ Kesalahan saat memuat gambar", + selectPositionAlert: "Lukis piksel pertama di lokasi tempat karya seni akan dimulai!", + waitingPosition: "๐Ÿ‘† Menunggu Anda melukis piksel referensi...", + positionSet: "โœ… Posisi berhasil diatur!", + positionTimeout: "โŒ Waktu habis untuk memilih posisi", + startPaintingMsg: "๐ŸŽจ Mulai melukis...", + paintingProgress: "๐Ÿงฑ Progres: {painted}/{total} piksel...", + noCharges: "โŒ› Tidak ada muatan. Menunggu {time}...", + paintingStopped: "โน๏ธ Melukis dihentikan oleh pengguna", + paintingComplete: "โœ… Melukis selesai! {count} piksel telah dilukis.", + paintingError: "โŒ Kesalahan selama melukis", + missingRequirements: "โŒ Unggah gambar dan pilih posisi terlebih dahulu", + progress: "Progres", + pixels: "Piksel", + charges: "Muatan", + estimatedTime: "Perkiraan waktu", + initMessage: "Klik 'Unggah Gambar' untuk memulai", + waitingInit: "Menunggu inisialisasi...", + resizeSuccess: "โœ… Gambar berhasil diubah ukurannya menjadi {width}x{height}", + paintingPaused: "โธ๏ธ Melukis dijeda di posisi X: {x}, Y: {y}", + captchaNeeded: "โ— Token CAPTCHA diperlukan. Lukis satu piksel secara manual untuk melanjutkan.", + saveData: "Simpan Progres", + loadData: "Muat Progres", + saveToFile: "Simpan ke File", + loadFromFile: "Muat dari File", + dataManager: "Data", + autoSaved: "โœ… Progres disimpan secara otomatis", + dataLoaded: "โœ… Progres berhasil dimuat", + fileSaved: "โœ… Berhasil disimpan ke file", + fileLoaded: "โœ… Berhasil dimuat dari file", + noSavedData: "โŒ Tidak ditemukan progres yang disimpan", + savedDataFound: "โœ… Progres yang disimpan ditemukan! Muat untuk melanjutkan?", + savedDate: "Disimpan pada: {date}", + clickLoadToContinue: "Klik 'Muat Progres' untuk melanjutkan.", + fileError: "โŒ Kesalahan saat memproses file", + invalidFileFormat: "โŒ Format file tidak valid", + paintingSpeed: "Kecepatan Melukis", + pixelsPerSecond: "piksel/detik", + speedSetting: "Kecepatan: {speed} piksel/detik", + settings: "Pengaturan", + botSettings: "Pengaturan Bot", + close: "Tutup", + language: "Bahasa", + themeSettings: "Pengaturan Tema", + themeSettingsDesc: "Pilih tema warna favorit Anda untuk antarmuka.", + languageSelectDesc: "Pilih bahasa yang Anda inginkan. Perubahan akan berlaku segera.", + autoCaptcha: "Penyelesai CAPTCHA Otomatis", + autoCaptchaDesc: "Mencoba menyelesaikan CAPTCHA secara otomatis dengan mensimulasikan penempatan piksel manual saat token kedaluwarsa.", + applySettings: "Terapkan Pengaturan", + settingsSaved: "โœ… Pengaturan berhasil disimpan!", + cooldownSettings: "Pengaturan Cooldown", + waitCharges: "Tunggu hingga muatan mencapai", + captchaSolving: "๐Ÿค– Mencoba menyelesaikan CAPTCHA...", + captchaFailed: "โŒ Gagal menyelesaikan CAPTCHA. Lukis satu piksel secara manual.", + automation: "Automasi", + noChargesThreshold: "โŒ› Menunggu muatan mencapai {threshold}. Saat ini: {current}. Berikutnya dalam {time}...", + }, } // GLOBAL STATE @@ -551,53 +658,280 @@ language: "en", paintingSpeed: CONFIG.PAINTING_SPEED.DEFAULT, // pixels per second cooldownChargeThreshold: CONFIG.COOLDOWN_CHARGE_THRESHOLD, + overlayOpacity: CONFIG.OVERLAY.OPACITY_DEFAULT, + blueMarbleEnabled: CONFIG.OVERLAY.BLUE_MARBLE_DEFAULT, } // Placeholder for the resize preview update function let _updateResizePreview = () => {}; - // Turnstile token handling (promise-based) inspired by external logic - let turnstileToken = null - let _resolveToken = null - let tokenPromise = new Promise((resolve) => { _resolveToken = resolve }) - - function setTurnstileToken(t) { - if (_resolveToken) { - _resolveToken(t) - _resolveToken = null + // --- OVERLAY UPDATE: New OverlayManager class to handle all overlay logic --- + class OverlayManager { + constructor() { + this.isEnabled = false; + this.startCoords = null; // { region: {x, y}, pixel: {x, y} } + this.imageBitmap = null; + this.chunkedTiles = new Map(); // Map<"tileX,tileY", ImageBitmap> + this.tileSize = 1000; } - turnstileToken = t - } - async function ensureToken() { - if (!turnstileToken) { - updateUI("captchaNeeded", "error") - Utils.showAlert(Utils.t("captchaNeeded"), "error") - try { await tokenPromise } catch (_) {} + toggle() { + this.isEnabled = !this.isEnabled; + console.log(`Overlay ${this.isEnabled ? 'enabled' : 'disabled'}.`); + return this.isEnabled; } - return turnstileToken - } - // Intercept fetch to capture Turnstile token from pixel placement requests - const originalFetch = window.fetch - window.fetch = async (url, options) => { - if (typeof url === "string" && url.includes("https://backend.wplace.live/s0/pixel/")) { - try { - const payload = JSON.parse(options.body) - if (payload.t) { - console.log("โœ… Turnstile Token Captured:", payload.t) - setTurnstileToken(payload.t) - if (document.querySelector("#statusText")?.textContent.includes("CAPTCHA")) { - Utils.showAlert("Token captured successfully! You can start the bot now.", "success") - updateUI("colorsFound", "success", { count: state.availableColors.length }) - } + enable() { this.isEnabled = true; } + disable() { this.isEnabled = false; } + clear() { + this.disable(); + this.imageBitmap = null; + this.chunkedTiles.clear(); + } + + async setImage(imageBitmap) { + this.imageBitmap = imageBitmap; + if (this.imageBitmap && this.startCoords) { + await this.processImageIntoChunks(); } - } catch (_) { /* ignore */ } } - return originalFetch(url, options) - } - // LANGUAGE DETECTION + async setPosition(startPosition, region) { + if (!startPosition || !region) { + this.startCoords = null; + this.chunkedTiles.clear(); + return; + } + this.startCoords = { region, pixel: startPosition }; + if (this.imageBitmap) { + await this.processImageIntoChunks(); + } + } + + // --- OVERLAY UPDATE: Simplified chunking logic for solid, semi-transparent overlay --- + async processImageIntoChunks() { + if (!this.imageBitmap || !this.startCoords) return; + + this.chunkedTiles.clear(); + const { width: imageWidth, height: imageHeight } = this.imageBitmap; + const { x: startPixelX, y: startPixelY } = this.startCoords.pixel; + const { x: startRegionX, y: startRegionY } = this.startCoords.region; + + const endPixelX = startPixelX + imageWidth; + const endPixelY = startPixelY + imageHeight; + + const startTileX = startRegionX + Math.floor(startPixelX / this.tileSize); + const startTileY = startRegionY + Math.floor(startPixelY / this.tileSize); + const endTileX = startRegionX + Math.floor(endPixelX / this.tileSize); + const endTileY = startRegionY + Math.floor(endPixelY / this.tileSize); + + for (let ty = startTileY; ty <= endTileY; ty++) { + for (let tx = startTileX; tx <= endTileX; tx++) { + const tileKey = `${tx},${ty}`; + + // Calculate the portion of the image that overlaps with this tile + const imgStartX = (tx - startRegionX) * this.tileSize - startPixelX; + const imgStartY = (ty - startRegionY) * this.tileSize - startPixelY; + + // Crop coordinates within the source image + const sX = Math.max(0, imgStartX); + const sY = Math.max(0, imgStartY); + const sW = Math.min(imageWidth - sX, this.tileSize - (sX - imgStartX)); + const sH = Math.min(imageHeight - sY, this.tileSize - (sY - imgStartY)); + + if (sW <= 0 || sH <= 0) continue; + + // Destination coordinates on the new chunk canvas + const dX = Math.max(0, -imgStartX); + const dY = Math.max(0, -imgStartY); + + const chunkCanvas = new OffscreenCanvas(this.tileSize, this.tileSize); + const chunkCtx = chunkCanvas.getContext('2d'); + chunkCtx.imageSmoothingEnabled = false; + + chunkCtx.drawImage(this.imageBitmap, sX, sY, sW, sH, dX, dY, sW, sH); + + // --- NEW: BLUE MARBLE EFFECT --- + if (state.blueMarbleEnabled) { + const imageData = chunkCtx.getImageData(0, 0, this.tileSize, this.tileSize); + const data = imageData.data; + for (let pixelY = 0; pixelY < this.tileSize; pixelY++) { + for (let pixelX = 0; pixelX < this.tileSize; pixelX++) { + const canvasX = pixelX; + const canvasY = pixelY; + const imageX = canvasX - dX; + const imageY = canvasY - dY; + + if ((imageX + imageY) % 2 === 0) { + const index = (canvasY * this.tileSize + canvasX) * 4; + if (data[index + 3] > 0) { + data[index + 3] = 0; + } + } + } + } + chunkCtx.putImageData(imageData, 0, 0); + } + + const chunkBitmap = await chunkCanvas.transferToImageBitmap(); + this.chunkedTiles.set(tileKey, chunkBitmap); + } + } + + console.log(`Overlay processed into ${this.chunkedTiles.size} chunks.`); + } + + // --- OVERLAY UPDATE: Simplified compositing logic for solid, semi-transparent overlay --- + async processAndRespondToTileRequest(eventData) { + const { endpoint, blobID, blobData } = eventData; + + let finalBlob = blobData; + + if (this.isEnabled && this.chunkedTiles.size > 0) { + const tileMatch = endpoint.match(/(\d+)\/(\d+)\.png/); + if (tileMatch) { + const tileX = parseInt(tileMatch[1], 10); + const tileY = parseInt(tileMatch[2], 10); + const tileKey = `${tileX},${tileY}`; + + const chunkBitmap = this.chunkedTiles.get(tileKey); + if (chunkBitmap) { + try { + const originalTileBitmap = await createImageBitmap(blobData); + const canvas = new OffscreenCanvas(originalTileBitmap.width, originalTileBitmap.height); + const ctx = canvas.getContext('2d'); + ctx.imageSmoothingEnabled = false; + + // Draw original tile first + ctx.drawImage(originalTileBitmap, 0, 0); + + // Set opacity and draw our solid overlay chunk on top + ctx.globalAlpha = state.overlayOpacity; + ctx.drawImage(chunkBitmap, 0, 0); + + finalBlob = await canvas.convertToBlob({ type: 'image/png' }); + } catch (e) { + console.error("Error compositing overlay:", e); + } + } + } + } + + // Send the (possibly modified) blob back to the injected script + window.postMessage({ + source: 'auto-image-overlay', + blobID: blobID, + blobData: finalBlob + }, '*'); + } +} + +const overlayManager = new OverlayManager(); + +// Turnstile token handling (promise-based) inspired by external logic +let turnstileToken = null +let _resolveToken = null +let tokenPromise = new Promise((resolve) => { _resolveToken = resolve }) + +function setTurnstileToken(t) { + if (_resolveToken) { + _resolveToken(t) + _resolveToken = null + } + turnstileToken = t +} + +async function ensureToken() { + if (!turnstileToken) { + updateUI("captchaNeeded", "error") + Utils.showAlert(Utils.t("captchaNeeded"), "error") + try { await tokenPromise } catch (_) {} + } + return turnstileToken +} + +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; + window.fetch = async function(...args) { + const response = await originalFetch.apply(this, args); + const url = (args[0] instanceof Request) ? args[0].url : args[0]; + + 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:", payload.t); + 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, token } = event.data; + + if (source === 'auto-image-tile' && endpoint && blobID && blobData) { + overlayManager.processAndRespondToTileRequest(event.data); + } + + if (source === 'turnstile-capture' && token) { + setTurnstileToken(token); + if (document.querySelector("#statusText")?.textContent.includes("CAPTCHA")) { + Utils.showAlert("Token captured successfully! You can start the bot now.", "success"); + updateUI("colorsFound", "success", { count: state.availableColors.length }); + } + } +}); + async function detectLanguage() { try { const response = await fetch("https://backend.wplace.live/me", { @@ -624,11 +958,9 @@ return null; }, - // Optimized DOM creation helpers createElement: (tag, props = {}, children = []) => { const element = document.createElement(tag) - // Set properties efficiently Object.entries(props).forEach(([key, value]) => { if (key === 'style' && typeof value === 'object') { Object.assign(element.style, value) @@ -641,7 +973,6 @@ } }) - // Append children efficiently if (typeof children === 'string') { element.textContent = children } else if (Array.isArray(children)) { @@ -657,7 +988,6 @@ return element }, - // Create button with common styling createButton: (id, text, icon, onClick, style = CONFIG.CSS_CLASSES.BUTTON_PRIMARY) => { const button = Utils.createElement('button', { id: id, @@ -727,11 +1057,16 @@ 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)), - // The color metric from colour-converter.js for higher accuracy findClosestPaletteColor: (r, g, b, palette) => { let menorDist = Infinity; let cor = [0, 0, 0]; - if (!palette || palette.length === 0) return cor; + if (!palette || palette.length === 0) { + // If no palette provided, use all available colors from COLOR_MAP + const availableColors = Object.values(CONFIG.COLOR_MAP) + .filter(color => color.rgb !== null) + .map(color => [color.rgb.r, color.rgb.g, color.rgb.b]); + palette = availableColors; + } for (let i = 0; i < palette.length; i++) { const [pr, pg, pb] = palette[i]; @@ -836,19 +1171,15 @@ calculateEstimatedTime: (remainingPixels, charges, cooldown) => { if (remainingPixels <= 0) return 0 - // Calculate time based on painting speed (pixels per second) const paintingSpeedDelay = state.paintingSpeed > 0 ? (1000 / state.paintingSpeed) : 1000 - const timeFromSpeed = remainingPixels * paintingSpeedDelay // ms + const timeFromSpeed = remainingPixels * paintingSpeedDelay - // Calculate time based on charges and cooldown const cyclesNeeded = Math.ceil(remainingPixels / Math.max(charges, 1)) - const timeFromCharges = cyclesNeeded * cooldown // ms + const timeFromCharges = cyclesNeeded * cooldown - // Return the maximum of both calculations (the limiting factor) return Math.max(timeFromSpeed, timeFromCharges) }, - // Save/Load Progress Functions saveProgress: () => { try { const progressData = { @@ -904,10 +1235,8 @@ restoreProgress: (savedData) => { try { - // Restore state Object.assign(state, savedData.state) - // Restore image data if (savedData.imageData) { state.imageData = { ...savedData.imageData, @@ -915,7 +1244,6 @@ } } - // Restore painted map if (savedData.paintedMap) { state.paintedMap = savedData.paintedMap.map((row) => Array.from(row)) } @@ -1101,10 +1429,8 @@ const colorCache = new Map() function findClosestColor(targetRgb, availableColors) { - // Create cache key from RGB values const cacheKey = `${targetRgb[0]},${targetRgb[1]},${targetRgb[2]}` - // Check cache first if (colorCache.has(cacheKey)) { return colorCache.get(cacheKey) } @@ -1121,23 +1447,18 @@ let minDistance = Number.POSITIVE_INFINITY let closestColorId = availableColors[0]?.id || 1 - // Use optimized loop for better performance for (let i = 0; i < availableColors.length; i++) { const color = availableColors[i] const distance = Utils.colorDistance(targetRgb, color.rgb) if (distance < minDistance) { minDistance = distance closestColorId = color.id - - // If perfect match, break early if (distance === 0) break } } - // Cache the result for future use colorCache.set(cacheKey, closestColorId) - // Limit cache size to prevent memory leaks if (colorCache.size > 10000) { const firstKey = colorCache.keys().next().value colorCache.delete(firstKey) @@ -1151,91 +1472,131 @@ let updateStats = () => {} let updateDataButtons = () => {} - // --- START: Color Palette Functions --- function updateActiveColorPalette() { state.activeColorPalette = []; const activeSwatches = document.querySelectorAll('.wplace-color-swatch.active'); if (activeSwatches) { activeSwatches.forEach(swatch => { - const rgb = swatch.getAttribute('data-rgb').split(',').map(Number); - state.activeColorPalette.push(rgb); + const rgbStr = swatch.getAttribute('data-rgb'); + if (rgbStr) { + const rgb = rgbStr.split(',').map(Number); + state.activeColorPalette.push(rgb); + } }); } - // If the resize dialog is open, update its preview if (document.querySelector('.resize-container')?.style.display === 'block') { _updateResizePreview(); } } - function toggleAllColors(select, isPaid) { - const selector = isPaid ? '.wplace-color-swatch.paid' : '.wplace-color-swatch:not(.paid)'; - const swatches = document.querySelectorAll(selector); + function toggleAllColors(select, showingUnavailable = false) { + const swatches = document.querySelectorAll('.wplace-color-swatch'); if (swatches) { swatches.forEach(swatch => { - swatch.classList.toggle('active', select); + // 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 initializeColorPalette(container) { - const freeContainer = container.querySelector('#colors-free'); - const paidContainer = container.querySelector('#colors-paid'); - if (!freeContainer || !paidContainer) return; + const colorsContainer = container.querySelector('#colors-container'); + const showAllToggle = container.querySelector('#showAllColorsToggle'); + if (!colorsContainer) return; - freeContainer.innerHTML = ''; - paidContainer.innerHTML = ''; + // 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 = '
Upload an image first to capture available colors
'; + return; + } - // Create a unique set of colors to avoid duplicates - const uniqueColors = [...new Set(CONFIG.COLOR_PALETTE.map(JSON.stringify))].map(JSON.parse); + function populateColors(showUnavailable = false) { + colorsContainer.innerHTML = ''; + let availableCount = 0; + let totalCount = 0; - uniqueColors.forEach(rgb => { - const key = rgb.join(','); - const name = CONFIG.COLOR_NAMES[key] || `rgb(${key})`; - const isPaid = CONFIG.PAID_COLORS.has(key); + // Convert COLOR_MAP to array and filter out transparent + const allColors = Object.values(CONFIG.COLOR_MAP).filter(color => color.rgb !== null); - const colorItem = Utils.createElement('div', { className: 'wplace-color-item' }); - const swatch = Utils.createElement('button', { - className: `wplace-color-swatch ${isPaid ? 'paid' : ''}`, - title: name, - 'data-rgb': key, - }); - swatch.style.backgroundColor = `rgb(${key})`; + 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 + ); - const nameLabel = Utils.createElement('span', { className: 'wplace-color-item-name' }, name); + // If not showing all colors and this color is not available, skip it + if (!showUnavailable && !isAvailable) { + return; + } - // Default state: free are active, paid are not - if (!isPaid) { - swatch.classList.add('active'); - } + if (isAvailable) availableCount++; - swatch.addEventListener('click', () => { - swatch.classList.toggle('active'); - updateActiveColorPalette(); + 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); }); - colorItem.appendChild(swatch); - colorItem.appendChild(nameLabel); + updateActiveColorPalette(); + } - if (isPaid) { - paidContainer.appendChild(colorItem); - } else { - freeContainer.appendChild(colorItem); - } - }); + // Initialize with only available colors + populateColors(false); - // Add event listeners for master buttons - container.querySelector('#selectAllFreeBtn')?.addEventListener('click', () => toggleAllColors(true, false)); - container.querySelector('#unselectAllFreeBtn')?.addEventListener('click', () => toggleAllColors(false, false)); - container.querySelector('#selectAllPaidBtn')?.addEventListener('click', () => toggleAllColors(true, true)); - container.querySelector('#unselectAllPaidBtn')?.addEventListener('click', () => toggleAllColors(false, true)); + // Add toggle functionality + if (showAllToggle) { + showAllToggle.addEventListener('change', (e) => { + populateColors(e.target.checked); + }); + } - // Set the initial state - updateActiveColorPalette(); + container.querySelector('#selectAllBtn')?.addEventListener('click', () => toggleAllColors(true, showAllToggle?.checked)); + container.querySelector('#unselectAllBtn')?.addEventListener('click', () => toggleAllColors(false, showAllToggle?.checked)); } - // --- END: Color Palette Functions --- - - // --- START: Auto-CAPTCHA Solver --- async function handleCaptcha() { return new Promise(async (resolve, reject) => { if (!CONFIG.AUTO_CAPTCHA_ENABLED) { @@ -1243,23 +1604,19 @@ } try { - // Set a timeout for the entire operation const timeoutPromise = Utils.sleep(20000).then(() => reject(new Error("Auto-CAPTCHA timed out."))); const solvePromise = (async () => { - // 1. Find and click the main "Paint" button on the screen const mainPaintBtn = await Utils.waitForSelector('button.btn.btn-primary.btn-lg, button.btn-primary.sm\\:btn-xl', 200, 10000); if (!mainPaintBtn) throw new Error("Could not find the main paint button."); mainPaintBtn.click(); await Utils.sleep(500); - // 2. Select the transparent color to avoid wasting a real color const transBtn = await Utils.waitForSelector('button#color-0', 200, 5000); if (!transBtn) throw new Error("Could not find the transparent color button."); transBtn.click(); await Utils.sleep(500); - // 3. Find the canvas and simulate a click const canvas = await Utils.waitForSelector('canvas', 200, 5000); if (!canvas) throw new Error("Could not find the canvas element."); @@ -1275,7 +1632,6 @@ canvas.dispatchEvent(new KeyboardEvent('keyup', { key: ' ', code: 'Space', bubbles: true })); await Utils.sleep(500); - // 4. Find and click the confirmation button let confirmBtn = await Utils.waitForSelector('button.btn.btn-primary.btn-lg, button.btn.btn-primary.sm\\:btn-xl'); if (!confirmBtn) { const allPrimary = Array.from(document.querySelectorAll('button.btn-primary')); @@ -1284,7 +1640,6 @@ if (!confirmBtn) throw new Error("Could not find the confirmation button."); confirmBtn.click(); - // The fetch interceptor will capture the token. We wait for it. await tokenPromise; resolve(); })(); @@ -1297,13 +1652,11 @@ } }); } - // --- END: Auto-CAPTCHA Solver --- async function createUI() { await detectLanguage() - // Clean up existing UI elements to prevent duplicates const existingContainer = document.getElementById("wplace-image-bot-container") const existingStats = document.getElementById("wplace-stats-container") const existingSettings = document.getElementById("wplace-settings-container") @@ -1746,6 +2099,11 @@ .wplace-btn:disabled::before { display: none; } + + .wplace-btn-overlay.active { + background: linear-gradient(135deg, #29b6f6 0%, #8e2de2 100%); + box-shadow: 0 0 15px #8e2de2; + } .wplace-stats { background: ${CONFIG.currentTheme === "Classic Autobot" ? "rgba(255,255,255,0.03)" : theme.secondary}; @@ -1779,7 +2137,6 @@ color: ${theme.highlight}; } - /* Styles for the new color display in stats */ .wplace-colors-section { margin-top: 10px; padding-top: 8px; @@ -1929,7 +2286,7 @@ CONFIG.currentTheme === "Classic Autobot" ? "0 0 20px rgba(0,0,0,0.5)" : "0 0 30px rgba(0, 255, 65, 0.5)" }; width: 90%; - max-width: 700px; /* Increased width */ + max-width: 700px; max-height: 90%; overflow: auto; font-family: ${theme.fontFamily}; @@ -1947,7 +2304,7 @@ } .resize-preview { - max-width: none; /* Allow image to exceed wrapper for zoom */ + max-width: none; transition: transform 0.1s ease; image-rendering: pixelated; image-rendering: -moz-crisp-edges; @@ -2004,7 +2361,7 @@ } .resize-zoom-controls { - grid-column: 1 / -1; /* Span full width */ + grid-column: 1 / -1; display: flex; align-items: center; gap: 10px; @@ -2028,12 +2385,13 @@ z-index: 9999; display: none; } - /* --- START: Color Palette Styles --- */ .wplace-color-grid { display: grid; - grid-template-columns: repeat(auto-fill, minmax(60px, 1fr)); /* Wider columns for name */ + grid-template-columns: repeat(auto-fill, minmax(60px, 1fr)); gap: 10px; padding-top: 8px; + max-height: 300px; + overflow-y: auto; } .wplace-color-item { display: flex; @@ -2060,8 +2418,10 @@ position: relative; margin: 0 auto; } - .wplace-color-swatch.paid { - border-color: gold; + .wplace-color-swatch.unavailable { + border-color: #666; + border-style: dashed; + cursor: not-allowed; } .wplace-color-swatch:hover { transform: scale(1.1); @@ -2071,6 +2431,10 @@ opacity: 0.3; filter: grayscale(80%); } + .wplace-color-swatch.unavailable:not(.active) { + opacity: 0.2; + filter: grayscale(90%); + } .wplace-color-swatch.active::after { content: 'โœ”'; position: absolute; @@ -2088,9 +2452,7 @@ background: rgba(255,255,255,0.1); margin: 8px 0; } - /* --- END: Color Palette Styles --- */ - /* Cooldown slider styles */ .wplace-cooldown-control { margin-top: 8px; } @@ -2126,7 +2488,6 @@ ${ CONFIG.currentTheme === "Neon Retro" ? ` - /* Retro checkbox styling */ input[type="checkbox"] { -webkit-appearance: none; -moz-appearance: none; @@ -2154,12 +2515,10 @@ font-weight: bold; } - /* Icon styling for retro feel */ .fas, .fa { filter: drop-shadow(0 0 3px currentColor); } - /* Speed Control Styles */ .wplace-speed-control { margin-top: 12px; padding: 12px; @@ -2242,7 +2601,6 @@ opacity: 0.8; } - /* Settings Window Styles */ #wplace-settings-container { position: fixed; top: 50%; @@ -2554,6 +2912,12 @@ ${Utils.t("stopPainting")} +
+ +
@@ -2696,6 +3060,32 @@ + +
+ +
+ +
+
+ Overlay Opacity +
${Math.round(state.overlayOpacity * 100)}%
+
+ +
+ + +
+
+