diff --git a/Auto-Image.js b/Auto-Image.js index 65618f4..c9f3e42 100644 --- a/Auto-Image.js +++ b/Auto-Image.js @@ -1064,6 +1064,8 @@ resizeSettings: null, originalImage: null, resizeIgnoreMask: null, + antiGriefEnabled: false, + _antiGriefTimer: null, } let _updateResizePreview = () => { }; @@ -2490,10 +2492,16 @@ // Update overlay button state const toggleOverlayBtn = document.getElementById('toggleOverlayBtn'); + const antiGriefBtn = document.getElementById('antiGriefBtn'); if (toggleOverlayBtn) { toggleOverlayBtn.disabled = false; toggleOverlayBtn.classList.add('active'); } + const antiGriefBtn2 = document.getElementById('antiGriefBtn'); + if (antiGriefBtn2) antiGriefBtn2.disabled = false; + if (antiGriefBtn) { + antiGriefBtn.disabled = false; + } console.log('Overlay restored from data'); return true; @@ -2676,11 +2684,147 @@ return bestId } - // UI UPDATE FUNCTIONS (declared early to avoid reference errors) + let updateUI = () => { } let updateStats = () => { } let updateDataButtons = () => { } + async function startAntiGrief() { + stopAntiGrief(); + if (!state.imageLoaded || !state.startPosition || !state.region) return; + state.antiGriefEnabled = true; + const antiGriefBtn = document.getElementById('antiGriefBtn'); + if (antiGriefBtn) { + antiGriefBtn.disabled = false; + antiGriefBtn.classList.add('active'); + antiGriefBtn.setAttribute('aria-pressed', 'true'); + } + + const defendOnce = async () => { + if (!state.antiGriefEnabled) return; + if (!state.imageLoaded || !state.startPosition || !state.region) return; + if (state.running) return; // skip while painting is running + try { + // Ensure we have latest tile caches to read colors + if (overlayManager && overlayManager.imageBitmap && overlayManager.startCoords) { + await overlayManager.processImageIntoChunks(); + } + + const { width, height, pixels } = state.imageData; + const mask = (state.resizeIgnoreMask && state.resizeIgnoreMask.length === width * height) ? state.resizeIgnoreMask : null; + const { x: startX, y: startY } = state.startPosition; + const { x: regionX, y: regionY } = state.region; + + const tileSize = 1000; + let totalMismatches = 0; + + for (let localY = 0; localY < height && state.antiGriefEnabled; localY++) { + const absY = startY + localY; + const adderY = Math.floor(absY / tileSize); + const pixelY = absY % tileSize; + let currentRegionX = null, currentRegionY = regionY + adderY; + let batch = null; + + for (let localX = 0; localX < width && state.antiGriefEnabled; localX++) { + const pixIndex = (localY * width + localX); + if (mask && mask[pixIndex]) continue; + const idx = pixIndex * 4; + const r = pixels[idx], g = pixels[idx + 1], b = pixels[idx + 2], a = pixels[idx + 3]; + const tThresh = state.customTransparencyThreshold || CONFIG.TRANSPARENCY_THRESHOLD; + if (a < tThresh) continue; + if (!state.paintWhitePixels && Utils.isWhitePixel(r, g, b)) continue; + + const absX = startX + localX; + const adderX = Math.floor(absX / tileSize); + const pixelX = absX % tileSize; + + const regionTileX = regionX + adderX; + const regionTileY = regionY + adderY; + + if (currentRegionX === null) currentRegionX = regionTileX; + if (regionTileX !== currentRegionX) { + if (batch && batch.pixels.length > 0) { + const ok = await sendBatchWithRetry(batch.pixels, batch.regionX, batch.regionY); + if (ok) { + state.currentCharges = Math.max(0, state.currentCharges - batch.pixels.length); + } + } + batch = null; + currentRegionX = regionTileX; + } + + let existing = null; + try { + existing = await overlayManager.getTilePixelColor(regionTileX, regionTileY, pixelX, pixelY); + } catch (_) {} + if (!existing) continue; + const [er, eg, eb] = existing; + + // Desired palette color + const desiredId = findClosestColor([r, g, b], state.availableColors); + const existingId = findClosestColor([er, eg, eb], state.availableColors); + if (desiredId === existingId) continue; // already correct + + // Initialize batch for this region + if (!batch) batch = { regionX: regionTileX, regionY: regionTileY, pixels: [] }; + batch.pixels.push({ x: pixelX, y: pixelY, color: desiredId }); + totalMismatches++; + + if (batch.pixels.length >= Math.max(1, Math.floor(state.currentCharges))) { + await ensureToken(); + const ok = await sendBatchWithRetry(batch.pixels, batch.regionX, batch.regionY); + if (ok) { + state.currentCharges = Math.max(0, state.currentCharges - batch.pixels.length); + } + batch.pixels = []; + + while (state.currentCharges < Math.max(1, state.cooldownChargeThreshold) && state.antiGriefEnabled && !state.running) { + const { charges, cooldown } = await WPlaceService.getCharges(); + state.currentCharges = Math.floor(charges); + state.cooldown = cooldown; + if (state.currentCharges >= state.cooldownChargeThreshold) break; + await Utils.sleep(Math.max(1000, cooldown)); + } + } + } + + // Flush the row's last batch + if (batch && batch.pixels.length > 0 && state.antiGriefEnabled) { + await ensureToken(); + const ok = await sendBatchWithRetry(batch.pixels, batch.regionX, batch.regionY); + if (ok) { + state.currentCharges = Math.max(0, state.currentCharges - batch.pixels.length); + } + } + } + + if (totalMismatches > 0) { + console.log(`🛡️ Anti-Grief repainted ${totalMismatches} pixels.`); + } + } catch (e) { + console.warn('Anti-Grief iteration failed:', e); + } finally { + try { await updateStats(); } catch {} + } + }; + + await defendOnce(); + state._antiGriefTimer = setInterval(defendOnce, 30000); + } + + function stopAntiGrief() { + state.antiGriefEnabled = false; + if (state._antiGriefTimer) { + clearInterval(state._antiGriefTimer); + state._antiGriefTimer = null; + } + const antiGriefBtn = document.getElementById('antiGriefBtn'); + if (antiGriefBtn) { + antiGriefBtn.classList.remove('active'); + antiGriefBtn.setAttribute('aria-pressed', 'false'); + } + } + function updateActiveColorPalette() { state.activeColorPalette = []; const activeSwatches = document.querySelectorAll('.wplace-color-swatch.active'); @@ -2702,7 +2846,7 @@ const swatches = document.querySelectorAll('.wplace-color-swatch'); if (swatches) { swatches.forEach(swatch => { - // Only toggle colors that are available or if we're showing unavailable colors + const isUnavailable = swatch.classList.contains('unavailable'); if (!isUnavailable || showingUnavailable) { // Don't try to select unavailable colors @@ -2720,10 +2864,9 @@ const showAllToggle = container.querySelector('#showAllColorsToggle'); if (!colorsContainer) return; - // Use already captured colors from state (captured during upload) - // Don't re-fetch colors here, use what was captured when user clicked upload + if (!state.availableColors || state.availableColors.length === 0) { - // If no colors have been captured yet, show message + colorsContainer.innerHTML = '