diff --git a/Auto-Image.js b/Auto-Image.js index 41db943..4200f49 100644 --- a/Auto-Image.js +++ b/Auto-Image.js @@ -43,12 +43,11 @@ "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", }, - // --- CORRECTED PAID COLORS --- - // Corresponds to IDs 32-61 from the main palette PAID_COLORS: new Set([ - "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" + "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 @@ -177,6 +176,7 @@ const TEXT = { en: { title: "WPlace Auto-Image", + toggleOverlay: "Toggle Overlay", scanColors: "Scan Colors", uploadImage: "Upload Image", resizeImage: "Resize Image", @@ -552,57 +552,256 @@ language: "en", paintingSpeed: CONFIG.PAINTING_SPEED.DEFAULT, // pixels per second cooldownChargeThreshold: CONFIG.COOLDOWN_CHARGE_THRESHOLD, - // --- NEW: Overlay state --- - overlayCanvas: null, - overlayCtx: null, - gameCanvas: null, } // 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); + + 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 = 0.6; + 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", { @@ -629,11 +828,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) @@ -646,7 +843,6 @@ } }) - // Append children efficiently if (typeof children === 'string') { element.textContent = children } else if (Array.isArray(children)) { @@ -662,7 +858,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, @@ -732,7 +927,6 @@ 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]; @@ -841,19 +1035,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 = { @@ -909,10 +1099,8 @@ restoreProgress: (savedData) => { try { - // Restore state Object.assign(state, savedData.state) - // Restore image data if (savedData.imageData) { state.imageData = { ...savedData.imageData, @@ -920,7 +1108,6 @@ } } - // Restore painted map if (savedData.paintedMap) { state.paintedMap = savedData.paintedMap.map((row) => Array.from(row)) } @@ -1106,10 +1293,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) } @@ -1126,23 +1311,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) @@ -1156,7 +1336,6 @@ let updateStats = () => {} let updateDataButtons = () => {} - // --- START: Color Palette Functions --- function updateActiveColorPalette() { state.activeColorPalette = []; const activeSwatches = document.querySelectorAll('.wplace-color-swatch.active'); @@ -1166,7 +1345,6 @@ state.activeColorPalette.push(rgb); }); } - // If the resize dialog is open, update its preview if (document.querySelector('.resize-container')?.style.display === 'block') { _updateResizePreview(); } @@ -1191,7 +1369,6 @@ freeContainer.innerHTML = ''; paidContainer.innerHTML = ''; - // Create a unique set of colors to avoid duplicates const uniqueColors = [...new Set(CONFIG.COLOR_PALETTE.map(JSON.stringify))].map(JSON.parse); uniqueColors.forEach(rgb => { @@ -1209,7 +1386,6 @@ const nameLabel = Utils.createElement('span', { className: 'wplace-color-item-name' }, name); - // Default state: free are active, paid are not if (!isPaid) { swatch.classList.add('active'); } @@ -1229,18 +1405,13 @@ } }); - // 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)); - // Set the initial state updateActiveColorPalette(); } - // --- END: Color Palette Functions --- - - // --- START: Auto-CAPTCHA Solver --- async function handleCaptcha() { return new Promise(async (resolve, reject) => { if (!CONFIG.AUTO_CAPTCHA_ENABLED) { @@ -1248,23 +1419,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."); @@ -1280,7 +1447,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')); @@ -1289,7 +1455,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(); })(); @@ -1302,56 +1467,11 @@ } }); } - // --- END: Auto-CAPTCHA Solver --- - - // --- START: Overlay Management --- - async function initializeOverlay() { - state.gameCanvas = await Utils.waitForSelector('canvas'); - if (!state.gameCanvas || document.getElementById('wplace-overlay-canvas')) { - console.log("Overlay not initialized (game canvas not found or overlay exists)."); - return; // Already initialized or game canvas not found - } - - state.overlayCanvas = document.createElement('canvas'); - state.overlayCanvas.id = 'wplace-overlay-canvas'; - state.overlayCtx = state.overlayCanvas.getContext('2d'); - - // Style the overlay - state.overlayCanvas.style.position = 'absolute'; - state.overlayCanvas.style.pointerEvents = 'none'; - state.overlayCanvas.style.zIndex = '1'; // Just above the game canvas - state.overlayCanvas.style.display = 'none'; - state.overlayCanvas.style.imageRendering = 'pixelated'; // Keep pixels sharp - - // Insert it into the DOM, right after the game canvas - state.gameCanvas.parentElement.insertBefore(state.overlayCanvas, state.gameCanvas.nextSibling); - - const syncOverlay = () => { - if (!state.gameCanvas || !state.overlayCanvas) return; - // The canvas element's width/height attributes are the resolution - // The style width/height are the display size, but transform handles that. - state.overlayCanvas.width = state.gameCanvas.width; - state.overlayCanvas.height = state.gameCanvas.height; - // Copy position and transform to align perfectly - state.overlayCanvas.style.top = state.gameCanvas.offsetTop + 'px'; - state.overlayCanvas.style.left = state.gameCanvas.offsetLeft + 'px'; - state.overlayCanvas.style.transform = state.gameCanvas.style.transform; - }; - - // Use a MutationObserver to watch for style changes (pan/zoom) - const observer = new MutationObserver(syncOverlay); - observer.observe(state.gameCanvas, { attributes: true, attributeFilter: ['style', 'width', 'height'] }); - - syncOverlay(); // Initial sync - console.log("✅ Overlay Initialized."); - } - // --- END: Overlay Management --- 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") @@ -1680,7 +1800,7 @@ ? `linear-gradient(135deg, ${theme.accent} 0%, #4a4a4a 100%)` : theme.accent }; - ${CONFIG.currentTheme === "Classic Autobot" ? `border: 1px solid rgba(255,255,255,0.1);` : ""} + ${CONFIG.currentTheme === "Classic Autobot" ? "border: 1px solid rgba(255,255,255,0.1);" : ""} } ${ @@ -1794,6 +1914,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}; @@ -1827,7 +1952,6 @@ color: ${theme.highlight}; } - /* Styles for the new color display in stats */ .wplace-colors-section { margin-top: 10px; padding-top: 8px; @@ -1977,7 +2101,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}; @@ -1995,7 +2119,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; @@ -2052,7 +2176,7 @@ } .resize-zoom-controls { - grid-column: 1 / -1; /* Span full width */ + grid-column: 1 / -1; display: flex; align-items: center; gap: 10px; @@ -2076,10 +2200,9 @@ 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; } @@ -2136,9 +2259,7 @@ background: rgba(255,255,255,0.1); margin: 8px 0; } - /* --- END: Color Palette Styles --- */ - /* Cooldown slider styles */ .wplace-cooldown-control { margin-top: 8px; } @@ -2174,7 +2295,6 @@ ${ CONFIG.currentTheme === "Neon Retro" ? ` - /* Retro checkbox styling */ input[type="checkbox"] { -webkit-appearance: none; -moz-appearance: none; @@ -2202,12 +2322,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; @@ -2290,7 +2408,6 @@ opacity: 0.8; } - /* Settings Window Styles */ #wplace-settings-container { position: fixed; top: 50%; @@ -2602,6 +2719,12 @@ ${Utils.t("stopPainting")} +
+ +
@@ -2933,7 +3056,6 @@ background: #4a5568; } - /* Dragging state styles */ .wplace-dragging { opacity: 0.9; box-shadow: 0 30px 60px rgba(0,0,0,0.4), 0 0 0 1px rgba(255,255,255,0.2); @@ -2956,11 +3078,11 @@

${Utils.t("resizeImage")}

-
 Color Palette @@ -3001,7 +3122,6 @@
-