diff --git a/Extension/Script-manager.js b/Extension/Script-manager.js
index 98af2dc..a5fbf2a 100644
--- a/Extension/Script-manager.js
+++ b/Extension/Script-manager.js
@@ -34,6 +34,13 @@
description: 'Automated repair and maintenance tasks',
icon: '🔧',
category: 'utility'
+ },
+ {
+ name: 'Acc-Switch.js',
+ displayName: '🔄 Account Switcher',
+ description: 'Temporary account switching script',
+ icon: '🔄',
+ category: 'automation'
}
];
diff --git a/Extension/auto-image-styles.css b/Extension/auto-image-styles.css
index 3e45a67..3cb715e 100644
--- a/Extension/auto-image-styles.css
+++ b/Extension/auto-image-styles.css
@@ -1920,3 +1920,162 @@
color: var(--wplace-icon-palette);
margin-right: 6px;
}
+
+/* Account Management Styles */
+.wplace-account-info {
+ display: flex;
+ flex-direction: column;
+ gap: 12px;
+ margin-bottom: 12px;
+ padding: 12px;
+ background: var(--wplace-secondary);
+ border: 1px solid var(--wplace-border-color);
+ border-radius: var(--wplace-radius);
+}
+
+.wplace-account-current {
+ display: flex;
+ align-items: center;
+ gap: 8px;
+ font-size: 14px;
+ color: var(--wplace-text);
+}
+
+.wplace-account-current i {
+ color: var(--wplace-highlight);
+ width: 16px;
+}
+
+.wplace-account-current strong {
+ color: var(--wplace-success);
+}
+
+.wplace-account-switch {
+ display: flex;
+ align-items: center;
+ gap: 8px;
+}
+
+.wplace-toggle-switch {
+ display: flex;
+ align-items: center;
+ gap: 10px;
+ cursor: pointer;
+ user-select: none;
+}
+
+.wplace-toggle-switch input[type="checkbox"] {
+ display: none;
+}
+
+.wplace-toggle-slider {
+ position: relative;
+ width: 44px;
+ height: 24px;
+ background: var(--wplace-accent);
+ border: 1px solid var(--wplace-border-color);
+ border-radius: 12px;
+ transition: all 0.3s ease;
+}
+
+.wplace-toggle-slider:before {
+ content: '';
+ position: absolute;
+ top: 2px;
+ left: 2px;
+ width: 18px;
+ height: 18px;
+ background: var(--wplace-text);
+ border-radius: 50%;
+ transition: all 0.3s ease;
+}
+
+.wplace-toggle-switch input:checked + .wplace-toggle-slider {
+ background: var(--wplace-success);
+ border-color: var(--wplace-success);
+}
+
+.wplace-toggle-switch input:checked + .wplace-toggle-slider:before {
+ transform: translateX(20px);
+ background: white;
+}
+
+.wplace-toggle-label {
+ font-size: 14px;
+ color: var(--wplace-text);
+ font-weight: 500;
+}
+
+.wplace-account-list {
+ max-height: 200px;
+ overflow-y: auto;
+ border: 1px solid var(--wplace-border-color);
+ border-radius: var(--wplace-radius);
+ background: var(--wplace-secondary);
+}
+
+.wplace-account-item {
+ display: flex;
+ align-items: center;
+ justify-content: space-between;
+ padding: 10px 12px;
+ border-bottom: 1px solid var(--wplace-border-color);
+ transition: all 0.2s ease;
+}
+
+.wplace-account-item:last-child {
+ border-bottom: none;
+}
+
+.wplace-account-item:hover {
+ background: var(--wplace-accent);
+}
+
+.wplace-account-item.current {
+ background: var(--wplace-success)20;
+ border-left: 3px solid var(--wplace-success);
+}
+
+.wplace-account-info-item {
+ display: flex;
+ align-items: center;
+ gap: 8px;
+}
+
+.wplace-account-number {
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ width: 24px;
+ height: 24px;
+ background: var(--wplace-highlight);
+ color: white;
+ border-radius: 50%;
+ font-size: 12px;
+ font-weight: bold;
+}
+
+.wplace-account-name {
+ font-size: 14px;
+ color: var(--wplace-text);
+ font-weight: 500;
+}
+
+.wplace-account-status {
+ font-size: 12px;
+ color: var(--wplace-text-secondary);
+}
+
+.wplace-account-placeholder {
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ gap: 8px;
+ padding: 20px;
+ color: var(--wplace-text-secondary);
+ font-style: italic;
+}
+
+.wplace-account-placeholder i {
+ color: var(--wplace-warning);
+}
diff --git a/Extension/background.js b/Extension/background.js
index 2c904b1..c2dc166 100644
--- a/Extension/background.js
+++ b/Extension/background.js
@@ -48,7 +48,7 @@ async function executeLocalScript(scriptName, tabId) {
} else {
scriptUrl = chrome.runtime.getURL(`scripts/${scriptName}`);
}
-
+
const response = await fetch(scriptUrl);
if (!response.ok) {
@@ -67,38 +67,38 @@ async function executeLocalScript(scriptName, tabId) {
world: "MAIN", // Key: executes in page context, not extension context
func: (code, name, themeCSS, languages) => {
console.log(`%c🚀 Executing ${name}...`, 'color: #4ade80; font-weight: bold; font-size: 14px;');
-
+
// Create detailed resource report
console.group(`%c📊 WPlace AutoBOT Resource Report for ${name}`, 'color: #3b82f6; font-weight: bold; font-size: 16px;');
-
+
// Debug: Log what resources we received
console.log(`%c📦 Raw Resources Received:`, 'color: #8b5cf6; font-weight: bold;');
console.log(` - Themes object:`, themeCSS);
console.log(` - Languages object:`, languages);
console.log(` - Theme count: ${Object.keys(themeCSS || {}).length}`);
console.log(` - Language count: ${Object.keys(languages || {}).length}`);
-
+
// Inject CSS themes if available
if (themeCSS && Object.keys(themeCSS).length > 0) {
console.group(`%c🎨 Theme Processing`, 'color: #8b5cf6; font-weight: bold;');
console.log(`%c📁 Loading ${Object.keys(themeCSS).length} theme files from extension local storage...`, 'color: #8b5cf6;');
-
+
// Create a global themes object
window.AUTOBOT_THEMES = themeCSS;
-
+
// Log detailed theme information
Object.entries(themeCSS).forEach(([filename, content]) => {
console.log(`%c📄 Theme File: ${filename}`, 'color: #8b5cf6; font-weight: bold;');
console.log(` 📏 Size: ${content.length.toLocaleString()} characters`);
console.log(` 📍 Source: Extension local file (chrome-extension://)`);
console.log(` 🔗 Full path: themes/${filename}`);
-
+
// Show first few lines as preview
const preview = content.substring(0, 200).split('\\n').slice(0, 3).join('\\n');
console.log(` 👀 Preview: ${preview}${content.length > 200 ? '...' : ''}`);
console.log(` ✅ Status: Loaded successfully`);
});
-
+
// Inject auto-image-styles.css if available
if (themeCSS['auto-image-styles.css']) {
const autoImageContent = themeCSS['auto-image-styles.css'];
@@ -106,7 +106,7 @@ async function executeLocalScript(scriptName, tabId) {
styleElement.id = 'autobot-auto-image-styles';
styleElement.textContent = autoImageContent;
document.head.appendChild(styleElement);
-
+
console.log(`%c✨ AUTO-INJECTED: auto-image-styles.css`, 'color: #10b981; font-weight: bold;');
console.log(` 📏 Injected size: ${autoImageContent.length.toLocaleString()} characters`);
console.log(` 📍 Source: Extension local file`);
@@ -122,14 +122,14 @@ async function executeLocalScript(scriptName, tabId) {
console.log(` 📋 Expected files: auto-image-styles.css, acrylic.css, classic.css, etc.`);
window.AUTOBOT_THEMES = {};
}
-
+
// Inject language data if available
if (languages && Object.keys(languages).length > 0) {
console.group(`%c🌍 Language Processing`, 'color: #06b6d4; font-weight: bold;');
console.log(`%c📁 Loading ${Object.keys(languages).length} language files from extension local storage...`, 'color: #06b6d4;');
-
+
window.AUTOBOT_LANGUAGES = languages;
-
+
// Log detailed language information
Object.entries(languages).forEach(([filename, content]) => {
console.log(`%c📄 Language File: ${filename}`, 'color: #06b6d4; font-weight: bold;');
@@ -137,18 +137,18 @@ async function executeLocalScript(scriptName, tabId) {
console.log(` 📏 Keys count: ${Object.keys(content).length.toLocaleString()}`);
console.log(` 📍 Source: Extension local file (chrome-extension://)`);
console.log(` 🔗 Full path: lang/${filename}`);
-
+
// Show some sample keys
const sampleKeys = Object.keys(content).slice(0, 5);
console.log(` 🔑 Sample keys: ${sampleKeys.join(', ')}${Object.keys(content).length > 5 ? '...' : ''}`);
console.log(` ✅ Status: Loaded successfully`);
});
-
+
// Helper function to get language data with detailed logging
- window.getLanguage = function(lang = 'en') {
+ window.getLanguage = function (lang = 'en') {
const langFile = lang + '.json';
const result = window.AUTOBOT_LANGUAGES[langFile] || window.AUTOBOT_LANGUAGES['en.json'] || {};
-
+
console.group(`%c🔤 Language Access: ${lang.toUpperCase()}`, 'color: #06b6d4; font-weight: bold;');
console.log(` 📋 Requested: ${lang}`);
console.log(` 📄 File: ${langFile}`);
@@ -157,10 +157,10 @@ async function executeLocalScript(scriptName, tabId) {
console.log(` ✅ Success: ${window.AUTOBOT_LANGUAGES[langFile] ? 'Found exact match' : 'Fallback to English'}`);
console.log(` 📝 Data preview:`, result);
console.groupEnd();
-
+
return result;
};
-
+
console.log(`%c🔤 Available languages: ${Object.keys(languages).map(f => f.replace('.json', '')).join(', ')}`, 'color: #06b6d4;');
console.groupEnd();
} else {
@@ -168,16 +168,16 @@ async function executeLocalScript(scriptName, tabId) {
console.log(` 📁 Expected source: Extension local files`);
console.log(` 📋 Expected files: en.json, de.json, fr.json, etc.`);
window.AUTOBOT_LANGUAGES = {};
- window.getLanguage = function() {
+ window.getLanguage = function () {
console.warn(`%c⚠️ getLanguage() called but no languages available`, 'color: #f59e0b;');
- return {};
+ return {};
};
}
-
+
// Helper function to apply theme with detailed logging
- window.applyTheme = function(themeName) {
+ window.applyTheme = function (themeName) {
console.group(`%c🎨 Theme Application: ${themeName}`, 'color: #8b5cf6; font-weight: bold;');
-
+
if (!window.AUTOBOT_THEMES || Object.keys(window.AUTOBOT_THEMES).length === 0) {
console.error(`%c❌ No themes available in extension`, 'color: #ef4444; font-weight: bold;');
console.log(` 📁 Expected source: Extension local files`);
@@ -185,38 +185,38 @@ async function executeLocalScript(scriptName, tabId) {
console.groupEnd();
return false;
}
-
+
const themeFile = themeName + '.css';
console.log(` 📋 Requested theme: ${themeName}`);
console.log(` 📄 Looking for file: ${themeFile}`);
console.log(` 📁 Available themes: ${Object.keys(window.AUTOBOT_THEMES).join(', ')}`);
-
+
if (window.AUTOBOT_THEMES[themeFile]) {
const themeContent = window.AUTOBOT_THEMES[themeFile];
-
+
// Remove existing theme
const existing = document.getElementById('autobot-theme');
if (existing) {
console.log(` 🗑️ Removing previous theme element`);
existing.remove();
}
-
+
// Apply new theme
const styleElement = document.createElement('style');
styleElement.id = 'autobot-theme';
styleElement.textContent = themeContent;
document.head.appendChild(styleElement);
-
+
console.log(`%c✅ Theme applied successfully: ${themeName}`, 'color: #10b981; font-weight: bold;');
console.log(` 📏 Content size: ${themeContent.length.toLocaleString()} characters`);
console.log(` 📍 Source: Extension local file`);
console.log(` 🎯 Target:
as
+ `
+
+ const resizeContainer = document.createElement("div")
+ resizeContainer.className = "resize-container"
+ resizeContainer.innerHTML = `
+ ${Utils.t("resizeImage")}
+
+
+
+
+
+
+
+
+
+
+
+
+
100%
+
+ Drag to pan • Pinch to zoom • Double‑tap to zoom
+
+
+
+
+
+
+
+
+
+ Color Palette
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Advanced Color Matching
+
+
+
+
+
+
+ Chroma Weight
+ ${state.chromaPenaltyWeight}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ `
+
+ const resizeOverlay = document.createElement("div")
+ resizeOverlay.className = "resize-overlay"
+
+ document.body.appendChild(container)
+ document.body.appendChild(resizeOverlay)
+ document.body.appendChild(resizeContainer)
+ document.body.appendChild(statsContainer)
+ document.body.appendChild(settingsContainer)
+
+ const uploadBtn = container.querySelector("#uploadBtn")
+ const resizeBtn = container.querySelector("#resizeBtn")
+ const selectPosBtn = container.querySelector("#selectPosBtn")
+ const startBtn = container.querySelector("#startBtn")
+ const stopBtn = container.querySelector("#stopBtn")
+ const saveBtn = container.querySelector("#saveBtn")
+ const loadBtn = container.querySelector("#loadBtn")
+ const saveToFileBtn = container.querySelector("#saveToFileBtn")
+ const loadFromFileBtn = container.querySelector("#loadFromFileBtn")
+
+ // Disable load/upload buttons until initial setup is complete (startup only)
+ if (loadBtn) {
+ loadBtn.disabled = !state.initialSetupComplete;
+ loadBtn.title = state.initialSetupComplete ? "" : "🔄 Waiting for initial setup to complete...";
+ }
+ if (loadFromFileBtn) {
+ loadFromFileBtn.disabled = !state.initialSetupComplete;
+ loadFromFileBtn.title = state.initialSetupComplete ? "" : "🔄 Waiting for initial setup to complete...";
+ }
+ if (uploadBtn) {
+ uploadBtn.disabled = !state.initialSetupComplete;
+ uploadBtn.title = state.initialSetupComplete ? "" : "🔄 Waiting for initial setup to complete...";
+ }
+
+ const minimizeBtn = container.querySelector("#minimizeBtn")
+ const compactBtn = container.querySelector("#compactBtn")
+ const statsBtn = container.querySelector("#statsBtn")
+ const toggleOverlayBtn = container.querySelector("#toggleOverlayBtn");
+ const statusText = container.querySelector("#statusText")
+ const progressBar = container.querySelector("#progressBar")
+ const statsArea = statsContainer.querySelector("#statsArea")
+ const content = container.querySelector(".wplace-content")
+ const closeStatsBtn = statsContainer.querySelector("#closeStatsBtn")
+ const refreshChargesBtn = statsContainer.querySelector("#refreshChargesBtn")
+ const cooldownSlider = container.querySelector("#cooldownSlider");
+ const cooldownValue = container.querySelector("#cooldownValue");
+
+ if (!uploadBtn || !selectPosBtn || !startBtn || !stopBtn) {
+ console.error("Some UI elements not found:", {
+ uploadBtn: !!uploadBtn,
+ selectPosBtn: !!selectPosBtn,
+ startBtn: !!startBtn,
+ stopBtn: !!stopBtn,
+ })
+ }
+
+ if (!statsContainer || !statsArea || !closeStatsBtn) {
+ console.error("Stats UI elements not found:", {
+ statsContainer: !!statsContainer,
+ statsArea: !!statsArea,
+ closeStatsBtn: !!closeStatsBtn,
+ })
+ }
+
+ const header = container.querySelector(".wplace-header")
+
+ makeDraggable(container)
+
+ function makeDraggable(element) {
+ let pos1 = 0,
+ pos2 = 0,
+ pos3 = 0,
+ pos4 = 0
+ let isDragging = false
+ const header = element.querySelector(".wplace-header") || element.querySelector(".wplace-settings-header")
+
+ if (!header) {
+ console.warn("No draggable header found for element:", element)
+ return
+ }
+
+ header.onmousedown = dragMouseDown
+
+ function dragMouseDown(e) {
+ if (e.target.closest(".wplace-header-btn") || e.target.closest("button")) return
+
+ e.preventDefault()
+ isDragging = true
+
+ const rect = element.getBoundingClientRect()
+
+ element.style.transform = "none"
+ element.style.top = rect.top + "px"
+ element.style.left = rect.left + "px"
+
+ pos3 = e.clientX
+ pos4 = e.clientY
+ element.classList.add("wplace-dragging")
+ document.onmouseup = closeDragElement
+ document.onmousemove = elementDrag
+
+ document.body.style.userSelect = "none"
+ }
+
+ function elementDrag(e) {
+ if (!isDragging) return
+
+ e.preventDefault()
+ pos1 = pos3 - e.clientX
+ pos2 = pos4 - e.clientY
+ pos3 = e.clientX
+ pos4 = e.clientY
+
+ let newTop = element.offsetTop - pos2
+ let newLeft = element.offsetLeft - pos1
+
+ const rect = element.getBoundingClientRect()
+ const maxTop = window.innerHeight - rect.height
+ const maxLeft = window.innerWidth - rect.width
+
+ newTop = Math.max(0, Math.min(newTop, maxTop))
+ newLeft = Math.max(0, Math.min(newLeft, maxLeft))
+
+ element.style.top = newTop + "px"
+ element.style.left = newLeft + "px"
+ }
+
+ function closeDragElement() {
+ isDragging = false
+ element.classList.remove("wplace-dragging")
+ document.onmouseup = null
+ document.onmousemove = null
+ document.body.style.userSelect = ""
+ }
+ }
+
+ makeDraggable(statsContainer)
+ makeDraggable(container)
+
+ if (statsBtn && closeStatsBtn) {
+ statsBtn.addEventListener("click", () => {
+ const isVisible = statsContainer.style.display !== "none"
+ if (isVisible) {
+ statsContainer.style.display = "none"
+ statsBtn.innerHTML = ''
+ statsBtn.title = "Show Stats"
+ } else {
+ statsContainer.style.display = "block"
+ statsBtn.innerHTML = ''
+ statsBtn.title = "Hide Stats"
+ }
+ })
+
+ closeStatsBtn.addEventListener("click", () => {
+ statsContainer.style.display = "none"
+ statsBtn.innerHTML = ''
+ statsBtn.title = "Show Stats"
+ })
+
+ const refreshAllAccountsBtn = statsContainer.querySelector("#refreshAllAccountsBtn");
+ if (refreshAllAccountsBtn) {
+ refreshAllAccountsBtn.addEventListener('click', fetchAllAccountDetails);
+ }
+
+ const autoSwapToggle = statsContainer.querySelector("#autoSwapToggle");
+ if (autoSwapToggle) {
+ autoSwapToggle.checked = CONFIG.autoSwap;
+ autoSwapToggle.addEventListener('change', (e) => {
+ CONFIG.autoSwap = e.target.checked;
+ });
+ }
+
+ if (refreshChargesBtn) {
+ refreshChargesBtn.addEventListener("click", async () => {
+ refreshChargesBtn.innerHTML = ''
+ refreshChargesBtn.disabled = true
+
+ try {
+ await updateStats()
+ } catch (error) {
+ console.error("Error refreshing charges:", error)
+ } finally {
+ refreshChargesBtn.innerHTML = ''
+ refreshChargesBtn.disabled = false
+ }
+ })
+ }
+ }
+ if (statsContainer && statsBtn) {
+ statsContainer.style.display = "block";
+ statsBtn.innerHTML = '';
+ statsBtn.title = "Hide Stats";
+ }
+
+ const settingsBtn = container.querySelector("#settingsBtn")
+ const closeSettingsBtn = settingsContainer.querySelector("#closeSettingsBtn")
+ const applySettingsBtn = settingsContainer.querySelector("#applySettingsBtn");
+
+
+ if (settingsBtn && closeSettingsBtn && applySettingsBtn) {
+ settingsBtn.addEventListener("click", () => {
+ const isVisible = settingsContainer.style.display !== "none"
+ if (isVisible) {
+ settingsContainer.style.animation = "settingsFadeOut 0.3s ease-out forwards"
+ setTimeout(() => {
+ settingsContainer.style.display = "none"
+ settingsContainer.style.animation = ""
+ }, 300)
+ } else {
+ settingsContainer.style.top = "50%"
+ settingsContainer.style.left = "50%"
+ settingsContainer.style.transform = "translate(-50%, -50%)"
+ settingsContainer.style.display = "block"
+ settingsContainer.style.animation = "settingsSlideIn 0.4s ease-out"
+ }
+ })
+
+ closeSettingsBtn.addEventListener("click", () => {
+ settingsContainer.style.animation = "settingsFadeOut 0.3s ease-out forwards"
+ setTimeout(() => {
+ settingsContainer.style.display = "none"
+ settingsContainer.style.animation = ""
+ settingsContainer.style.top = "50%"
+ settingsContainer.style.left = "50%"
+ settingsContainer.style.transform = "translate(-50%, -50%)"
+ }, 300)
+ })
+
+ applySettingsBtn.addEventListener("click", () => {
+ // Sync advanced settings before save
+ const colorAlgorithmSelect = document.getElementById('colorAlgorithmSelect');
+ if (colorAlgorithmSelect) state.colorMatchingAlgorithm = colorAlgorithmSelect.value;
+ const enableChromaPenaltyToggle = document.getElementById('enableChromaPenaltyToggle');
+ if (enableChromaPenaltyToggle) state.enableChromaPenalty = enableChromaPenaltyToggle.checked;
+ const chromaPenaltyWeightSlider = document.getElementById('chromaPenaltyWeightSlider');
+ if (chromaPenaltyWeightSlider) state.chromaPenaltyWeight = parseFloat(chromaPenaltyWeightSlider.value) || 0.15;
+ const transparencyThresholdInput = document.getElementById('transparencyThresholdInput');
+ if (transparencyThresholdInput) {
+ const v = parseInt(transparencyThresholdInput.value, 10); if (!isNaN(v) && v >= 0 && v <= 255) state.customTransparencyThreshold = v;
+ }
+ const whiteThresholdInput = document.getElementById('whiteThresholdInput');
+ if (whiteThresholdInput) {
+ const v = parseInt(whiteThresholdInput.value, 10); if (!isNaN(v) && v >= 200 && v <= 255) state.customWhiteThreshold = v;
+ }
+ // Update functional thresholds
+ CONFIG.TRANSPARENCY_THRESHOLD = state.customTransparencyThreshold;
+ CONFIG.WHITE_THRESHOLD = state.customWhiteThreshold;
+ // Notifications
+ const notifEnabledToggle = document.getElementById('notifEnabledToggle');
+ const notifOnChargesToggle = document.getElementById('notifOnChargesToggle');
+ const notifOnlyUnfocusedToggle = document.getElementById('notifOnlyUnfocusedToggle');
+ const notifIntervalInput = document.getElementById('notifIntervalInput');
+ if (notifEnabledToggle) state.notificationsEnabled = !!notifEnabledToggle.checked;
+ if (notifOnChargesToggle) state.notifyOnChargesReached = !!notifOnChargesToggle.checked;
+ if (notifOnlyUnfocusedToggle) state.notifyOnlyWhenUnfocused = !!notifOnlyUnfocusedToggle.checked;
+ if (notifIntervalInput) {
+ const v = parseInt(notifIntervalInput.value, 10);
+ if (!isNaN(v) && v >= 1 && v <= 60) state.notificationIntervalMinutes = v;
+ }
+ saveBotSettings();
+ Utils.showAlert(Utils.t("settingsSaved"), "success");
+ closeSettingsBtn.click();
+ NotificationManager.syncFromState();
+ });
+
+ makeDraggable(settingsContainer)
+
+ const tokenSourceSelect = settingsContainer.querySelector("#tokenSourceSelect")
+ if (tokenSourceSelect) {
+ tokenSourceSelect.addEventListener("change", (e) => {
+ state.tokenSource = e.target.value
+ saveBotSettings()
+ console.log(`🔑 Token source changed to: ${state.tokenSource}`)
+ const sourceNames = {
+ 'generator': 'Automatic Generator',
+ 'hybrid': 'Generator + Auto Fallback',
+ 'manual': 'Manual Pixel Placement'
+ }
+ Utils.showAlert(`Token source set to: ${sourceNames[state.tokenSource]}`, "success")
+ })
+ }
+
+ // Batch mode controls
+ const batchModeSelect = settingsContainer.querySelector("#batchModeSelect")
+ const normalBatchControls = settingsContainer.querySelector("#normalBatchControls")
+ const randomBatchControls = settingsContainer.querySelector("#randomBatchControls")
+ const randomBatchMin = settingsContainer.querySelector("#randomBatchMin")
+ const randomBatchMax = settingsContainer.querySelector("#randomBatchMax")
+
+ if (batchModeSelect) {
+ batchModeSelect.addEventListener("change", (e) => {
+ state.batchMode = e.target.value
+
+ // Switch between normal and random controls
+ if (normalBatchControls && randomBatchControls) {
+ if (e.target.value === 'random') {
+ normalBatchControls.style.display = 'none'
+ randomBatchControls.style.display = 'block'
+ } else {
+ normalBatchControls.style.display = 'block'
+ randomBatchControls.style.display = 'none'
+ }
+ }
+
+ saveBotSettings()
+ console.log(`📦 Batch mode changed to: ${state.batchMode}`)
+ Utils.showAlert(`Batch mode set to: ${state.batchMode === 'random' ? 'Random Range' : 'Normal Fixed Size'}`, "success")
+ })
+ }
+
+ if (randomBatchMin) {
+ randomBatchMin.addEventListener("input", (e) => {
+ const min = parseInt(e.target.value)
+ if (min >= 1 && min <= 1000) {
+ state.randomBatchMin = min
+ // Ensure min doesn't exceed max
+ if (randomBatchMax && min > state.randomBatchMax) {
+ state.randomBatchMax = min
+ randomBatchMax.value = min
+ }
+ saveBotSettings()
+ }
+ })
+ }
+
+ if (randomBatchMax) {
+ randomBatchMax.addEventListener("input", (e) => {
+ const max = parseInt(e.target.value)
+ if (max >= 1 && max <= 1000) {
+ state.randomBatchMax = max
+ // Ensure max doesn't go below min
+ if (randomBatchMin && max < state.randomBatchMin) {
+ state.randomBatchMin = max
+ randomBatchMin.value = max
+ }
+ saveBotSettings()
+ }
+ })
+ }
+
+ const languageSelect = settingsContainer.querySelector("#languageSelect")
+ if (languageSelect) {
+ languageSelect.addEventListener("change", (e) => {
+ const newLanguage = e.target.value
+ state.language = newLanguage
+ localStorage.setItem('wplace_language', newLanguage)
+
+ setTimeout(() => {
+ settingsContainer.style.display = "none"
+ createUI()
+ }, 100)
+ })
+ }
+
+ const themeSelect = settingsContainer.querySelector("#themeSelect")
+ if (themeSelect) {
+ themeSelect.addEventListener("change", (e) => {
+ const newTheme = e.target.value
+ switchTheme(newTheme)
+ })
+ }
+
+ const overlayOpacitySlider = settingsContainer.querySelector("#overlayOpacitySlider");
+ const overlayOpacityValue = settingsContainer.querySelector("#overlayOpacityValue");
+ const enableBlueMarbleToggle = settingsContainer.querySelector("#enableBlueMarbleToggle");
+
+ if (overlayOpacitySlider && overlayOpacityValue) {
+ overlayOpacitySlider.addEventListener('input', (e) => {
+ const opacity = parseFloat(e.target.value);
+ state.overlayOpacity = opacity;
+ overlayOpacityValue.textContent = `${Math.round(opacity * 100)}%`;
+ });
+ }
+
+ // Speed slider event listener
+ const speedSlider = settingsContainer.querySelector("#speedSlider");
+ const speedValue = settingsContainer.querySelector("#speedValue");
+ if (speedSlider && speedValue) {
+ speedSlider.addEventListener('input', (e) => {
+ const speed = parseInt(e.target.value, 10);
+ state.paintingSpeed = speed;
+ speedValue.textContent = `${speed} (batch size)`;
+ saveBotSettings();
+ });
+ }
+
+ if (enableBlueMarbleToggle) {
+ enableBlueMarbleToggle.addEventListener('click', async () => {
+ state.blueMarbleEnabled = enableBlueMarbleToggle.checked;
+ if (state.imageLoaded && overlayManager.imageBitmap) {
+ Utils.showAlert("Re-processing overlay...", "info");
+ await overlayManager.processImageIntoChunks();
+ Utils.showAlert("Overlay updated!", "success");
+ }
+ });
+ }
+
+ // (Advanced color listeners moved outside to work with resize dialog)
+ // (Advanced color listeners moved outside to work with resize dialog)
+ // Notifications listeners
+ const notifPermBtn = settingsContainer.querySelector("#notifRequestPermBtn");
+ const notifTestBtn = settingsContainer.querySelector("#notifTestBtn");
+ if (notifPermBtn) {
+ notifPermBtn.addEventListener("click", async () => {
+ const perm = await NotificationManager.requestPermission();
+ if (perm === "granted") Utils.showAlert("Notifications enabled.", "success");
+ else Utils.showAlert("Notifications permission denied.", "warning");
+ });
+ }
+ if (notifTestBtn) {
+ notifTestBtn.addEventListener("click", () => {
+ NotificationManager.notify("WPlace — Test", "This is a test notification.", "wplace-notify-test", true);
+ });
+ }
+
+ }
+
+ const widthSlider = resizeContainer.querySelector("#widthSlider")
+ const heightSlider = resizeContainer.querySelector("#heightSlider")
+ const widthValue = resizeContainer.querySelector("#widthValue")
+ const heightValue = resizeContainer.querySelector("#heightValue")
+ const keepAspect = resizeContainer.querySelector("#keepAspect")
+ const paintWhiteToggle = resizeContainer.querySelector("#paintWhiteToggle");
+ const zoomSlider = resizeContainer.querySelector("#zoomSlider");
+ const zoomValue = resizeContainer.querySelector('#zoomValue');
+ const zoomInBtn = resizeContainer.querySelector('#zoomInBtn');
+ const zoomOutBtn = resizeContainer.querySelector('#zoomOutBtn');
+ const zoomFitBtn = resizeContainer.querySelector('#zoomFitBtn');
+ const zoomActualBtn = resizeContainer.querySelector('#zoomActualBtn');
+ const panModeBtn = resizeContainer.querySelector('#panModeBtn');
+ const panStage = resizeContainer.querySelector('#resizePanStage');
+ const canvasStack = resizeContainer.querySelector('#resizeCanvasStack');
+ const baseCanvas = resizeContainer.querySelector('#resizeCanvas');
+ const maskCanvas = resizeContainer.querySelector('#maskCanvas');
+ const baseCtx = baseCanvas.getContext('2d');
+ const maskCtx = maskCanvas.getContext('2d');
+ const confirmResize = resizeContainer.querySelector("#confirmResize")
+ const cancelResize = resizeContainer.querySelector("#cancelResize")
+ const downloadPreviewBtn = resizeContainer.querySelector("#downloadPreviewBtn");
+ const clearIgnoredBtn = resizeContainer.querySelector('#clearIgnoredBtn');
+
+ if (compactBtn) {
+ compactBtn.addEventListener("click", () => {
+ container.classList.toggle("wplace-compact")
+ const isCompact = container.classList.contains("wplace-compact")
+
+ if (isCompact) {
+ compactBtn.innerHTML = ''
+ compactBtn.title = "Expand Mode"
+ } else {
+ compactBtn.innerHTML = ''
+ compactBtn.title = "Compact Mode"
+ }
+ })
+ }
+
+ if (minimizeBtn) {
+ minimizeBtn.addEventListener("click", () => {
+ state.minimized = !state.minimized
+ if (state.minimized) {
+ container.classList.add("wplace-minimized")
+ content.classList.add("wplace-hidden")
+ minimizeBtn.innerHTML = ''
+ minimizeBtn.title = "Restore"
+ } else {
+ container.classList.remove("wplace-minimized")
+ content.classList.remove("wplace-hidden")
+ minimizeBtn.innerHTML = ''
+ minimizeBtn.title = "Minimize"
+ }
+ saveBotSettings()
+ })
+ }
+
+ if (toggleOverlayBtn) {
+ toggleOverlayBtn.addEventListener('click', () => {
+ const isEnabled = overlayManager.toggle();
+ toggleOverlayBtn.classList.toggle('active', isEnabled);
+ toggleOverlayBtn.setAttribute('aria-pressed', isEnabled ? 'true' : 'false');
+ Utils.showAlert(`Overlay ${isEnabled ? 'enabled' : 'disabled'}.`, 'info');
+ });
+ }
+
+ if (state.minimized) {
+ container.classList.add("wplace-minimized")
+ content.classList.add("wplace-hidden")
+ if (minimizeBtn) {
+ minimizeBtn.innerHTML = ''
+ minimizeBtn.title = "Restore"
+ }
+ } else {
+ container.classList.remove("wplace-minimized")
+ content.classList.remove("wplace-hidden")
+ if (minimizeBtn) {
+ minimizeBtn.innerHTML = ''
+ minimizeBtn.title = "Minimize"
+ }
+ }
+
+ if (saveBtn) {
+ saveBtn.addEventListener("click", () => {
+ if (!state.imageLoaded) {
+ Utils.showAlert(Utils.t("missingRequirements"), "error")
+ return
+ }
+
+ const success = Utils.saveProgress()
+ if (success) {
+ updateUI("autoSaved", "success")
+ Utils.showAlert(Utils.t("autoSaved"), "success")
+ } else {
+ Utils.showAlert("❌ Erro ao salvar progresso", "error")
+ }
+ })
+ }
+
+ if (loadBtn) {
+ loadBtn.addEventListener("click", () => {
+ // Check if initial setup is complete
+ if (!state.initialSetupComplete) {
+ Utils.showAlert("🔄 Please wait for the initial setup to complete before loading progress.", "warning");
+ return;
+ }
+
+ const savedData = Utils.loadProgress()
+ if (!savedData) {
+ updateUI("noSavedData", "warning")
+ Utils.showAlert(Utils.t("noSavedData"), "warning")
+ return
+ }
+
+ const confirmLoad = confirm(
+ `${Utils.t("savedDataFound")}\n\n` +
+ `Saved: ${new Date(savedData.timestamp).toLocaleString()}\n` +
+ `Progress: ${savedData.state.paintedPixels}/${savedData.state.totalPixels} pixels`,
+ )
+
+ if (confirmLoad) {
+ const success = Utils.restoreProgress(savedData)
+ if (success) {
+ updateUI("dataLoaded", "success")
+ Utils.showAlert(Utils.t("dataLoaded"), "success")
+ updateDataButtons()
+
+ updateStats()
+
+ // Restore overlay if image data was loaded from localStorage
+ Utils.restoreOverlayFromData().catch(error => {
+ console.error('Failed to restore overlay from localStorage:', error);
+ });
+
+ if (!state.colorsChecked) {
+ uploadBtn.disabled = false;
+ } else {
+ uploadBtn.disabled = false;
+ selectPosBtn.disabled = false;
+ }
+
+ if (state.imageLoaded && state.startPosition && state.region && state.colorsChecked) {
+ startBtn.disabled = false
+ }
+ } else {
+ Utils.showAlert("❌ Erro ao carregar progresso", "error")
+ }
+ }
+ })
+ }
+
+ if (saveToFileBtn) {
+ saveToFileBtn.addEventListener("click", () => {
+ const success = Utils.saveProgressToFile()
+ if (success) {
+ updateUI("fileSaved", "success")
+ Utils.showAlert(Utils.t("fileSaved"), "success")
+ } else {
+ Utils.showAlert(Utils.t("fileError"), "error")
+ }
+ })
+ }
+
+ if (loadFromFileBtn) {
+ loadFromFileBtn.addEventListener("click", async () => {
+ // Check if initial setup is complete
+ if (!state.initialSetupComplete) {
+ Utils.showAlert("🔄 Please wait for the initial setup to complete before loading from file.", "warning");
+ return;
+ }
+
+ try {
+ const success = await Utils.loadProgressFromFile()
+ if (success) {
+ updateUI("fileLoaded", "success")
+ Utils.showAlert(Utils.t("fileLoaded"), "success")
+ updateDataButtons()
+
+ await updateStats()
+
+ // Restore overlay if image data was loaded from file
+ await Utils.restoreOverlayFromData().catch(error => {
+ console.error('Failed to restore overlay from file:', error);
+ });
+
+ if (state.colorsChecked) {
+ uploadBtn.disabled = false
+ selectPosBtn.disabled = false
+ resizeBtn.disabled = false
+ } else {
+ uploadBtn.disabled = false;
+ }
+
+ if (state.imageLoaded && state.startPosition && state.region && state.colorsChecked) {
+ startBtn.disabled = false
+ }
+ }
+ } catch (error) {
+ if (error.message === "Invalid JSON file") {
+ Utils.showAlert(Utils.t("invalidFileFormat"), "error")
+ } else {
+ Utils.showAlert(Utils.t("fileError"), "error")
+ }
+ }
+ })
+ }
+
+ updateUI = (messageKey, type = "default", params = {}) => {
+ const message = Utils.t(messageKey, params)
+ statusText.textContent = message
+ statusText.className = `wplace-status status-${type}`
+ statusText.style.animation = "none"
+ void statusText.offsetWidth
+ statusText.style.animation = "slideIn 0.3s ease-out"
+ }
+
+ updateStats = async () => {
+ localStorage.removeItem("lp");
+ const { id, charges, cooldown, max, droplets } = await WPlaceService.getCharges();
+ state.currentCharges = Math.floor(charges);
+ state.cooldown = cooldown;
+ state.maxCharges = Math.floor(max) > 1 ? Math.floor(max) : state.maxCharges;
+ // Evaluate notifications every time we refresh server-side charges
+ NotificationManager.maybeNotifyChargesReached();
+
+ if (cooldownSlider.max != state.maxCharges) {
+ cooldownSlider.max = state.maxCharges;
+ }
+
+ let imageStatsHTML = '';
+ if (state.imageLoaded) {
+ const progress = state.totalPixels > 0 ? Math.round((state.paintedPixels / state.totalPixels) * 100) : 0;
+ const remainingPixels = state.totalPixels - state.paintedPixels;
+ state.estimatedTime = Utils.calculateEstimatedTime(remainingPixels, state.currentCharges, state.cooldown);
+ if (progressBar) progressBar.style.width = `${progress}%`;
+
+ imageStatsHTML = `
+
+
${Utils.t("progress")}
+
${progress}%
+
+
+
${Utils.t("pixels")}
+
${state.paintedPixels}/${state.totalPixels}
+
+
+
${Utils.t("estimatedTime")}
+
${Utils.formatTime(state.estimatedTime)}
+
+ `;
+ }
+
+ let colorSwatchesHTML = '';
+ if (state.colorsChecked) {
+ colorSwatchesHTML = state.availableColors.map(color => {
+ const rgbString = `rgb(${color.rgb.join(',')})`;
+ return ``;
+ }).join('');
+ }
+
+ let totalAllCharges = 0;
+ if (state.allAccountsInfo.length > 0) {
+ totalAllCharges = state.allAccountsInfo.reduce((sum, acc) => sum + Math.floor(acc.Charges || 0), 0);
+ totalMaxCharges = state.allAccountsInfo.reduce((sum, acc) => sum + Math.floor(acc.Max || 0), 0);
+ }
+
+ if (statsArea) statsArea.innerHTML = `
+ ${imageStatsHTML}
+
+
Total All Accounts Charges
+
${totalAllCharges}/${totalMaxCharges}
+
+
+
${Utils.t("charges")}
+
${Math.floor(state.currentCharges)} / ${state.maxCharges}
+
+ ${state.colorsChecked ? `
+
+
Available Colors (${state.availableColors.length})
+
+ ${colorSwatchesHTML}
+
+
+ ` : ''}
+ `;
+
+ // Sync with the accounts list
+ if (state.allAccountsInfo.length > 0 && id) {
+ const currentAccountInList = state.allAccountsInfo.find(acc => acc.ID === id);
+ if (currentAccountInList) {
+ currentAccountInList.Charges = state.currentCharges;
+ currentAccountInList.Max = state.maxCharges;
+ currentAccountInList.Droplets = droplets;
+ // Re-render the list to show the updated charge count
+ renderAccountsList();
+ }
+ }
+ }
+
+ updateDataButtons = () => {
+ const hasImageData = state.imageLoaded && state.imageData
+ saveBtn.disabled = !hasImageData
+ saveToFileBtn.disabled = !hasImageData
+ }
+
+ updateDataButtons()
+
+ function showResizeDialog(processor) {
+ let baseProcessor = processor;
+ let width, height;
+ if (state.originalImage?.dataUrl) {
+ baseProcessor = new ImageProcessor(state.originalImage.dataUrl);
+ width = state.originalImage.width;
+ height = state.originalImage.height;
+ } else {
+ const dims = processor.getDimensions();
+ width = dims.width;
+ height = dims.height;
+ }
+ const aspectRatio = width / height;
+
+ const rs = state.resizeSettings;
+ widthSlider.max = width * 2;
+ heightSlider.max = height * 2;
+ let initialW = width;
+ let initialH = height;
+ if (rs && Number.isFinite(rs.width) && Number.isFinite(rs.height) && rs.width > 0 && rs.height > 0) {
+ initialW = rs.width;
+ initialH = rs.height;
+ }
+ // Clamp to slider ranges
+ initialW = Math.max(parseInt(widthSlider.min, 10) || 10, Math.min(initialW, parseInt(widthSlider.max, 10)));
+ initialH = Math.max(parseInt(heightSlider.min, 10) || 10, Math.min(initialH, parseInt(heightSlider.max, 10)));
+ widthSlider.value = initialW;
+ heightSlider.value = initialH;
+ widthValue.textContent = initialW;
+ heightValue.textContent = initialH;
+ zoomSlider.value = 1;
+ if (zoomValue) zoomValue.textContent = '100%';
+ paintWhiteToggle.checked = state.paintWhitePixels;
+
+ let _previewTimer = null;
+ let _previewJobId = 0;
+ let _isDraggingSize = false;
+ let _zoomLevel = 1;
+ let _ditherWorkBuf = null;
+ let _ditherEligibleBuf = null;
+ const ensureDitherBuffers = (n) => {
+ if (!_ditherWorkBuf || _ditherWorkBuf.length !== n * 3) _ditherWorkBuf = new Float32Array(n * 3);
+ if (!_ditherEligibleBuf || _ditherEligibleBuf.length !== n) _ditherEligibleBuf = new Uint8Array(n);
+ return { work: _ditherWorkBuf, eligible: _ditherEligibleBuf };
+ };
+ let _maskImageData = null;
+ let _maskData = null;
+ let _dirty = null;
+ const _resetDirty = () => { _dirty = { minX: Infinity, minY: Infinity, maxX: -1, maxY: -1 }; };
+ const _markDirty = (x, y) => {
+ if (!_dirty) _resetDirty();
+ if (x < _dirty.minX) _dirty.minX = x;
+ if (y < _dirty.minY) _dirty.minY = y;
+ if (x > _dirty.maxX) _dirty.maxX = x;
+ if (y > _dirty.maxY) _dirty.maxY = y;
+ };
+ const _flushDirty = () => {
+ if (!_dirty || _dirty.maxX < _dirty.minX || _dirty.maxY < _dirty.minY) return;
+ const x = Math.max(0, _dirty.minX);
+ const y = Math.max(0, _dirty.minY);
+ const w = Math.min(maskCanvas.width - x, _dirty.maxX - x + 1);
+ const h = Math.min(maskCanvas.height - y, _dirty.maxY - y + 1);
+ if (w > 0 && h > 0) maskCtx.putImageData(_maskImageData, 0, 0, x, y, w, h);
+ _resetDirty();
+ };
+ const _ensureMaskOverlayBuffers = (w, h, rebuildFromMask = false) => {
+ if (!_maskImageData || _maskImageData.width !== w || _maskImageData.height !== h) {
+ _maskImageData = maskCtx.createImageData(w, h);
+ _maskData = _maskImageData.data;
+ rebuildFromMask = true;
+ }
+ if (rebuildFromMask) {
+ const m = state.resizeIgnoreMask;
+ const md = _maskData;
+ md.fill(0);
+ if (m) {
+ for (let i = 0; i < m.length; i++) if (m[i]) { const p = i * 4; md[p] = 255; md[p + 1] = 0; md[p + 2] = 0; md[p + 3] = 150; }
+ }
+ maskCtx.putImageData(_maskImageData, 0, 0);
+ _resetDirty();
+ }
+ };
+ const ensureMaskSize = (w, h) => {
+ if (!state.resizeIgnoreMask || state.resizeIgnoreMask.length !== w * h) {
+ state.resizeIgnoreMask = new Uint8Array(w * h);
+ }
+ baseCanvas.width = w; baseCanvas.height = h;
+ maskCanvas.width = w; maskCanvas.height = h;
+ maskCtx.clearRect(0, 0, maskCanvas.width, maskCanvas.height);
+ // Ensure overlay buffers exist and rebuild from mask when dimensions change
+ _ensureMaskOverlayBuffers(w, h, true);
+ };
+ _updateResizePreview = async () => {
+ const jobId = ++_previewJobId;
+ const newWidth = parseInt(widthSlider.value, 10);
+ const newHeight = parseInt(heightSlider.value, 10);
+ _zoomLevel = parseFloat(zoomSlider.value);
+
+ widthValue.textContent = newWidth;
+ heightValue.textContent = newHeight;
+
+ ensureMaskSize(newWidth, newHeight);
+ canvasStack.style.width = newWidth + 'px';
+ canvasStack.style.height = newHeight + 'px';
+ baseCtx.imageSmoothingEnabled = false;
+ if (!state.availableColors || state.availableColors.length === 0) {
+ if (baseProcessor !== processor && (!baseProcessor.img || !baseProcessor.canvas)) {
+ await baseProcessor.load();
+ }
+ baseCtx.clearRect(0, 0, newWidth, newHeight);
+ baseCtx.drawImage(baseProcessor.img, 0, 0, newWidth, newHeight);
+ // Draw existing mask overlay buffer
+ maskCtx.clearRect(0, 0, maskCanvas.width, maskCanvas.height);
+ if (_maskImageData) maskCtx.putImageData(_maskImageData, 0, 0);
+ updateZoomLayout();
+ return;
+ }
+ if (baseProcessor !== processor && (!baseProcessor.img || !baseProcessor.canvas)) {
+ await baseProcessor.load();
+ }
+ baseCtx.clearRect(0, 0, newWidth, newHeight);
+ baseCtx.drawImage(baseProcessor.img, 0, 0, newWidth, newHeight);
+ const imgData = baseCtx.getImageData(0, 0, newWidth, newHeight);
+ const data = imgData.data;
+
+ const tThresh = state.customTransparencyThreshold || CONFIG.TRANSPARENCY_THRESHOLD;
+
+ const applyFSDither = () => {
+ const w = newWidth, h = newHeight;
+ const n = w * h;
+ const { work, eligible } = ensureDitherBuffers(n);
+ for (let y = 0; y < h; y++) {
+ for (let x = 0; x < w; x++) {
+ const idx = y * w + x;
+ const i4 = idx * 4;
+ const r = data[i4], g = data[i4 + 1], b = data[i4 + 2], a = data[i4 + 3];
+ const isEligible = a >= tThresh && (state.paintWhitePixels || !Utils.isWhitePixel(r, g, b));
+ eligible[idx] = isEligible ? 1 : 0;
+ work[idx * 3] = r;
+ work[idx * 3 + 1] = g;
+ work[idx * 3 + 2] = b;
+ if (!isEligible) {
+ data[i4 + 3] = 0; // transparent in preview overlay
+ }
+ }
+ }
+
+ const diffuse = (nx, ny, er, eg, eb, factor) => {
+ if (nx < 0 || nx >= w || ny < 0 || ny >= h) return;
+ const nidx = ny * w + nx;
+ if (!eligible[nidx]) return;
+ const base = nidx * 3;
+ work[base] = Math.min(255, Math.max(0, work[base] + er * factor));
+ work[base + 1] = Math.min(255, Math.max(0, work[base + 1] + eg * factor));
+ work[base + 2] = Math.min(255, Math.max(0, work[base + 2] + eb * factor));
+ };
+
+ for (let y = 0; y < h; y++) {
+ for (let x = 0; x < w; x++) {
+ const idx = y * w + x;
+ if (!eligible[idx]) continue;
+ const base = idx * 3;
+ const r0 = work[base], g0 = work[base + 1], b0 = work[base + 2];
+ const [nr, ng, nb] = Utils.findClosestPaletteColor(r0, g0, b0, state.activeColorPalette);
+ const i4 = idx * 4;
+ data[i4] = nr;
+ data[i4 + 1] = ng;
+ data[i4 + 2] = nb;
+ data[i4 + 3] = 255;
+
+ const er = r0 - nr;
+ const eg = g0 - ng;
+ const eb = b0 - nb;
+
+ diffuse(x + 1, y, er, eg, eb, 7 / 16);
+ diffuse(x - 1, y + 1, er, eg, eb, 3 / 16);
+ diffuse(x, y + 1, er, eg, eb, 5 / 16);
+ diffuse(x + 1, y + 1, er, eg, eb, 1 / 16);
+ }
+ }
+ };
+
+ // Skip expensive dithering while user is dragging sliders
+ if (state.ditheringEnabled && !_isDraggingSize) {
+ applyFSDither();
+ } else {
+ for (let i = 0; i < data.length; i += 4) {
+ const r = data[i], g = data[i + 1], b = data[i + 2], a = data[i + 3];
+ if (a < tThresh || (!state.paintWhitePixels && Utils.isWhitePixel(r, g, b))) {
+ data[i + 3] = 0;
+ continue;
+ }
+ const [nr, ng, nb] = Utils.findClosestPaletteColor(r, g, b, state.activeColorPalette);
+ data[i] = nr;
+ data[i + 1] = ng;
+ data[i + 2] = nb;
+ data[i + 3] = 255;
+ }
+ }
+
+ if (jobId !== _previewJobId) return;
+ baseCtx.putImageData(imgData, 0, 0);
+ maskCtx.clearRect(0, 0, maskCanvas.width, maskCanvas.height);
+ if (_maskImageData) maskCtx.putImageData(_maskImageData, 0, 0);
+ updateZoomLayout();
+ };
+
+ const onWidthInput = () => {
+ if (keepAspect.checked) {
+ heightSlider.value = Math.round(parseInt(widthSlider.value, 10) / aspectRatio);
+ }
+ _updateResizePreview();
+ const curW = parseInt(widthSlider.value, 10);
+ const curH = parseInt(heightSlider.value, 10);
+ state.resizeSettings = { baseWidth: width, baseHeight: height, width: curW, height: curH };
+ saveBotSettings();
+ // Auto-fit after size changes
+ const fit = (typeof computeFitZoom === 'function') ? computeFitZoom() : 1;
+ if (!isNaN(fit) && isFinite(fit)) applyZoom(fit);
+ };
+
+ const onHeightInput = () => {
+ if (keepAspect.checked) {
+ widthSlider.value = Math.round(parseInt(heightSlider.value, 10) * aspectRatio);
+ }
+ _updateResizePreview();
+ const curW = parseInt(widthSlider.value, 10);
+ const curH = parseInt(heightSlider.value, 10);
+ state.resizeSettings = { baseWidth: width, baseHeight: height, width: curW, height: curH };
+ saveBotSettings();
+ // Auto-fit after size changes
+ const fit = (typeof computeFitZoom === 'function') ? computeFitZoom() : 1;
+ if (!isNaN(fit) && isFinite(fit)) applyZoom(fit);
+ };
+
+ paintWhiteToggle.onchange = (e) => {
+ state.paintWhitePixels = e.target.checked;
+ _updateResizePreview();
+ };
+
+ let panX = 0, panY = 0;
+ const clampPan = () => {
+ const wrapRect = panStage?.getBoundingClientRect() || { width: 0, height: 0 };
+ const w = (baseCanvas.width || 1) * _zoomLevel;
+ const h = (baseCanvas.height || 1) * _zoomLevel;
+ if (w <= wrapRect.width) {
+ panX = Math.floor((wrapRect.width - w) / 2);
+ } else {
+ const minX = wrapRect.width - w;
+ panX = Math.min(0, Math.max(minX, panX));
+ }
+ if (h <= wrapRect.height) {
+ panY = Math.floor((wrapRect.height - h) / 2);
+ } else {
+ const minY = wrapRect.height - h;
+ panY = Math.min(0, Math.max(minY, panY));
+ }
+ };
+ let _panRaf = 0;
+ const applyPan = () => {
+ if (_panRaf) return;
+ _panRaf = requestAnimationFrame(() => {
+ clampPan();
+ canvasStack.style.transform = `translate3d(${Math.round(panX)}px, ${Math.round(panY)}px, 0) scale(${_zoomLevel})`;
+ _panRaf = 0;
+ });
+ };
+
+ const updateZoomLayout = () => {
+ const w = baseCanvas.width || 1, h = baseCanvas.height || 1;
+ baseCanvas.style.width = w + 'px';
+ baseCanvas.style.height = h + 'px';
+ maskCanvas.style.width = w + 'px';
+ maskCanvas.style.height = h + 'px';
+ canvasStack.style.width = w + 'px';
+ canvasStack.style.height = h + 'px';
+ applyPan();
+ };
+ const applyZoom = (z) => {
+ _zoomLevel = Math.max(0.05, Math.min(20, z || 1));
+ zoomSlider.value = _zoomLevel;
+ updateZoomLayout();
+ if (zoomValue) zoomValue.textContent = `${Math.round(_zoomLevel * 100)}%`;
+ };
+ zoomSlider.addEventListener('input', () => {
+ applyZoom(parseFloat(zoomSlider.value));
+ });
+ if (zoomInBtn) zoomInBtn.addEventListener('click', () => applyZoom(parseFloat(zoomSlider.value) + 0.1));
+ if (zoomOutBtn) zoomOutBtn.addEventListener('click', () => applyZoom(parseFloat(zoomSlider.value) - 0.1));
+ const computeFitZoom = () => {
+ const wrapRect = panStage?.getBoundingClientRect();
+ if (!wrapRect) return 1;
+ const w = baseCanvas.width || 1;
+ const h = baseCanvas.height || 1;
+ const margin = 10;
+ const scaleX = (wrapRect.width - margin) / w;
+ const scaleY = (wrapRect.height - margin) / h;
+ return Math.max(0.05, Math.min(20, Math.min(scaleX, scaleY)));
+ };
+ if (zoomFitBtn) zoomFitBtn.addEventListener('click', () => { applyZoom(computeFitZoom()); centerInView(); });
+ if (zoomActualBtn) zoomActualBtn.addEventListener('click', () => { applyZoom(1); centerInView(); });
+
+ const centerInView = () => {
+ if (!panStage) return;
+ const rect = panStage.getBoundingClientRect();
+ const w = (baseCanvas.width || 1) * _zoomLevel;
+ const h = (baseCanvas.height || 1) * _zoomLevel;
+ panX = Math.floor((rect.width - w) / 2);
+ panY = Math.floor((rect.height - h) / 2);
+ applyPan();
+ };
+
+ let isPanning = false; let startX = 0, startY = 0, startPanX = 0, startPanY = 0;
+ let allowPan = false; // Space key
+ let panMode = false; // Explicit pan mode toggle for touch/one-button mice
+ const isPanMouseButton = (e) => e.button === 1 || e.button === 2;
+ const setCursor = (val) => { if (panStage) panStage.style.cursor = val; };
+ const isPanActive = (e) => panMode || allowPan || isPanMouseButton(e);
+ const updatePanModeBtn = () => {
+ if (!panModeBtn) return;
+ panModeBtn.classList.toggle('active', panMode);
+ panModeBtn.setAttribute('aria-pressed', panMode ? 'true' : 'false');
+ };
+ if (panModeBtn) {
+ updatePanModeBtn();
+ panModeBtn.addEventListener('click', () => { panMode = !panMode; updatePanModeBtn(); setCursor(panMode ? 'grab' : ''); });
+ }
+ if (panStage) {
+ panStage.addEventListener('contextmenu', (e) => { if (allowPan) e.preventDefault(); });
+ window.addEventListener('keydown', (e) => { if (e.code === 'Space') { allowPan = true; setCursor('grab'); } });
+ window.addEventListener('keyup', (e) => { if (e.code === 'Space') { allowPan = false; if (!isPanning) setCursor(''); } });
+ panStage.addEventListener('mousedown', (e) => {
+ if (!isPanActive(e)) return;
+ e.preventDefault();
+ isPanning = true; startX = e.clientX; startY = e.clientY; startPanX = panX; startPanY = panY;
+ setCursor('grabbing');
+ });
+ window.addEventListener('mousemove', (e) => {
+ if (!isPanning) return;
+ const dx = e.clientX - startX; const dy = e.clientY - startY;
+ panX = startPanX + dx; panY = startPanY + dy; applyPan();
+ });
+ window.addEventListener('mouseup', () => { if (isPanning) { isPanning = false; setCursor(allowPan ? 'grab' : ''); } });
+ panStage.addEventListener('wheel', (e) => {
+ if (!e.ctrlKey && !e.metaKey) return;
+ e.preventDefault();
+ const rect = panStage.getBoundingClientRect();
+ const cx = e.clientX - rect.left - panX;
+ const cy = e.clientY - rect.top - panY;
+ const before = _zoomLevel;
+ const step = Math.max(0.05, Math.min(0.5, Math.abs(e.deltaY) > 20 ? 0.2 : 0.1));
+ const next = Math.max(0.05, Math.min(20, before + (e.deltaY > 0 ? -step : step)));
+ if (next === before) return;
+ const scale = next / before;
+ panX = panX - cx * (scale - 1);
+ panY = panY - cy * (scale - 1);
+ applyZoom(next);
+ }, { passive: false });
+ let lastTouchDist = null;
+ let touchStartTime = 0;
+ let doubleTapTimer = null;
+ panStage.addEventListener('touchstart', (e) => {
+ if (e.touches.length === 1) {
+ const t = e.touches[0];
+ isPanning = true; startX = t.clientX; startY = t.clientY; startPanX = panX; startPanY = panY;
+ setCursor('grabbing');
+ const now = Date.now();
+ if (now - touchStartTime < 300) {
+ // double tap -> toggle 100%/fit
+ const z = Math.abs(_zoomLevel - 1) < 0.01 ? computeFitZoom() : 1;
+ applyZoom(z);
+ centerInView();
+ if (doubleTapTimer) clearTimeout(doubleTapTimer);
+ } else {
+ touchStartTime = now;
+ doubleTapTimer = setTimeout(() => { doubleTapTimer = null; }, 320);
+ }
+ } else if (e.touches.length === 2) {
+ // Pinch start
+ const [a, b] = e.touches;
+ lastTouchDist = Math.hypot(b.clientX - a.clientX, b.clientY - a.clientY);
+ }
+ }, { passive: true });
+ panStage.addEventListener('touchmove', (e) => {
+ if (e.touches.length === 1 && isPanning) {
+ const t = e.touches[0];
+ const dx = t.clientX - startX; const dy = t.clientY - startY;
+ panX = startPanX + dx; panY = startPanY + dy; applyPan();
+ } else if (e.touches.length === 2 && lastTouchDist != null) {
+ e.preventDefault();
+ const [a, b] = e.touches;
+ const dist = Math.hypot(b.clientX - a.clientX, b.clientY - a.clientY);
+ const rect = panStage.getBoundingClientRect();
+ const centerX = (a.clientX + b.clientX) / 2 - rect.left - panX;
+ const centerY = (a.clientY + b.clientY) / 2 - rect.top - panY;
+ const before = _zoomLevel;
+ const scale = dist / (lastTouchDist || dist);
+ const next = Math.max(0.05, Math.min(20, before * scale));
+ if (next !== before) {
+ panX = panX - centerX * (next / before - 1);
+ panY = panY - centerY * (next / before - 1);
+ applyZoom(next);
+ }
+ lastTouchDist = dist;
+ }
+ }, { passive: false });
+ panStage.addEventListener('touchend', () => {
+ isPanning = false; lastTouchDist = null; setCursor(panMode || allowPan ? 'grab' : '');
+ });
+ }
+ const schedulePreview = () => {
+ if (_previewTimer) clearTimeout(_previewTimer);
+ const run = () => {
+ _previewTimer = null;
+ _updateResizePreview();
+ };
+ if (window.requestIdleCallback) {
+ _previewTimer = setTimeout(() => requestIdleCallback(run, { timeout: 150 }), 50);
+ } else {
+ _previewTimer = setTimeout(() => requestAnimationFrame(run), 50);
+ }
+ };
+ // Track dragging to reduce work and skip dithering during drag
+ const markDragStart = () => { _isDraggingSize = true; };
+ const markDragEnd = () => { _isDraggingSize = false; schedulePreview(); };
+ widthSlider.addEventListener('pointerdown', markDragStart);
+ heightSlider.addEventListener('pointerdown', markDragStart);
+ widthSlider.addEventListener('pointerup', markDragEnd);
+ heightSlider.addEventListener('pointerup', markDragEnd);
+ widthSlider.addEventListener("input", () => { onWidthInput(); schedulePreview(); });
+ heightSlider.addEventListener("input", () => { onHeightInput(); schedulePreview(); });
+
+ // Mask painting UX: brush size, modes, row/column fills, and precise coords
+ let draggingMask = false;
+ let lastPaintX = -1, lastPaintY = -1;
+ let brushSize = 1;
+ let rowColSize = 1;
+ let maskMode = 'ignore'; // 'ignore' | 'unignore' | 'toggle'
+ const brushEl = resizeContainer.querySelector('#maskBrushSize');
+ const brushValEl = resizeContainer.querySelector('#maskBrushSizeValue');
+ const btnIgnore = resizeContainer.querySelector('#maskModeIgnore');
+ const btnUnignore = resizeContainer.querySelector('#maskModeUnignore');
+ const btnToggle = resizeContainer.querySelector('#maskModeToggle');
+ const clearIgnoredBtnEl = resizeContainer.querySelector('#clearIgnoredBtn');
+ const invertMaskBtn = resizeContainer.querySelector('#invertMaskBtn');
+ const rowColSizeEl = resizeContainer.querySelector('#rowColSize');
+ const rowColSizeValEl = resizeContainer.querySelector('#rowColSizeValue');
+
+ const updateModeButtons = () => {
+ const map = [
+ [btnIgnore, 'ignore'],
+ [btnUnignore, 'unignore'],
+ [btnToggle, 'toggle']
+ ];
+ for (const [el, m] of map) {
+ if (!el) continue;
+ const active = maskMode === m;
+ el.classList.toggle('active', active);
+ el.setAttribute('aria-pressed', active ? 'true' : 'false');
+ }
+ };
+ const setMode = (mode) => { maskMode = mode; updateModeButtons(); };
+ if (brushEl && brushValEl) {
+ brushEl.addEventListener('input', () => { brushSize = parseInt(brushEl.value, 10) || 1; brushValEl.textContent = brushSize; });
+ brushValEl.textContent = brushEl.value;
+ brushSize = parseInt(brushEl.value, 10) || 1;
+ }
+ if (rowColSizeEl && rowColSizeValEl) {
+ rowColSizeEl.addEventListener('input', () => { rowColSize = parseInt(rowColSizeEl.value, 10) || 1; rowColSizeValEl.textContent = rowColSize; });
+ rowColSizeValEl.textContent = rowColSizeEl.value;
+ rowColSize = parseInt(rowColSizeEl.value, 10) || 1;
+ }
+ if (btnIgnore) btnIgnore.addEventListener('click', () => setMode('ignore'));
+ if (btnUnignore) btnUnignore.addEventListener('click', () => setMode('unignore'));
+ if (btnToggle) btnToggle.addEventListener('click', () => setMode('toggle'));
+ // Initialize button state (default to toggle mode)
+ updateModeButtons();
+
+ const mapClientToPixel = (clientX, clientY) => {
+ // Compute without rounding until final step to avoid drift at higher zoom
+ const rect = baseCanvas.getBoundingClientRect();
+ const scaleX = rect.width / baseCanvas.width;
+ const scaleY = rect.height / baseCanvas.height;
+ const dx = (clientX - rect.left) / scaleX;
+ const dy = (clientY - rect.top) / scaleY;
+ const x = Math.floor(dx);
+ const y = Math.floor(dy);
+ return { x, y };
+ };
+
+ const ensureMask = (w, h) => {
+ if (!state.resizeIgnoreMask || state.resizeIgnoreMask.length !== w * h) {
+ state.resizeIgnoreMask = new Uint8Array(w * h);
+ }
+ };
+
+ const paintCircle = (cx, cy, radius, value) => {
+ const w = baseCanvas.width, h = baseCanvas.height;
+ ensureMask(w, h);
+ const r2 = radius * radius;
+ for (let yy = cy - radius; yy <= cy + radius; yy++) {
+ if (yy < 0 || yy >= h) continue;
+ for (let xx = cx - radius; xx <= cx + radius; xx++) {
+ if (xx < 0 || xx >= w) continue;
+ const dx = xx - cx, dy = yy - cy;
+ if (dx * dx + dy * dy <= r2) {
+ const idx = yy * w + xx;
+ let val = state.resizeIgnoreMask[idx];
+ if (maskMode === 'toggle') {
+ val = val ? 0 : 1;
+ } else if (maskMode === 'ignore') {
+ val = 1;
+ } else {
+ val = 0;
+ }
+ state.resizeIgnoreMask[idx] = val;
+ if (_maskData) {
+ const p = idx * 4;
+ if (val) { _maskData[p] = 255; _maskData[p + 1] = 0; _maskData[p + 2] = 0; _maskData[p + 3] = 150; }
+ else { _maskData[p] = 0; _maskData[p + 1] = 0; _maskData[p + 2] = 0; _maskData[p + 3] = 0; }
+ _markDirty(xx, yy);
+ }
+ }
+ }
+ }
+ };
+
+ const paintRow = (y, value) => {
+ const w = baseCanvas.width, h = baseCanvas.height;
+ ensureMask(w, h);
+ if (y < 0 || y >= h) return;
+
+ // Paint multiple rows based on rowColSize
+ const halfSize = Math.floor(rowColSize / 2);
+ const startY = Math.max(0, y - halfSize);
+ const endY = Math.min(h - 1, y + halfSize);
+
+ for (let rowY = startY; rowY <= endY; rowY++) {
+ for (let x = 0; x < w; x++) {
+ const idx = rowY * w + x;
+ let val = state.resizeIgnoreMask[idx];
+ if (maskMode === 'toggle') {
+ val = val ? 0 : 1;
+ } else if (maskMode === 'ignore') {
+ val = 1;
+ } else {
+ val = 0;
+ }
+ state.resizeIgnoreMask[idx] = val;
+ if (_maskData) {
+ const p = idx * 4;
+ if (val) { _maskData[p] = 255; _maskData[p + 1] = 0; _maskData[p + 2] = 0; _maskData[p + 3] = 150; }
+ else { _maskData[p] = 0; _maskData[p + 1] = 0; _maskData[p + 2] = 0; _maskData[p + 3] = 0; }
+ }
+ }
+ if (_maskData) { _markDirty(0, rowY); _markDirty(w - 1, rowY); }
+ }
+ };
+
+ const paintColumn = (x, value) => {
+ const w = baseCanvas.width, h = baseCanvas.height;
+ ensureMask(w, h);
+ if (x < 0 || x >= w) return;
+
+ // Paint multiple columns based on rowColSize
+ const halfSize = Math.floor(rowColSize / 2);
+ const startX = Math.max(0, x - halfSize);
+ const endX = Math.min(w - 1, x + halfSize);
+
+ for (let colX = startX; colX <= endX; colX++) {
+ for (let y = 0; y < h; y++) {
+ const idx = y * w + colX;
+ let val = state.resizeIgnoreMask[idx];
+ if (maskMode === 'toggle') {
+ val = val ? 0 : 1;
+ } else if (maskMode === 'ignore') {
+ val = 1;
+ } else {
+ val = 0;
+ }
+ state.resizeIgnoreMask[idx] = val;
+ if (_maskData) {
+ const p = idx * 4;
+ if (val) { _maskData[p] = 255; _maskData[p + 1] = 0; _maskData[p + 2] = 0; _maskData[p + 3] = 150; }
+ else { _maskData[p] = 0; _maskData[p + 1] = 0; _maskData[p + 2] = 0; _maskData[p + 3] = 0; }
+ }
+ }
+ if (_maskData) { _markDirty(colX, 0); _markDirty(colX, h - 1); }
+ }
+ };
+
+ const redrawMaskOverlay = () => {
+ // Only flush the dirty region; full rebuild happens on size change
+ _flushDirty();
+ };
+
+ const handlePaint = (e) => {
+ // Suppress painting while panning
+ if ((e.buttons & 4) === 4 || (e.buttons & 2) === 2 || allowPan) return;
+ const { x, y } = mapClientToPixel(e.clientX, e.clientY);
+ const w = baseCanvas.width, h = baseCanvas.height;
+ if (x < 0 || y < 0 || x >= w || y >= h) return;
+ const radius = Math.max(1, Math.floor(brushSize / 2));
+ if (e.shiftKey) {
+ paintRow(y);
+ } else if (e.altKey) {
+ paintColumn(x);
+ } else {
+ paintCircle(x, y, radius);
+ }
+ lastPaintX = x; lastPaintY = y;
+ redrawMaskOverlay();
+ };
+
+ maskCanvas.addEventListener('mousedown', (e) => {
+ if (e.button === 1 || e.button === 2 || allowPan) return; // let pan handler manage
+ draggingMask = true; handlePaint(e);
+ });
+ // Avoid hijacking touch gestures for panning/zooming
+ maskCanvas.addEventListener('touchstart', (e) => { /* let panStage handle */ }, { passive: true });
+ maskCanvas.addEventListener('touchmove', (e) => { /* let panStage handle */ }, { passive: true });
+ maskCanvas.addEventListener('touchend', (e) => { /* let panStage handle */ }, { passive: true });
+ window.addEventListener('mousemove', (e) => { if (draggingMask) handlePaint(e); });
+ window.addEventListener('mouseup', () => { if (draggingMask) { draggingMask = false; saveBotSettings(); } });
+
+ if (clearIgnoredBtnEl) clearIgnoredBtnEl.addEventListener('click', () => {
+ const w = baseCanvas.width, h = baseCanvas.height;
+ if (state.resizeIgnoreMask) state.resizeIgnoreMask.fill(0);
+ _ensureMaskOverlayBuffers(w, h, true);
+ _updateResizePreview();
+ saveBotSettings();
+ });
+
+ if (invertMaskBtn) invertMaskBtn.addEventListener('click', () => {
+ if (!state.resizeIgnoreMask) return;
+ for (let i = 0; i < state.resizeIgnoreMask.length; i++) state.resizeIgnoreMask[i] = state.resizeIgnoreMask[i] ? 0 : 1;
+ const w = baseCanvas.width, h = baseCanvas.height;
+ _ensureMaskOverlayBuffers(w, h, true);
+ _updateResizePreview();
+ saveBotSettings();
+ });
+
+ confirmResize.onclick = async () => {
+ const newWidth = parseInt(widthSlider.value, 10);
+ const newHeight = parseInt(heightSlider.value, 10);
+
+ // Generate the final paletted image data
+ const tempCanvas = document.createElement('canvas');
+ const tempCtx = tempCanvas.getContext('2d');
+ tempCanvas.width = newWidth;
+ tempCanvas.height = newHeight;
+ tempCtx.imageSmoothingEnabled = false;
+ if (baseProcessor !== processor && (!baseProcessor.img || !baseProcessor.canvas)) {
+ await baseProcessor.load();
+ }
+ tempCtx.drawImage(baseProcessor.img, 0, 0, newWidth, newHeight);
+ const imgData = tempCtx.getImageData(0, 0, newWidth, newHeight);
+ const data = imgData.data;
+ const tThresh2 = state.customTransparencyThreshold || CONFIG.TRANSPARENCY_THRESHOLD;
+ let totalValidPixels = 0;
+ const mask = (state.resizeIgnoreMask && state.resizeIgnoreMask.length === newWidth * newHeight) ? state.resizeIgnoreMask : null;
+
+ const applyFSDitherFinal = async () => {
+ const w = newWidth, h = newHeight;
+ const n = w * h;
+ const { work, eligible } = ensureDitherBuffers(n);
+ for (let y = 0; y < h; y++) {
+ for (let x = 0; x < w; x++) {
+ const idx = y * w + x;
+ const i4 = idx * 4;
+ const r = data[i4], g = data[i4 + 1], b = data[i4 + 2], a = data[i4 + 3];
+ const masked = mask && mask[idx];
+ const isEligible = !masked && a >= tThresh2 && (state.paintWhitePixels || !Utils.isWhitePixel(r, g, b));
+ eligible[idx] = isEligible ? 1 : 0;
+ work[idx * 3] = r;
+ work[idx * 3 + 1] = g;
+ work[idx * 3 + 2] = b;
+ if (!isEligible) {
+ data[i4 + 3] = 0;
+ }
+ }
+ // Yield to keep UI responsive
+ if ((y & 15) === 0) await Promise.resolve();
+ }
+
+ const diffuse = (nx, ny, er, eg, eb, factor) => {
+ if (nx < 0 || nx >= w || ny < 0 || ny >= h) return;
+ const nidx = ny * w + nx;
+ if (!eligible[nidx]) return;
+ const base = nidx * 3;
+ work[base] = Math.min(255, Math.max(0, work[base] + er * factor));
+ work[base + 1] = Math.min(255, Math.max(0, work[base + 1] + eg * factor));
+ work[base + 2] = Math.min(255, Math.max(0, work[base + 2] + eb * factor));
+ };
+
+ for (let y = 0; y < h; y++) {
+ for (let x = 0; x < w; x++) {
+ const idx = y * w + x;
+ if (!eligible[idx]) continue;
+ const base = idx * 3;
+ const r0 = work[base], g0 = work[base + 1], b0 = work[base + 2];
+ const [nr, ng, nb] = Utils.findClosestPaletteColor(r0, g0, b0, state.activeColorPalette);
+ const i4 = idx * 4;
+ data[i4] = nr;
+ data[i4 + 1] = ng;
+ data[i4 + 2] = nb;
+ data[i4 + 3] = 255;
+ totalValidPixels++;
+
+ const er = r0 - nr;
+ const eg = g0 - ng;
+ const eb = b0 - nb;
+
+ diffuse(x + 1, y, er, eg, eb, 7 / 16);
+ diffuse(x - 1, y + 1, er, eg, eb, 3 / 16);
+ diffuse(x, y + 1, er, eg, eb, 5 / 16);
+ diffuse(x + 1, y + 1, er, eg, eb, 1 / 16);
+ }
+ // Yield every row to reduce jank
+ await Promise.resolve();
+ }
+ };
+
+ if (state.ditheringEnabled) {
+ await applyFSDitherFinal();
+ } else {
+ for (let i = 0; i < data.length; i += 4) {
+ const r = data[i], g = data[i + 1], b = data[i + 2], a = data[i + 3];
+ const masked = mask && mask[(i >> 2)];
+ const isTransparent = a < tThresh2 || masked;
+ const isWhiteAndSkipped = !state.paintWhitePixels && Utils.isWhitePixel(r, g, b);
+ if (isTransparent || isWhiteAndSkipped) {
+ data[i + 3] = 0; // overlay transparency
+ continue;
+ }
+ totalValidPixels++;
+ const [nr, ng, nb] = Utils.findClosestPaletteColor(r, g, b, state.activeColorPalette);
+ data[i] = nr;
+ data[i + 1] = ng;
+ data[i + 2] = nb;
+ data[i + 3] = 255;
+ }
+ }
+ tempCtx.putImageData(imgData, 0, 0);
+
+ // Save the final pixel data for painting
+ // Persist the paletted (and possibly dithered) pixels so painting uses the same output seen in overlay
+ const palettedPixels = new Uint8ClampedArray(imgData.data);
+ state.imageData.pixels = palettedPixels;
+ state.imageData.width = newWidth;
+ state.imageData.height = newHeight;
+ state.imageData.totalPixels = totalValidPixels;
+ state.totalPixels = totalValidPixels;
+ state.paintedPixels = 0;
+
+ state.resizeSettings = { baseWidth: width, baseHeight: height, width: newWidth, height: newHeight };
+ saveBotSettings();
+
+ const finalImageBitmap = await createImageBitmap(tempCanvas);
+ await overlayManager.setImage(finalImageBitmap);
+ overlayManager.enable();
+ toggleOverlayBtn.classList.add('active');
+ toggleOverlayBtn.setAttribute('aria-pressed', 'true');
+
+ // Keep state.imageData.processor as the original-based source; painting uses paletted pixels already stored
+
+ updateStats();
+ updateUI("resizeSuccess", "success", { width: newWidth, height: newHeight });
+ closeResizeDialog();
+ };
+
+ downloadPreviewBtn.onclick = () => {
+ try {
+ const w = baseCanvas.width, h = baseCanvas.height;
+ const out = document.createElement('canvas');
+ out.width = w; out.height = h;
+ const octx = out.getContext('2d');
+ octx.imageSmoothingEnabled = false;
+ octx.drawImage(baseCanvas, 0, 0);
+ octx.drawImage(maskCanvas, 0, 0);
+ const link = document.createElement('a');
+ link.download = 'wplace-preview.png';
+ link.href = out.toDataURL();
+ link.click();
+ } catch (e) { console.warn('Failed to download preview:', e); }
+ };
+
+ cancelResize.onclick = closeResizeDialog;
+
+ resizeOverlay.style.display = "block";
+ resizeContainer.style.display = "block";
+
+ // Reinitialize color palette with current available colors
+ initializeColorPalette(resizeContainer);
+
+ _updateResizePreview();
+ _resizeDialogCleanup = () => {
+ try { zoomSlider.replaceWith(zoomSlider.cloneNode(true)); } catch { }
+ try { if (zoomInBtn) zoomInBtn.replaceWith(zoomInBtn.cloneNode(true)); } catch { }
+ try { if (zoomOutBtn) zoomOutBtn.replaceWith(zoomOutBtn.cloneNode(true)); } catch { }
+ };
+ setTimeout(() => {
+ if (typeof computeFitZoom === 'function') {
+ const z = computeFitZoom();
+ if (!isNaN(z) && isFinite(z)) {
+ applyZoom(z);
+ centerInView();
+ }
+ } else {
+ centerInView();
+ }
+ }, 0);
+ }
+
+ function closeResizeDialog() {
+ try { if (typeof _resizeDialogCleanup === 'function') { _resizeDialogCleanup(); } } catch { }
+ resizeOverlay.style.display = "none";
+ resizeContainer.style.display = "none";
+ _updateResizePreview = () => { };
+ try { if (typeof cancelAnimationFrame === 'function' && _panRaf) { cancelAnimationFrame(_panRaf); } } catch { }
+ try { if (_previewTimer) { clearTimeout(_previewTimer); _previewTimer = null; } } catch { }
+ _maskImageData = null; _maskData = null; _dirty = null;
+ _ditherWorkBuf = null; _ditherEligibleBuf = null;
+ _resizeDialogCleanup = null;
+ }
+
+ if (uploadBtn) {
+ uploadBtn.addEventListener("click", async () => {
+ const availableColors = Utils.extractAvailableColors();
+ if (availableColors.length < 10) {
+ updateUI("noColorsFound", "error");
+ Utils.showAlert(Utils.t("noColorsFound"), "error");
+ return;
+ }
+
+ if (!state.colorsChecked) {
+ state.availableColors = availableColors;
+ state.colorsChecked = true;
+ updateUI("colorsFound", "success", { count: availableColors.length });
+ updateStats();
+ selectPosBtn.disabled = false;
+ // Only enable resize button if image is also loaded
+ if (state.imageLoaded) {
+ resizeBtn.disabled = false;
+ }
+ }
+
+ try {
+ updateUI("loadingImage", "default")
+ const imageSrc = await Utils.createImageUploader()
+ if (!imageSrc) {
+ updateUI("colorsFound", "success", { count: state.availableColors.length });
+ return;
+ }
+
+ const processor = new ImageProcessor(imageSrc)
+ await processor.load()
+
+ const { width, height } = processor.getDimensions()
+ const pixels = processor.getPixelData()
+
+ let totalValidPixels = 0;
+ for (let i = 0; i < pixels.length; i += 4) {
+ const isTransparent = pixels[i + 3] < (state.customTransparencyThreshold || CONFIG.TRANSPARENCY_THRESHOLD);
+ const isWhiteAndSkipped = !state.paintWhitePixels && Utils.isWhitePixel(pixels[i], pixels[i + 1], pixels[i + 2]);
+ if (!isTransparent && !isWhiteAndSkipped) {
+ totalValidPixels++;
+ }
+ }
+
+ state.imageData = {
+ width,
+ height,
+ pixels,
+ totalPixels: totalValidPixels,
+ processor,
+ }
+
+ state.totalPixels = totalValidPixels
+ state.paintedPixels = 0
+ state.imageLoaded = true
+ state.lastPosition = { x: 0, y: 0 }
+
+ // Initialize painted map for tracking
+ Utils.initializePaintedMap(width, height);
+
+ // New image: clear previous resize settings
+ state.resizeSettings = null;
+ // Also clear any previous ignore mask
+ state.resizeIgnoreMask = null;
+ // Save original image for this browser (dataUrl + dims)
+ state.originalImage = { dataUrl: imageSrc, width, height };
+ saveBotSettings();
+
+ // Use the original image for the overlay initially
+ const imageBitmap = await createImageBitmap(processor.img);
+ await overlayManager.setImage(imageBitmap);
+ overlayManager.enable();
+ toggleOverlayBtn.disabled = false;
+ toggleOverlayBtn.classList.add('active');
+ toggleOverlayBtn.setAttribute('aria-pressed', 'true');
+
+ // Only enable resize button if colors have also been captured
+ if (state.colorsChecked) {
+ resizeBtn.disabled = false;
+ }
+ saveBtn.disabled = false
+
+ if (state.startPosition) {
+ startBtn.disabled = false
+ }
+
+ updateStats()
+ updateDataButtons()
+ updateUI("imageLoaded", "success", { count: totalValidPixels })
+ } catch {
+ updateUI("imageError", "error")
+ }
+ })
+ }
+
+ if (resizeBtn) {
+ resizeBtn.addEventListener("click", () => {
+ if (state.imageLoaded && state.imageData.processor && state.colorsChecked) {
+ showResizeDialog(state.imageData.processor)
+ } else if (!state.colorsChecked) {
+ Utils.showAlert("Please upload an image first to capture available colors", "warning")
+ }
+ })
+ }
+
+ if (selectPosBtn) {
+ selectPosBtn.addEventListener("click", async () => {
+ if (state.selectingPosition) return
+
+ state.selectingPosition = true
+ state.startPosition = null
+ state.region = null
+ startBtn.disabled = true
+
+ Utils.showAlert(Utils.t("selectPositionAlert"), "info")
+ updateUI("waitingPosition", "default")
+
+ const tempFetch = async (url, options) => {
+ if (
+ typeof url === "string" &&
+ url.includes("https://backend.wplace.live/s0/pixel/") &&
+ options?.method?.toUpperCase() === "POST"
+ ) {
+ try {
+ const response = await originalFetch(url, options)
+ const clonedResponse = response.clone()
+ const data = await clonedResponse.json()
+
+ if (data?.painted === 1) {
+ const regionMatch = url.match(/\/pixel\/(\d+)\/(\d+)/)
+ if (regionMatch && regionMatch.length >= 3) {
+ state.region = {
+ x: Number.parseInt(regionMatch[1]),
+ y: Number.parseInt(regionMatch[2]),
+ }
+ }
+
+ const payload = JSON.parse(options.body)
+ if (payload?.coords && Array.isArray(payload.coords)) {
+ state.startPosition = {
+ x: payload.coords[0],
+ y: payload.coords[1],
+ }
+ state.lastPosition = { x: 0, y: 0 }
+
+ await overlayManager.setPosition(state.startPosition, state.region);
+
+ if (state.imageLoaded) {
+ startBtn.disabled = false
+ }
+
+ window.fetch = originalFetch
+ state.selectingPosition = false
+ updateUI("positionSet", "success")
+ }
+ }
+
+ return response
+ } catch {
+ return originalFetch(url, options)
+ }
+ }
+ return originalFetch(url, options)
+ }
+
+ const originalFetch = window.fetch;
+ window.fetch = tempFetch;
+
+ setTimeout(() => {
+ if (state.selectingPosition) {
+ window.fetch = originalFetch
+ state.selectingPosition = false
+ updateUI("positionTimeout", "error")
+ Utils.showAlert(Utils.t("positionTimeout"), "error")
+ }
+ }, 120000)
+ })
+ }
+
+ async function startPainting() {
+ if (!state.imageLoaded || !state.startPosition || !state.region) {
+ updateUI("missingRequirements", "error")
+ return false
+ }
+ await ensureToken()
+ if (!turnstileToken) return false
+
+ state.running = true
+ state.stopFlag = false
+ startBtn.disabled = true
+ stopBtn.disabled = false
+ uploadBtn.disabled = true
+ selectPosBtn.disabled = true
+ resizeBtn.disabled = true
+ saveBtn.disabled = true
+ toggleOverlayBtn.disabled = true;
+
+ updateUI("startPaintingMsg", "success")
+
+ try {
+ await getAccounts();
+ await processImage();
+ return true
+ } catch {
+ updateUI("paintingError", "error")
+ return false
+ } finally {
+ state.running = false
+ stopBtn.disabled = true
+ saveBtn.disabled = false
+
+ if (!state.stopFlag) {
+ startBtn.disabled = true
+ uploadBtn.disabled = false
+ selectPosBtn.disabled = false
+ resizeBtn.disabled = false
+ } else {
+ startBtn.disabled = false
+ }
+ toggleOverlayBtn.disabled = false;
+ }
+ }
+
+ if (startBtn) {
+ startBtn.addEventListener("click", startPainting)
+ }
+
+ if (stopBtn) {
+ stopBtn.addEventListener("click", () => {
+ state.stopFlag = true
+ state.running = false
+ stopBtn.disabled = true
+ updateUI("paintingStopped", "warning")
+
+ if (state.imageLoaded && state.paintedPixels > 0) {
+ Utils.saveProgress()
+ Utils.showAlert(Utils.t("autoSaved"), "success")
+ }
+ })
+ }
+
+ const checkSavedProgress = () => {
+ const savedData = Utils.loadProgress()
+ if (savedData && savedData.state.paintedPixels > 0) {
+ const savedDate = new Date(savedData.timestamp).toLocaleString()
+ const progress = Math.round((savedData.state.paintedPixels / savedData.state.totalPixels) * 100)
+
+ Utils.showAlert(
+ `${Utils.t("savedDataFound")}\n\n` +
+ `Saved: ${savedDate}\n` +
+ `Progress: ${savedData.state.paintedPixels}/${savedData.state.totalPixels} pixels (${progress}%)\n` +
+ `${Utils.t("clickLoadToContinue")}`,
+ "info",
+ )
+ }
+ }
+
+ setTimeout(checkSavedProgress, 1000)
+
+ if (cooldownSlider && cooldownValue) {
+ cooldownSlider.addEventListener("input", (e) => {
+ const threshold = parseInt(e.target.value);
+ state.cooldownChargeThreshold = threshold;
+ cooldownValue.textContent = threshold;
+ saveBotSettings();
+ NotificationManager.resetEdgeTracking(); // prevent spurious notify after threshold change
+ });
+ }
+
+ loadBotSettings();
+ // Ensure notification poller reflects current settings
+ NotificationManager.syncFromState();
+ }
+
+ async function processImage() {
+ const { width, height, pixels } = state.imageData
+ const { x: startX, y: startY } = state.startPosition
+ const { x: regionX, y: regionY } = state.region
+
+ const tThresh2 = state.customTransparencyThreshold || CONFIG.TRANSPARENCY_THRESHOLD;
+ const isEligibleAt = (x, y) => {
+ const idx = (y * width + x) * 4;
+ const r = pixels[idx], g = pixels[idx + 1], b = pixels[idx + 2], a = pixels[idx + 3];
+ if (a < tThresh2) return false;
+ if (!state.paintWhitePixels && Utils.isWhitePixel(r, g, b)) return false;
+ return true;
+ };
+
+ let startRow = 0;
+ let startCol = 0;
+ let foundStart = false;
+ let seen = 0;
+ const target = Math.max(0, Math.min(state.paintedPixels || 0, width * height));
+ for (let y = 0; y < height && !foundStart; y++) {
+ for (let x = 0; x < width; x++) {
+ if (!isEligibleAt(x, y)) continue;
+ if (seen === target) { startRow = y; startCol = x; foundStart = true; break; }
+ seen++;
+ }
+ }
+ if (!foundStart) { startRow = height; startCol = 0; }
+
+ let pixelBatch = null;
+ let skippedPixels = { transparent: 0, white: 0, alreadyPainted: 0 };
+
+ try {
+ outerLoop: for (let y = startRow; y < height; y++) {
+ for (let x = y === startRow ? startCol : 0; x < width; x++) {
+ if (state.stopFlag) {
+ if (pixelBatch && pixelBatch.pixels.length > 0) {
+ console.log(`🎯 Sending final batch before stop with ${pixelBatch.pixels.length} pixels`);
+ const success = await sendBatchWithRetry(pixelBatch.pixels, pixelBatch.regionX, pixelBatch.regionY);
+ if (success) {
+ pixelBatch.pixels.forEach(() => { state.paintedPixels++; });
+ state.currentCharges -= pixelBatch.pixels.length;
+ updateStats();
+ }
+ }
+ state.lastPosition = { x, y }
+ updateUI("paintingPaused", "warning", { x, y })
+ break outerLoop
+ }
+
+
+ const idx = (y * width + x) * 4
+ const r = pixels[idx]
+ const g = pixels[idx + 1]
+ const b = pixels[idx + 2]
+ const alpha = pixels[idx + 3]
+
+ const tThresh2 = state.customTransparencyThreshold || CONFIG.TRANSPARENCY_THRESHOLD;
+ if (alpha < tThresh2 || (!state.paintWhitePixels && Utils.isWhitePixel(r, g, b))) {
+ if (alpha < tThresh2) {
+ skippedPixels.transparent++;
+ } else {
+ skippedPixels.white++;
+ }
+ continue;
+ }
+
+ let targetRgb;
+ if (Utils.isWhitePixel(r, g, b)) {
+ targetRgb = [255, 255, 255];
+ } else {
+ targetRgb = Utils.findClosestPaletteColor(r, g, b, state.activeColorPalette);
+ }
+
+ const colorId = findClosestColor([r, g, b], state.availableColors);
+
+ let absX = startX + x;
+ let absY = startY + y;
+
+ let adderX = Math.floor(absX / 1000);
+ let adderY = Math.floor(absY / 1000);
+ let pixelX = absX % 1000;
+ let pixelY = absY % 1000;
+
+ if (!pixelBatch ||
+ pixelBatch.regionX !== regionX + adderX ||
+ pixelBatch.regionY !== regionY + adderY) {
+
+ if (pixelBatch && pixelBatch.pixels.length > 0) {
+ console.log(`🌍 Sending region-change batch with ${pixelBatch.pixels.length} pixels (switching to region ${regionX + adderX},${regionY + adderY})`);
+ const success = await sendBatchWithRetry(pixelBatch.pixels, pixelBatch.regionX, pixelBatch.regionY);
+
+ if (success) {
+ pixelBatch.pixels.forEach((p) => {
+ state.paintedPixels++;
+ // Mark pixel as painted in map
+ Utils.markPixelPainted(p.x, p.y, pixelBatch.regionX, pixelBatch.regionY);
+ });
+ state.currentCharges -= pixelBatch.pixels.length;
+ updateUI("paintingProgress", "default", {
+ painted: state.paintedPixels,
+ total: state.totalPixels,
+ })
+
+ // Use smart save instead of fixed interval
+ Utils.performSmartSave();
+
+ if (CONFIG.PAINTING_SPEED_ENABLED && state.paintingSpeed > 0 && pixelBatch.pixels.length > 0) {
+ // paintingSpeed now represents batch size, so add a small delay based on batch size
+ const batchDelayFactor = Math.max(1, 100 / state.paintingSpeed); // Larger batches = less delay
+ const totalDelay = Math.max(100, batchDelayFactor * pixelBatch.pixels.length);
+ await Utils.sleep(totalDelay)
+ }
+ updateStats();
+ } else {
+ // If batch failed after all retries, stop painting to prevent infinite loops
+ console.error(`❌ Batch failed permanently after retries. Stopping painting.`);
+ state.stopFlag = true;
+ break outerLoop;
+ }
+ }
+
+ pixelBatch = {
+ regionX: regionX + adderX,
+ regionY: regionY + adderY,
+ pixels: []
+ };
+ }
+
+
+ try {
+ const tileRegionX = pixelBatch ? (pixelBatch.regionX) : (regionX + adderX);
+ const tileRegionY = pixelBatch ? (pixelBatch.regionY) : (regionY + adderY);
+ const tileKeyParts = [(regionX + adderX), (regionY + adderY)];
+ const existingColorRGBA = await overlayManager.getTilePixelColor(tileKeyParts[0], tileKeyParts[1], pixelX, pixelY).catch(() => null);
+ if (existingColorRGBA && Array.isArray(existingColorRGBA)) {
+ const [er, eg, eb] = existingColorRGBA;
+ const existingColorId = findClosestColor([er, eg, eb], state.availableColors);
+ // console.log(`pixel at (${pixelX}, ${pixelY}) has color ${existingColorId} it should be ${colorId}`);
+ if (existingColorId === colorId) {
+ skippedPixels.alreadyPainted++;
+ console.log(`Skipped already painted pixel at (${pixelX}, ${pixelY})`);
+ continue; // Skip
+ }
+ }
+ } catch (e) {
+ /* ignore */
+ }
+
+ pixelBatch.pixels.push({
+ x: pixelX,
+ y: pixelY,
+ color: colorId,
+ localX: x,
+ localY: y,
+ });
+
+ // Calculate batch size based on mode (normal/random)
+ const maxBatchSize = calculateBatchSize();
+ if (pixelBatch.pixels.length >= maxBatchSize) {
+ const modeText = state.batchMode === 'random' ? `random (${state.randomBatchMin}-${state.randomBatchMax})` : 'normal';
+ console.log(`📦 Sending batch with ${pixelBatch.pixels.length} pixels (mode: ${modeText}, target: ${maxBatchSize})`);
+ const success = await sendBatchWithRetry(pixelBatch.pixels, pixelBatch.regionX, pixelBatch.regionY);
+
+ if (success) {
+ pixelBatch.pixels.forEach((pixel) => {
+ state.paintedPixels++;
+ // Mark pixel as painted in map
+ Utils.markPixelPainted(pixel.x, pixel.y, pixelBatch.regionX, pixelBatch.regionY);
+ })
+
+ state.currentCharges -= pixelBatch.pixels.length;
+ updateStats()
+ updateUI("paintingProgress", "default", {
+ painted: state.paintedPixels,
+ total: state.totalPixels,
+ })
+
+ // Use smart save instead of fixed interval
+ Utils.performSmartSave();
+
+ if (CONFIG.PAINTING_SPEED_ENABLED && state.paintingSpeed > 0 && pixelBatch.pixels.length > 0) {
+ const delayPerPixel = 1000 / state.paintingSpeed // ms per pixel
+ const totalDelay = Math.max(100, delayPerPixel * pixelBatch.pixels.length) // minimum 100ms
+ await Utils.sleep(totalDelay)
+ }
+ } else {
+ // If batch failed after all retries, stop painting to prevent infinite loops
+ console.error(`❌ Batch failed permanently after retries. Stopping painting.`);
+ state.stopFlag = true;
+ break outerLoop;
+ }
+
+ pixelBatch.pixels = [];
+ }
+ if (!CONFIG.autoSwap) {
+ while (state.currentCharges < state.cooldownChargeThreshold && !state.stopFlag) {
+ const { charges, cooldown } = await WPlaceService.getCharges();
+ state.currentCharges = Math.floor(charges);
+ state.cooldown = cooldown;
+
+ if (state.currentCharges >= state.cooldownChargeThreshold) {
+ // Edge-trigger a notification the instant threshold is crossed
+ NotificationManager.maybeNotifyChargesReached(true);
+ updateStats();
+ break;
+ }
+
+ // Enable save button during cooldown wait
+ saveBtn.disabled = false;
+
+ updateUI("noChargesThreshold", "warning", {
+ time: Utils.formatTime(state.cooldown),
+ threshold: state.cooldownChargeThreshold,
+ current: state.currentCharges
+ });
+ await updateStats();
+
+ // Allow auto save during cooldown
+ Utils.performSmartSave();
+
+ await Utils.sleep(state.cooldown);
+ }
+ }
+ else {
+ if (state.currentCharges < state.cooldownChargeThreshold && !state.stopFlag) {
+ console.log("⚠️ Charges too low, swapping to next account...");
+
+ const accounts = JSON.parse(localStorage.getItem("accounts")) || [];
+ if (accounts.length === 0) {
+ console.warn("❌ No accounts available, stopping painting.");
+ state.stopFlag = true;
+ return;
+ }
+
+ state.accountIndex = (state.accountIndex + 1) % accounts.length;
+ console.log("🔄 Switching to account index:", state.accountIndex);
+
+ const nextToken = accounts[state.accountIndex];
+ console.log("🔑 Next token:", nextToken);
+
+ if (!nextToken) {
+ console.warn("⚠️ Invalid token, skipping...");
+ return;
+ }
+
+ swapAccountTrigger(nextToken);
+
+ let maxRetries = 20;
+ let retryCount = 0;
+ let swapSuccess = false;
+
+ while (!swapSuccess && retryCount < maxRetries) {
+ console.log(`⏳ Waiting for account swap... (Attempt ${retryCount + 1}/${maxRetries})`);
+
+ // Wait for a short period before checking.
+ await new Promise(resolve => setTimeout(resolve, 1000));
+
+ try {
+ await fetchAccount();
+
+ console.log("✅ Account swap confirmed.");
+ swapSuccess = true;
+ } catch (error) {
+ console.warn("❌ Account swap not yet successful. Retrying...", error);
+ retryCount++;
+ }
+ }
+
+ if (swapSuccess) {
+
+ const { charges, cooldown } = await WPlaceService.getCharges();
+ state.currentCharges = Math.floor(charges);
+ state.cooldown = cooldown;
+ Utils.performSmartSave();
+ updateStats();
+ } else {
+ console.error("❌ Failed to swap account after multiple retries. Stopping loop.");
+ state.stopFlag = true;
+ }
+ }
+ }
+
+ // Disable save button again after exiting wait loop (back to normal painting)
+ if (!state.stopFlag) {
+ saveBtn.disabled = true;
+ }
+ if (state.stopFlag) break outerLoop;
+
+ }
+ }
+
+ if (pixelBatch && pixelBatch.pixels.length > 0 && !state.stopFlag) {
+ console.log(`🏁 Sending final batch with ${pixelBatch.pixels.length} pixels`);
+ const success = await sendBatchWithRetry(pixelBatch.pixels, pixelBatch.regionX, pixelBatch.regionY);
+ if (success) {
+ pixelBatch.pixels.forEach((pixel) => {
+ state.paintedPixels++;
+ // Mark pixel as painted in map
+ Utils.markPixelPainted(pixel.x, pixel.y, pixelBatch.regionX, pixelBatch.regionY);
+ })
+ state.currentCharges -= pixelBatch.pixels.length;
+ // Final save with painted map
+ Utils.saveProgress();
+
+ if (CONFIG.PAINTING_SPEED_ENABLED && state.paintingSpeed > 0 && pixelBatch.pixels.length > 0) {
+ const delayPerPixel = 1000 / state.paintingSpeed // ms per pixel
+ const totalDelay = Math.max(100, delayPerPixel * pixelBatch.pixels.length) // minimum 100ms
+ await Utils.sleep(totalDelay)
+ }
+ } else {
+ // If final batch failed after retries, log it
+ console.warn(`⚠️ Final batch failed with ${pixelBatch.pixels.length} pixels after all retries.`);
+ }
+ }
+ } finally {
+ if (window._chargesInterval) clearInterval(window._chargesInterval)
+ window._chargesInterval = null
+ }
+
+ if (state.stopFlag) {
+ updateUI("paintingStopped", "warning")
+ // Save progress when stopped to preserve painted map
+ Utils.saveProgress()
+ } else {
+ updateUI("paintingComplete", "success", { count: state.paintedPixels })
+ state.lastPosition = { x: 0, y: 0 }
+ // Keep painted map until user starts new project
+ // state.paintedMap = null // Commented out to preserve data
+ Utils.saveProgress() // Save final complete state
+ overlayManager.clear();
+ const toggleOverlayBtn = document.getElementById('toggleOverlayBtn');
+ if (toggleOverlayBtn) {
+ toggleOverlayBtn.classList.remove('active');
+ toggleOverlayBtn.disabled = true;
+ }
+ }
+
+ // Log skip statistics
+ console.log(`📊 Pixel Statistics:`);
+ console.log(` Painted: ${state.paintedPixels}`);
+ console.log(` Skipped - Transparent: ${skippedPixels.transparent}`);
+ console.log(` Skipped - White (disabled): ${skippedPixels.white}`);
+ console.log(` Skipped - Already painted: ${skippedPixels.alreadyPainted}`);
+ console.log(` Total processed: ${state.paintedPixels + skippedPixels.transparent + skippedPixels.white + skippedPixels.alreadyPainted}`);
+
+ updateStats()
+ }
+
+ const loadAccountNames = () => {
+ try {
+ return JSON.parse(localStorage.getItem("wplace-account-names")) || {};
+ } catch (e) {
+ return {};
+ }
+ };
+
+ const saveAccountNames = (names) => {
+ try {
+ localStorage.setItem("wplace-account-names", JSON.stringify(names));
+ } catch (e) {
+ console.warn("Could not save account names:", e);
+ }
+ };
+
+ async function editAccountName(token, currentName) {
+ const newName = prompt("Enter a new name for this account:", currentName);
+ if (newName === null) return; // User cancelled
+
+ if (newName.trim() === "") {
+ Utils.showAlert("Account name cannot be empty.", "warning");
+ return;
+ }
+
+ const names = loadAccountNames();
+ names[token] = newName.trim();
+ saveAccountNames(names);
+
+ const accountInfo = state.allAccountsInfo.find(acc => acc.token === token);
+ if (accountInfo) {
+ accountInfo.displayName = newName.trim();
+ }
+
+ renderAccountsList();
+ Utils.showAlert("Account name updated.", "success");
+ }
+
+ async function deleteAccount(token, name) {
+ if (!confirm(`Are you sure you want to delete the account "${name}"?\nThis action cannot be undone.`)) {
+ return;
+ }
+
+ let accounts = JSON.parse(localStorage.getItem("accounts")) || [];
+ const initialCount = accounts.length;
+ accounts = accounts.filter(t => t !== token);
+ if (accounts.length < initialCount) {
+ localStorage.setItem("accounts", JSON.stringify(accounts));
+ }
+
+ const names = loadAccountNames();
+ delete names[token];
+ saveAccountNames(names);
+
+ state.allAccountsInfo = state.allAccountsInfo.filter(acc => acc.token !== token);
+ renderAccountsList();
+ Utils.showAlert(`Account "${name}" has been deleted.`, "success");
+ }
+
+ function renderAccountsList() {
+ const accountsListArea = document.getElementById('accountsListArea');
+ if (!accountsListArea) return;
+
+ accountsListArea.innerHTML = '';
+ if (state.allAccountsInfo.length === 0) {
+ accountsListArea.innerHTML = `No account data. Click to refresh.
`;
+ return;
+ }
+
+ const theme = getCurrentTheme();
+ state.allAccountsInfo.forEach((info, index) => {
+ const item = Utils.createElement('div', {
+ className: `wplace-account-item ${info.isCurrent ? 'is-current' : ''}`
+ });
+
+ const displayName = info.displayName || `Account ${index + 1}`; // this line is fine
+
+ const details = Utils.createElement('div', { className: 'wplace-account-details' });
+ details.appendChild(Utils.createElement('div', { className: 'wplace-account-name', title: displayName }, displayName));
+ // details.appendChild(Utils.createElement('div', { className: 'wplace-account-id' }, `ID: ${info.ID}`));
+
+ let stats;
+ if (info.error) {
+ stats = Utils.createElement('div', { className: 'wplace-account-stats', style: { color: theme.error } }, 'Error');
+ } else {
+ stats = Utils.createElement('div', {
+ className: 'wplace-account-stats',
+ innerHTML: `
+ ${Math.floor(info.Charges || 0)}/${Math.floor(info.Max || 0)}
+ ${Math.floor(info.Droplets || 0)}
+ `
+ });
+ }
+
+ const controls = Utils.createElement('div', { className: 'wplace-account-controls' });
+ const editBtn = Utils.createElement('button', {
+ className: 'wplace-account-btn edit-btn',
+ title: 'Edit Name',
+ innerHTML: ''
+ });
+ editBtn.addEventListener('click', (e) => {
+ e.stopPropagation();
+ editAccountName(info.token, displayName);
+ });
+
+ const deleteBtn = Utils.createElement('button', {
+ className: 'wplace-account-btn delete-btn',
+ title: 'Delete Account',
+ innerHTML: ''
+ });
+ deleteBtn.addEventListener('click', (e) => {
+ e.stopPropagation();
+ deleteAccount(info.token, displayName);
+ });
+
+ controls.appendChild(editBtn);
+ controls.appendChild(deleteBtn);
+
+ item.appendChild(details);
+ item.appendChild(stats);
+ item.appendChild(controls);
+
+ accountsListArea.appendChild(item);
+ });
+ }
+
+ async function fetchAllAccountDetails() {
+ if (state.isFetchingAllAccounts) {
+ Utils.showAlert("Already fetching account details.", "warning");
+ return;
+ }
+ state.isFetchingAllAccounts = true;
+
+ const refreshBtn = document.getElementById('refreshAllAccountsBtn');
+ if (refreshBtn) {
+ refreshBtn.innerHTML = '';
+ refreshBtn.disabled = true;
+ }
+
+ const accountsListArea = document.getElementById('accountsListArea');
+ if (accountsListArea) {
+ accountsListArea.innerHTML = `Initializing...
`;
+ }
+
+ let originalToken = null;
+
+ try {
+ await getAccounts();
+ const accountsTokens = JSON.parse(localStorage.getItem("accounts")) || [];
+ if (accountsTokens.length === 0) {
+ if (accountsListArea) accountsListArea.innerHTML = `No accounts found.
`;
+ return;
+ }
+
+ const { id: originalId } = await WPlaceService.getCharges();
+ state.allAccountsInfo = [];
+ renderAccountsList();
+
+ const accountNames = loadAccountNames();
+ for (let i = 0; i < accountsTokens.length; i++) {
+ const token = accountsTokens[i];
+ swapAccountTrigger(token);
+
+ let retries = 0;
+ let swapped = false;
+ let fetchedInfo = null;
+ while (retries < 5 && !swapped) {
+ await Utils.sleep(1000);
+ try {
+ fetchedInfo = await WPlaceService.fetchCheck();
+ if (fetchedInfo.ID) swapped = true;
+ } catch (e) { retries++; }
+ }
+
+ if (swapped) {
+ await fetchAccount();
+ // await purchase("max_charges");
+ const displayName = accountNames[token] || `Account ${i + 1}`;
+ if (fetchedInfo.ID === originalId) originalToken = token;
+ state.allAccountsInfo.push({ ...fetchedInfo, token, displayName, isCurrent: fetchedInfo.ID === originalId });
+ } else {
+ const displayName = accountNames[token] || `Account ${i + 1}`;
+ state.allAccountsInfo.push({ token, ID: `...${token.slice(-4)}`, displayName, error: 'Failed to fetch' });
+ }
+ renderAccountsList();
+ }
+ } catch (error) {
+ console.error("Error fetching all account details:", error);
+ if (accountsListArea) accountsListArea.innerHTML = `Error loading accounts.
`;
+ } finally {
+ if (originalToken) swapAccountTrigger(originalToken);
+ await Utils.sleep(1000);
+
+ // After switching back, update stats and sync the list
+ const meData = await WPlaceService.getCharges();
+ state.currentCharges = Math.floor(meData.charges);
+ state.cooldown = meData.cooldown;
+ state.maxCharges = Math.floor(meData.max) > 1 ? Math.floor(meData.max) : state.maxCharges;
+ const currentAccountInList = state.allAccountsInfo.find(acc => acc.ID === meData.id);
+ if (currentAccountInList) {
+ currentAccountInList.Charges = state.currentCharges;
+ currentAccountInList.Max = state.maxCharges;
+ currentAccountInList.Droplets = meData.droplets;
+ }
+ await updateStats(); // This will re-render the main stats
+ renderAccountsList(); // This will re-render the list with synced data
+
+ state.isFetchingAllAccounts = false;
+ if (refreshBtn) {
+ refreshBtn.innerHTML = '';
+ refreshBtn.disabled = false;
+ }
+ }
+ }
+
+ // Helper function to calculate batch size based on mode
+ function calculateBatchSize() {
+ let targetBatchSize;
+
+ if (state.batchMode === 'random') {
+ // Generate random batch size within the specified range
+ const min = Math.max(1, state.randomBatchMin);
+ const max = Math.max(min, state.randomBatchMax);
+ targetBatchSize = Math.floor(Math.random() * (max - min + 1)) + min;
+ console.log(`🎲 Random batch size generated: ${targetBatchSize} (range: ${min}-${max})`);
+ } else {
+ // Normal mode - use the fixed paintingSpeed value
+ targetBatchSize = state.paintingSpeed;
+ }
+
+ // Always limit by available charges
+ const maxAllowed = Math.floor(state.currentCharges);
+ const finalBatchSize = Math.min(targetBatchSize, maxAllowed);
+
+ return finalBatchSize;
+ }
+
+ // Helper function to retry batch until success with exponential backoff
+ async function sendBatchWithRetry(pixels, regionX, regionY, maxRetries = MAX_BATCH_RETRIES) {
+ let attempt = 0;
+ while (attempt < maxRetries && !state.stopFlag) {
+ attempt++;
+ console.log(`🔄 Attempting to send batch (attempt ${attempt}/${maxRetries}) for region ${regionX},${regionY} with ${pixels.length} pixels`);
+
+ const result = await sendPixelBatch(pixels, regionX, regionY);
+
+ if (result === true) {
+ console.log(`✅ Batch succeeded on attempt ${attempt}`);
+ return true;
+ } else if (result === "token_error") {
+ console.log(`🔑 Token error on attempt ${attempt}, regenerating...`);
+ updateUI("captchaSolving", "warning");
+ try {
+ await handleCaptcha();
+ // Don't count token regeneration as a failed attempt
+ attempt--;
+ continue;
+ } catch (e) {
+ console.error(`❌ Token regeneration failed on attempt ${attempt}:`, e);
+ updateUI("captchaFailed", "error");
+ // Wait longer before retrying after token failure
+ await Utils.sleep(5000);
+ }
+ } else {
+ console.warn(`⚠️ Batch failed on attempt ${attempt}, retrying...`);
+ // Exponential backoff with jitter
+ const baseDelay = Math.min(1000 * Math.pow(2, attempt - 1), 30000); // Max 30s
+ const jitter = Math.random() * 1000; // Add up to 1s random delay
+ await Utils.sleep(baseDelay + jitter);
+ }
+ }
+
+ if (attempt >= maxRetries) {
+ console.error(`❌ Batch failed after ${maxRetries} attempts (MAX_BATCH_RETRIES=${MAX_BATCH_RETRIES}). This will stop painting to prevent infinite loops.`);
+ updateUI("paintingError", "error");
+ return false;
+ }
+
+ return false;
+ }
+
+ async function sendPixelBatch(pixelBatch, regionX, regionY) {
+ let token = turnstileToken;
+
+ // Generate new token if we don't have one
+ if (!token) {
+ try {
+ console.log("🔑 Generating Turnstile token for pixel batch...");
+ token = await handleCaptcha();
+ turnstileToken = token; // Store for potential reuse
+ } catch (error) {
+ console.error("❌ Failed to generate Turnstile token:", error);
+ tokenPromise = new Promise((resolve) => { _resolveToken = resolve });
+ return "token_error";
+ }
+ }
+
+ const coords = new Array(pixelBatch.length * 2)
+ const colors = new Array(pixelBatch.length)
+ for (let i = 0; i < pixelBatch.length; i++) {
+ const pixel = pixelBatch[i]
+ coords[i * 2] = pixel.x
+ coords[i * 2 + 1] = pixel.y
+ colors[i] = pixel.color
+ }
+
+ try {
+ const payload = { coords, colors, t: token, fp: randStr(10) };
+ var wasmtoken = await createWasmToken(regionX, regionY, payload);
+ const res = await fetch(`https://backend.wplace.live/s0/pixel/${regionX}/${regionY}`, {
+ method: 'POST',
+ headers: { 'Content-Type': 'text/plain;charset=UTF-8', "x-pawtect-token":wasmtoken },
+ credentials: 'include',
+ body: JSON.stringify(payload),
+ });
+
+ if (res.status === 403) {
+ let data = null
+ try { data = await res.json() } catch (_) { }
+ console.error("❌ 403 Forbidden. Turnstile token might be invalid or expired.")
+
+ // Try to generate a new token and retry once
+ try {
+ console.log("🔄 Regenerating Turnstile token after 403...");
+ token = await handleCaptcha();
+ turnstileToken = token;
+
+ // Retry the request with new token
+ const retryPayload = { coords, colors, t: token, fp: randStr(10) };
+ var wasmtoken = await createWasmToken(regionX, regionY, retryPayload);
+ const retryRes = await fetch(
+ `https://backend.wplace.live/s0/pixel/${regionX}/${regionY}`,
+ {
+ method: 'POST',
+ headers: { 'Content-Type': 'text/plain;charset=UTF-8', "x-pawtect-token":wasmtoken },
+ credentials: 'include',
+ body: JSON.stringify(retryPayload),
+ }
+ );
+
+ if (retryRes.status === 403) {
+ turnstileToken = null;
+ tokenPromise = new Promise((resolve) => { _resolveToken = resolve });
+ return "token_error";
+ }
+
+ const retryData = await retryRes.json();
+ return retryData?.painted === pixelBatch.length;
+
+ } catch (retryError) {
+ console.error("❌ Token regeneration failed:", retryError);
+ turnstileToken = null;
+ tokenPromise = new Promise((resolve) => { _resolveToken = resolve });
+ return "token_error";
+ }
+ }
+
+ const data = await res.json()
+ return data?.painted === pixelBatch.length
+ } catch (e) {
+ console.error("Batch paint request failed:", e)
+ return false
+ }
+ }
+
+ function saveBotSettings() {
+ try {
+ const settings = {
+ paintingSpeed: state.paintingSpeed,
+ paintingSpeedEnabled: document.getElementById('enableSpeedToggle')?.checked,
+ batchMode: state.batchMode, // "normal" or "random"
+ randomBatchMin: state.randomBatchMin,
+ randomBatchMax: state.randomBatchMax,
+ cooldownChargeThreshold: state.cooldownChargeThreshold,
+ autoSwap: CONFIG.autoSwap,
+ tokenSource: state.tokenSource, // "generator", "hybrid", or "manual"
+ minimized: state.minimized,
+ overlayOpacity: state.overlayOpacity,
+ blueMarbleEnabled: document.getElementById('enableBlueMarbleToggle')?.checked,
+ ditheringEnabled: state.ditheringEnabled,
+ colorMatchingAlgorithm: state.colorMatchingAlgorithm,
+ enableChromaPenalty: state.enableChromaPenalty,
+ chromaPenaltyWeight: state.chromaPenaltyWeight,
+ customTransparencyThreshold: state.customTransparencyThreshold,
+ customWhiteThreshold: state.customWhiteThreshold,
+ paintWhitePixels: state.paintWhitePixels,
+ resizeSettings: state.resizeSettings,
+ originalImage: state.originalImage,
+ // Save ignore mask (as base64) with its dimensions
+ resizeIgnoreMask: (state.resizeIgnoreMask && state.resizeSettings && state.resizeSettings.width * state.resizeSettings.height === state.resizeIgnoreMask.length)
+ ? { w: state.resizeSettings.width, h: state.resizeSettings.height, data: btoa(String.fromCharCode(...state.resizeIgnoreMask)) }
+ : null,
+ // Notifications
+ notificationsEnabled: state.notificationsEnabled,
+ notifyOnChargesReached: state.notifyOnChargesReached,
+ notifyOnlyWhenUnfocused: state.notifyOnlyWhenUnfocused,
+ notificationIntervalMinutes: state.notificationIntervalMinutes,
+ };
+ CONFIG.PAINTING_SPEED_ENABLED = settings.paintingSpeedEnabled;
+ // AUTO_CAPTCHA_ENABLED is always true - no need to save/load
+
+ localStorage.setItem("wplace-bot-settings", JSON.stringify(settings));
+ } catch (e) {
+ console.warn("Could not save bot settings:", e);
+ }
+ }
+
+ function loadBotSettings() {
+ try {
+ const saved = localStorage.getItem("wplace-bot-settings");
+ if (!saved) return;
+ const settings = JSON.parse(saved);
+
+ state.paintingSpeed = settings.paintingSpeed || CONFIG.PAINTING_SPEED.DEFAULT;
+ state.batchMode = settings.batchMode || CONFIG.BATCH_MODE; // Default to "normal"
+ state.randomBatchMin = settings.randomBatchMin || CONFIG.RANDOM_BATCH_RANGE.MIN;
+ state.randomBatchMax = settings.randomBatchMax || CONFIG.RANDOM_BATCH_RANGE.MAX;
+ state.cooldownChargeThreshold = settings.cooldownChargeThreshold || CONFIG.COOLDOWN_CHARGE_THRESHOLD;
+ CONFIG.autoSwap = settings.autoSwap ?? false;
+ state.tokenSource = settings.tokenSource || CONFIG.TOKEN_SOURCE; // Default to "generator"
+ state.minimized = settings.minimized ?? false;
+ CONFIG.PAINTING_SPEED_ENABLED = settings.paintingSpeedEnabled ?? false;
+ CONFIG.AUTO_CAPTCHA_ENABLED = settings.autoCaptchaEnabled ?? false;
+ state.overlayOpacity = settings.overlayOpacity ?? CONFIG.OVERLAY.OPACITY_DEFAULT;
+ state.blueMarbleEnabled = settings.blueMarbleEnabled ?? CONFIG.OVERLAY.BLUE_MARBLE_DEFAULT;
+ state.ditheringEnabled = settings.ditheringEnabled ?? false;
+ state.colorMatchingAlgorithm = settings.colorMatchingAlgorithm || 'lab';
+ state.enableChromaPenalty = settings.enableChromaPenalty ?? true;
+ state.chromaPenaltyWeight = settings.chromaPenaltyWeight ?? 0.15;
+ state.customTransparencyThreshold = settings.customTransparencyThreshold ?? CONFIG.TRANSPARENCY_THRESHOLD;
+ state.customWhiteThreshold = settings.customWhiteThreshold ?? CONFIG.WHITE_THRESHOLD;
+ state.paintWhitePixels = settings.paintWhitePixels ?? true;
+ state.resizeSettings = settings.resizeSettings ?? null;
+ state.originalImage = settings.originalImage ?? null;
+ // Notifications
+ state.notificationsEnabled = settings.notificationsEnabled ?? CONFIG.NOTIFICATIONS.ENABLED;
+ state.notifyOnChargesReached = settings.notifyOnChargesReached ?? CONFIG.NOTIFICATIONS.ON_CHARGES_REACHED;
+ state.notifyOnlyWhenUnfocused = settings.notifyOnlyWhenUnfocused ?? CONFIG.NOTIFICATIONS.ONLY_WHEN_UNFOCUSED;
+ state.notificationIntervalMinutes = settings.notificationIntervalMinutes ?? CONFIG.NOTIFICATIONS.REPEAT_MINUTES;
+ // Restore ignore mask if dims match current resizeSettings
+ if (settings.resizeIgnoreMask && settings.resizeIgnoreMask.data && state.resizeSettings && settings.resizeIgnoreMask.w === state.resizeSettings.width && settings.resizeIgnoreMask.h === state.resizeSettings.height) {
+ try {
+ const bin = atob(settings.resizeIgnoreMask.data);
+ const arr = new Uint8Array(bin.length);
+ for (let i = 0; i < bin.length; i++) arr[i] = bin.charCodeAt(i);
+ state.resizeIgnoreMask = arr;
+ } catch { state.resizeIgnoreMask = null; }
+ } else {
+ state.resizeIgnoreMask = null;
+ }
+
+ const speedSlider = document.getElementById('speedSlider');
+ if (speedSlider) speedSlider.value = state.paintingSpeed;
+ const speedValue = document.getElementById('speedValue');
+ if (speedValue) speedValue.textContent = `${state.paintingSpeed} (batch size)`;
+
+ const enableSpeedToggle = document.getElementById('enableSpeedToggle');
+ if (enableSpeedToggle) enableSpeedToggle.checked = CONFIG.PAINTING_SPEED_ENABLED;
+
+ // Batch mode UI initialization
+ const batchModeSelect = document.getElementById('batchModeSelect');
+ if (batchModeSelect) batchModeSelect.value = state.batchMode;
+
+ const normalBatchControls = document.getElementById('normalBatchControls');
+ const randomBatchControls = document.getElementById('randomBatchControls');
+
+ // Show/hide appropriate controls based on batch mode
+ if (normalBatchControls && randomBatchControls) {
+ if (state.batchMode === 'random') {
+ normalBatchControls.style.display = 'none';
+ randomBatchControls.style.display = 'block';
+ } else {
+ normalBatchControls.style.display = 'block';
+ randomBatchControls.style.display = 'none';
+ }
+ }
+
+ const randomBatchMin = document.getElementById('randomBatchMin');
+ if (randomBatchMin) randomBatchMin.value = state.randomBatchMin;
+
+ const randomBatchMax = document.getElementById('randomBatchMax');
+ if (randomBatchMax) randomBatchMax.value = state.randomBatchMax;
+
+ // AUTO_CAPTCHA_ENABLED is always true - no toggle to set
+
+ const cooldownSlider = document.getElementById('cooldownSlider');
+ if (cooldownSlider) cooldownSlider.value = state.cooldownChargeThreshold;
+ const cooldownValue = document.getElementById('cooldownValue');
+ if (cooldownValue) cooldownValue.textContent = state.cooldownChargeThreshold;
+
+ const autoSwapToggle = document.getElementById('autoSwapToggle');
+ if (autoSwapToggle) autoSwapToggle.checked = CONFIG.autoSwap;
+
+ const overlayOpacitySlider = document.getElementById('overlayOpacitySlider');
+ if (overlayOpacitySlider) overlayOpacitySlider.value = state.overlayOpacity;
+ const overlayOpacityValue = document.getElementById('overlayOpacityValue');
+ if (overlayOpacityValue) overlayOpacityValue.textContent = `${Math.round(state.overlayOpacity * 100)}%`;
+ const enableBlueMarbleToggle = document.getElementById('enableBlueMarbleToggle');
+ if (enableBlueMarbleToggle) enableBlueMarbleToggle.checked = state.blueMarbleEnabled;
+
+ const tokenSourceSelect = document.getElementById('tokenSourceSelect');
+ if (tokenSourceSelect) tokenSourceSelect.value = state.tokenSource;
+
+ const colorAlgorithmSelect = document.getElementById('colorAlgorithmSelect');
+ if (colorAlgorithmSelect) colorAlgorithmSelect.value = state.colorMatchingAlgorithm;
+ const enableChromaPenaltyToggle = document.getElementById('enableChromaPenaltyToggle');
+ if (enableChromaPenaltyToggle) enableChromaPenaltyToggle.checked = state.enableChromaPenalty;
+ const chromaPenaltyWeightSlider = document.getElementById('chromaPenaltyWeightSlider');
+ if (chromaPenaltyWeightSlider) chromaPenaltyWeightSlider.value = state.chromaPenaltyWeight;
+ const chromaWeightValue = document.getElementById('chromaWeightValue');
+ if (chromaWeightValue) chromaWeightValue.textContent = state.chromaPenaltyWeight;
+ const transparencyThresholdInput = document.getElementById('transparencyThresholdInput');
+ if (transparencyThresholdInput) transparencyThresholdInput.value = state.customTransparencyThreshold;
+ const whiteThresholdInput = document.getElementById('whiteThresholdInput');
+ if (whiteThresholdInput) whiteThresholdInput.value = state.customWhiteThreshold;
+ // Notifications UI
+ const notifEnabledToggle = document.getElementById('notifEnabledToggle');
+ if (notifEnabledToggle) notifEnabledToggle.checked = state.notificationsEnabled;
+ const notifOnChargesToggle = document.getElementById('notifOnChargesToggle');
+ if (notifOnChargesToggle) notifOnChargesToggle.checked = state.notifyOnChargesReached;
+ const notifOnlyUnfocusedToggle = document.getElementById('notifOnlyUnfocusedToggle');
+ if (notifOnlyUnfocusedToggle) notifOnlyUnfocusedToggle.checked = state.notifyOnlyWhenUnfocused;
+ const notifIntervalInput = document.getElementById('notifIntervalInput');
+ if (notifIntervalInput) notifIntervalInput.value = state.notificationIntervalMinutes;
+ NotificationManager.resetEdgeTracking();
+
+ } catch (e) {
+ console.warn("Could not load bot settings:", e);
+ }
+ }
+
+ // Initialize Turnstile generator integration
+ console.log("🚀 WPlace Auto-Image with Turnstile Token Generator loaded");
+ console.log("🔑 Turnstile token generator: ALWAYS ENABLED (Background mode)");
+ console.log("🎯 Manual pixel captcha solving: Available as fallback/alternative");
+ console.log("📱 Turnstile widgets: DISABLED - pure background token generation only!");
+
+ // Function to enable file operations after initial startup setup is complete
+ function enableFileOperations() {
+ state.initialSetupComplete = true;
+
+ const loadBtn = document.querySelector("#loadBtn");
+ const loadFromFileBtn = document.querySelector("#loadFromFileBtn");
+ const uploadBtn = document.querySelector("#uploadBtn");
+
+ if (loadBtn) {
+ loadBtn.disabled = false;
+ loadBtn.title = "";
+ // Add a subtle animation to indicate the button is now available
+ loadBtn.style.animation = "pulse 0.6s ease-in-out";
+ setTimeout(() => {
+ if (loadBtn) loadBtn.style.animation = "";
+ }, 600);
+ console.log("✅ Load Progress button enabled after initial setup");
+ }
+
+ if (loadFromFileBtn) {
+ loadFromFileBtn.disabled = false;
+ loadFromFileBtn.title = "";
+ // Add a subtle animation to indicate the button is now available
+ loadFromFileBtn.style.animation = "pulse 0.6s ease-in-out";
+ setTimeout(() => {
+ if (loadFromFileBtn) loadFromFileBtn.style.animation = "";
+ }, 600);
+ console.log("✅ Load from File button enabled after initial setup");
+ }
+
+ if (uploadBtn) {
+ uploadBtn.disabled = false;
+ uploadBtn.title = "";
+ // Add a subtle animation to indicate the button is now available
+ uploadBtn.style.animation = "pulse 0.6s ease-in-out";
+ setTimeout(() => {
+ if (uploadBtn) uploadBtn.style.animation = "";
+ }, 600);
+ console.log("✅ Upload Image button enabled after initial setup");
+ }
+
+ // Show a notification that file operations are now available
+ Utils.showAlert("📂 File operations (Load/Upload) are now available!", "success");
+ }
+
+ // Optimized token initialization with better timing and error handling
+ async function initializeTokenGenerator() {
+ // Skip if already have valid token
+ if (isTokenValid()) {
+ console.log("✅ Valid token already available, skipping initialization");
+ updateUI("tokenReady", "success");
+ enableFileOperations(); // Enable file operations since initial setup is complete
+ return;
+ }
+
+ try {
+ console.log("🔧 Initializing Turnstile token generator...");
+ updateUI("initializingToken", "default");
+
+ // Pre-load Turnstile script first to avoid delays later
+ await Utils.loadTurnstile();
+
+ const token = await handleCaptchaWithRetry();
+ if (token) {
+ setTurnstileToken(token);
+ console.log("✅ Startup token generated successfully");
+ updateUI("tokenReady", "success");
+ Utils.showAlert("🔑 Token generator ready!", "success");
+ enableFileOperations(); // Enable file operations since initial setup is complete
+ } else {
+ console.warn("⚠️ Startup token generation failed, will retry when needed");
+ updateUI("tokenRetryLater", "warning");
+ // Still enable file operations even if initial token generation fails
+ // Users can load progress and use manual/hybrid modes
+ enableFileOperations();
+ }
+ } catch (error) {
+ console.warn("⚠️ Startup token generation failed:", error);
+ updateUI("tokenRetryLater", "warning");
+ // Still enable file operations even if initial setup fails
+ // Users can load progress and use manual/hybrid modes
+ enableFileOperations();
+ // Don't show error alert for initialization failures, just log them
+ }
+ }
+
+ // Load theme preference immediately on startup before creating UI
+ loadThemePreference();
+ async function createWasmToken(regionX,regionY, payload) {
+ try {
+ // Load the Pawtect module and WASM
+
+ async function getModURL(update) {
+ if (update === true || localStorage.getItem('pawtect-wasm-location') === null) {
+ const rootHTML = (await (await fetch(window.location.href)).text()).split('\n')
+
+ for (const line of rootHTML) {
+ if (line.includes(' r.ok ? r.json() : null);
+ if (me?.id)
+ {
+ mod.i(me.id);
+ console.log('✅ user ID set:', me.id);
+ }
+ } catch { }
+ } catch (userIdError) {
+ console.log('⚠️ Error setting user ID:', userIdError.message);
+ }
+ try {
+ const testUrl = `https://backend.wplace.live/s0/pixel/${regionX}/${regionY}`;
+ if (mod.r) {
+ mod.r(testUrl);
+ console.log('✅ Request URL set:', testUrl);
+ } else {
+ console.log('⚠️ request_url function (mod.r) not available');
+ }
+ } catch (urlError) {
+ console.log('⚠️ Error setting request URL:', urlError.message);
+ }
+
+ // Create test payload
+
+ console.log('📝 payload:', payload);
+
+ // Encode payload
+ const enc = new TextEncoder();
+ const dec = new TextDecoder();
+ const bodyStr = JSON.stringify(payload);
+ const bytes = enc.encode(bodyStr);
+ console.log('📏 Payload size:', bytes.length, 'bytes');
+ console.log('📄 Payload string:', bodyStr);
+
+ // Allocate WASM memory with validation
+ let inPtr;
+ try {
+ if (!wasm.__wbindgen_malloc) {
+ console.error('❌ __wbindgen_malloc function not found');
+ return null;
+ }
+
+ inPtr = wasm.__wbindgen_malloc(bytes.length, 1);
+ console.log('✅ WASM memory allocated, pointer:', inPtr);
+
+ // Copy data to WASM memory
+ const wasmBuffer = new Uint8Array(wasm.memory.buffer, inPtr, bytes.length);
+ wasmBuffer.set(bytes);
+ console.log('✅ Data copied to WASM memory');
+ } catch (memError) {
+ console.error('❌ Memory allocation error:', memError);
+ return null;
+ }
+
+ // Call the WASM function
+ console.log('🚀 Calling get_pawtected_endpoint_payload...');
+ let outPtr, outLen, token;
+ try {
+ const result = wasm.get_pawtected_endpoint_payload(inPtr, bytes.length);
+ console.log('✅ Function called, result type:', typeof result, result);
+
+ if (Array.isArray(result) && result.length === 2) {
+ [outPtr, outLen] = result;
+ console.log('✅ Got output pointer:', outPtr, 'length:', outLen);
+
+ // Decode the result
+ const outputBuffer = new Uint8Array(wasm.memory.buffer, outPtr, outLen);
+ token = dec.decode(outputBuffer);
+ console.log('✅ Token decoded successfully');
+ } else {
+ console.error('❌ Unexpected function result format:', result);
+ return null;
+ }
+ } catch (funcError) {
+ console.error('❌ Function call error:', funcError);
+ console.error('Stack trace:', funcError.stack);
+ return null;
+ }
+
+ // Cleanup memory
+ try {
+ if (wasm.__wbindgen_free && outPtr && outLen) {
+ wasm.__wbindgen_free(outPtr, outLen, 1);
+ console.log('✅ Output memory freed');
+ }
+ if (wasm.__wbindgen_free && inPtr) {
+ wasm.__wbindgen_free(inPtr, bytes.length, 1);
+ console.log('✅ Input memory freed');
+ }
+ } catch (cleanupError) {
+ console.log('⚠️ Cleanup warning:', cleanupError.message);
+ }
+
+ // Display results
+ console.log('');
+ console.log('🎉 SUCCESS!');
+ console.log('📊 Results:');
+ console.log(' Input coords: [1245984, 1088]');
+ console.log(' Token length:', token?.length || 0);
+ console.log(' Token preview:', token?.substring(0, 50) + '...');
+ console.log('');
+ console.log('🔑 Full token:');
+ console.log(token);
+ return token;
+ } catch (error) {
+ console.error('❌ Failed to generate fp parameter:', error);
+ return null;
+ }
+ return null;
+ }
+
+ async function purchase(type) {
+ // loadThemePreference()
+ let id;
+ let chargeMultiplier;
+ if (type === "max_charges") {
+ id = 70;
+ chargeMultiplier = 5;
+ } else if (type === "paint_charges") {
+ id = 80;
+ chargeMultiplier = 30;
+ } else {
+ console.error("Error: Invalid purchase type provided.");
+ return;
+ }
+ const { droplets } = await WPlaceService.getCharges();
+ console.log("There are currently : ", droplets, "droplets.");
+ try {
+ const amounts = Math.floor(droplets / 500);
+ if (amounts < 1) {
+ console.log("Not enough droplets to purchase.");
+ return;
+ }
+ const payload = {
+ "product": {
+ "id": id,
+ "amount": amounts
+ }
+ };
+ const res = fetch("https://backend.wplace.live/purchase", {
+ method: "POST",
+ headers: {
+ "Content-Type": "text/plain;charset=UTF-8"
+ },
+ body: JSON.stringify(payload),
+ credentials: "include"
+ });
+ console.log("Fetch POST return :", res);
+ const { droplets: newDroplets } = await WPlaceService.getCharges();
+ if (droplets != newDroplets) {
+ console.log("Successfully bought", amounts * chargeMultiplier, type.replace('_', ' '), ".");
+ }
+ else {
+ console.log("Failed to buy charges");
+ }
+ } catch (e) {
+ console.error("An error occurred during the purchase:", e);
+ }
+ }
+ function swapAccountTrigger(token) {
+ localStorage.removeItem("lp");
+ if (!token) return;
+ console.log("Sending token to extension...");
+ window.postMessage({
+ source: 'my-userscript',
+ type: 'setCookie',
+ value: token
+ }, '*');
+ }
+ async function getAccounts() {
+ return new Promise((resolve, reject) => {
+ console.log("Requesting accounts from extension...");
+ // Ask extension for accounts
+ window.postMessage({
+ source: "my-userscript",
+ type: "getAccounts"
+ }, "*");
+ function handler(event) {
+ if (event.source !== window) return;
+ if (event.data.source !== "extension") return;
+ if (event.data.type === "accountsData") {
+ window.removeEventListener("message", handler);
+ // Save to localStorage
+ try {
+ localStorage.setItem("accounts", JSON.stringify(event.data.accounts));
+ console.log("✅ Accounts saved to localStorage:", event.data.accounts);
+ } catch (e) {
+ console.error("❌ Failed to save accounts:", e);
+ }
+ resolve(event.data.accounts);
+ }
+ }
+ window.addEventListener("message", handler);
+ });
+ }
+ async function fetchAccount() {
+ const { ID, Charges, Max, Droplets } = await WPlaceService.fetchCheck();
+ console.log("User's ID :", ID);
+ console.log("User's Charges :", Charges, "/", Max);
+ console.log("User's Droplets :", Droplets);
+ }
+ createUI().then(() => {
+ // Generate token automatically after UI is ready
+ setTimeout(initializeTokenGenerator, 1000);
+
+ // Attach advanced color matching listeners (resize dialog)
+ const advancedInit = () => {
+ const chromaSlider = document.getElementById('chromaPenaltyWeightSlider');
+ const chromaValue = document.getElementById('chromaWeightValue');
+ const resetBtn = document.getElementById('resetAdvancedColorBtn');
+ const algoSelect = document.getElementById('colorAlgorithmSelect');
+ const chromaToggle = document.getElementById('enableChromaPenaltyToggle');
+ const transInput = document.getElementById('transparencyThresholdInput');
+ const whiteInput = document.getElementById('whiteThresholdInput');
+ const ditherToggle = document.getElementById('enableDitheringToggle');
+ if (algoSelect) algoSelect.addEventListener('change', e => { state.colorMatchingAlgorithm = e.target.value; saveBotSettings(); _updateResizePreview(); });
+ if (chromaToggle) chromaToggle.addEventListener('change', e => { state.enableChromaPenalty = e.target.checked; saveBotSettings(); _updateResizePreview(); });
+ if (chromaSlider && chromaValue) chromaSlider.addEventListener('input', e => { state.chromaPenaltyWeight = parseFloat(e.target.value) || 0.15; chromaValue.textContent = state.chromaPenaltyWeight.toFixed(2); saveBotSettings(); _updateResizePreview(); });
+ if (transInput) transInput.addEventListener('change', e => { const v = parseInt(e.target.value, 10); if (!isNaN(v) && v >= 0 && v <= 255) { state.customTransparencyThreshold = v; CONFIG.TRANSPARENCY_THRESHOLD = v; saveBotSettings(); _updateResizePreview(); } });
+ if (whiteInput) whiteInput.addEventListener('change', e => { const v = parseInt(e.target.value, 10); if (!isNaN(v) && v >= 200 && v <= 255) { state.customWhiteThreshold = v; CONFIG.WHITE_THRESHOLD = v; saveBotSettings(); _updateResizePreview(); } });
+ if (ditherToggle) ditherToggle.addEventListener('change', e => { state.ditheringEnabled = e.target.checked; saveBotSettings(); _updateResizePreview(); });
+ if (resetBtn) resetBtn.addEventListener('click', () => {
+ state.colorMatchingAlgorithm = 'lab'; state.enableChromaPenalty = true; state.chromaPenaltyWeight = 0.15; state.customTransparencyThreshold = CONFIG.TRANSPARENCY_THRESHOLD = 100; state.customWhiteThreshold = CONFIG.WHITE_THRESHOLD = 250; saveBotSettings(); const a = document.getElementById('colorAlgorithmSelect'); if (a) a.value = 'lab'; const ct = document.getElementById('enableChromaPenaltyToggle'); if (ct) ct.checked = true; if (chromaSlider) chromaSlider.value = 0.15; if (chromaValue) chromaValue.textContent = '0.15'; if (transInput) transInput.value = 100; if (whiteInput) whiteInput.value = 250; _updateResizePreview(); Utils.showAlert('Advanced color settings reset.', 'success');
+ });
+ };
+ // Delay to ensure resize UI built
+ setTimeout(advancedInit, 500);
+
+ // Add cleanup on page unload
+ window.addEventListener('beforeunload', () => {
+ Utils.cleanupTurnstile();
+ });
+ })
+})()
\ No newline at end of file
diff --git a/Extension/scripts/Auto-Image.js b/Extension/scripts/Auto-Image.js
index a379d53..7cef737 100644
--- a/Extension/scripts/Auto-Image.js
+++ b/Extension/scripts/Auto-Image.js
@@ -38,7 +38,7 @@ localStorage.removeItem("lp");
REPEAT_MINUTES: 5, // repeat reminder while threshold condition holds
},
OVERLAY: {
- OPACITY_DEFAULT: 0.2,
+ OPACITY_DEFAULT: 0.6,
BLUE_MARBLE_DEFAULT: false,
ditheringEnabled: false,
}, // --- START: Color data from colour-converter.js ---
@@ -229,6 +229,7 @@ localStorage.removeItem("lp");
COORDINATE_SNAKE: true,
COORDINATE_BLOCK_WIDTH: 6,
COORDINATE_BLOCK_HEIGHT: 2,
+ autoSwap: true,
};
const getCurrentTheme = () => CONFIG.THEMES[CONFIG.currentTheme];
@@ -251,7 +252,7 @@ localStorage.removeItem("lp");
const theme = getCurrentTheme();
console.group('%c🎨 Applying Theme in Auto-Image Script', 'color: #8b5cf6; font-weight: bold;');
console.log(`%c🎯 Target theme: ${CONFIG.currentTheme}`, 'color: #8b5cf6;');
-
+
// Toggle theme class on documentElement so CSS vars cascade to our UI
document.documentElement.classList.remove(
'wplace-theme-classic',
@@ -262,7 +263,7 @@ localStorage.removeItem("lp");
let themeClass = 'wplace-theme-classic'; // default
let themeFileName = 'classic'; // corresponding file name
-
+
if (CONFIG.currentTheme === 'Neon Retro') {
themeClass = 'wplace-theme-neon';
themeFileName = 'neon';
@@ -320,7 +321,7 @@ localStorage.removeItem("lp");
setVar('--wplace-border-width', '' + (theme.borderWidth || '1px'));
setVar('--wplace-backdrop', '' + (theme.backdropFilter || 'blur(10px)'));
setVar('--wplace-border-color', 'rgba(255,255,255,0.1)');
-
+
console.log(`%c🎨 Theme application complete`, 'color: #8b5cf6; font-weight: bold;');
console.groupEnd();
}
@@ -377,11 +378,11 @@ localStorage.removeItem("lp");
// First try: Check if extension has loaded local resources
if (window.AUTOBOT_LANGUAGES && Object.keys(window.AUTOBOT_LANGUAGES).length > 0) {
console.log(`%c🔍 Checking extension local resources...`, 'color: #06b6d4;');
-
+
const langFile = language + '.json';
if (window.AUTOBOT_LANGUAGES[langFile]) {
const translations = window.AUTOBOT_LANGUAGES[langFile];
-
+
// Validate that translations is an object with keys
if (
typeof translations === 'object' &&
@@ -721,6 +722,7 @@ localStorage.removeItem("lp");
_lastSaveTime: 0,
_saveInProgress: false,
paintedMap: null,
+ accountIndex: -1,
};
let _updateResizePreview = () => { };
@@ -2976,34 +2978,49 @@ localStorage.removeItem("lp");
},
async getCharges() {
- const defaultResult = {
- charges: 0,
- max: 1,
- cooldown: CONFIG.COOLDOWN_DEFAULT,
- };
-
try {
- const res = await fetch('https://backend.wplace.live/me', {
- credentials: 'include',
- });
-
- if (!res.ok) {
- console.error(`Failed to get charges: HTTP ${res.status}`);
- return defaultResult;
- }
-
- const data = await res.json();
-
+ const res = await fetch("https://backend.wplace.live/me", {
+ credentials: "include",
+ })
+ const data = await res.json()
return {
- charges: data.charges?.count ?? 0,
- max: data.charges?.max ?? 1,
- cooldown: data.charges?.cooldownMs ?? CONFIG.COOLDOWN_DEFAULT,
- };
+ id: data.id,
+ charges: data.charges?.count || 0,
+ max: data.charges?.max || 1,
+ cooldown: data.charges?.next || CONFIG.COOLDOWN_DEFAULT,
+ droplets: data.droplets || 0,
+ }
} catch (e) {
- console.error('Failed to get charges:', e);
- return defaultResult;
+ console.error("Failed to get charges:", e)
+ return {
+ id: null,
+ charges: 0,
+ max: 1,
+ cooldown: CONFIG.COOLDOWN_DEFAULT,
+ droplets: 0,
+ }
}
},
+
+ async fetchCheck() {
+ try {
+ const res = await fetch("https://backend.wplace.live/me", {
+ credentials: "include",
+ })
+ const data = await res.json()
+ return {
+ ID: data.id,
+ Charges: data.charges.count,
+ Max: data.charges.max,
+ Droplets: data.droplets
+ }
+ } catch (e) {
+ console.error("Failed to get ID:", e)
+ return {
+
+ }
+ }
+ }
};
// Desktop Notification Manager
@@ -3515,13 +3532,13 @@ localStorage.removeItem("lp");
// Load auto-image-styles.css - prioritize extension local resources
console.group('%c🎨 Loading Auto-Image Styles', 'color: #8b5cf6; font-weight: bold;');
-
+
let stylesLoaded = false;
-
+
// First try: Check if extension has loaded local CSS resources
if (window.AUTOBOT_THEMES && window.AUTOBOT_THEMES['auto-image-styles.css']) {
console.log('%c🔍 Found auto-image-styles.css in extension local resources!', 'color: #10b981; font-weight: bold;');
-
+
// Check if it's already injected by the extension
const existingStyle = document.getElementById('autobot-auto-image-styles');
if (existingStyle) {
@@ -3536,7 +3553,7 @@ localStorage.removeItem("lp");
styleElement.setAttribute('data-wplace-theme', 'true');
styleElement.textContent = window.AUTOBOT_THEMES['auto-image-styles.css'];
document.head.appendChild(styleElement);
-
+
console.log('%c✅ Injected auto-image-styles.css from extension local resources', 'color: #10b981; font-weight: bold;');
console.log(' 📍 Source: Extension local file');
console.log(' 📏 Size: ' + window.AUTOBOT_THEMES['auto-image-styles.css'].length + ' characters');
@@ -3550,7 +3567,7 @@ localStorage.removeItem("lp");
console.log(' 📋 Available themes:', Object.keys(window.AUTOBOT_THEMES));
}
}
-
+
// Fallback: Load from CDN if local resources not available
if (!stylesLoaded) {
console.log('%c🌐 Falling back to CDN loading...', 'color: #8b5cf6;');
@@ -3563,13 +3580,13 @@ localStorage.removeItem("lp");
console.log(' 🌐 URL: https://wplace-autobot.github.io/WPlace-AutoBOT/main/auto-image-styles.css');
console.log(' ⚠️ Performance: Network request required');
}
-
+
console.groupEnd();
// Ensure default theme is loaded from extension resources
if (window.applyTheme && typeof window.applyTheme === 'function') {
console.group('%c🎨 Loading Default Theme from Extension', 'color: #8b5cf6; font-weight: bold;');
-
+
// Determine the current theme file name
let defaultTheme = 'classic'; // fallback
if (CONFIG.currentTheme === 'Neon Retro') {
@@ -3579,10 +3596,10 @@ localStorage.removeItem("lp");
} else if (CONFIG.currentTheme === 'Acrylic') {
defaultTheme = 'acrylic';
}
-
+
console.log(`%c🎯 Loading theme: ${defaultTheme} (${CONFIG.currentTheme})`, 'color: #8b5cf6;');
const success = window.applyTheme(defaultTheme);
-
+
if (success) {
console.log(`%c✅ Default theme loaded from extension local resources`, 'color: #10b981; font-weight: bold;');
console.log(` 📍 Source: Extension local file (themes/${defaultTheme}.css)`);
@@ -3592,7 +3609,7 @@ localStorage.removeItem("lp");
console.warn(`%c⚠️ Failed to load theme ${defaultTheme} from extension`, 'color: #f59e0b;');
console.log(` 📝 Note: Theme CSS classes will still work`);
}
-
+
console.groupEnd();
} else {
console.warn(`%c⚠️ Extension applyTheme() function not available`, 'color: #f59e0b; font-weight: bold;');
@@ -6834,6 +6851,7 @@ localStorage.removeItem("lp");
updateUI('startPaintingMsg', 'success');
try {
+ await getAccounts();
await processImage();
} catch (e) {
console.error('Unexpected error:', e);
@@ -7422,20 +7440,79 @@ localStorage.removeItem("lp");
pixelBatch.pixels = [];
}
+ if (!CONFIG.autoSwap) {
+ if (state.displayCharges < state.cooldownChargeThreshold && !state.stopFlag) {
+ await Utils.dynamicSleep(() => {
+ if (state.displayCharges >= state.cooldownChargeThreshold) {
+ NotificationManager.maybeNotifyChargesReached(true);
+ return 0;
+ }
+ if (state.stopFlag) return 0;
+ return getMsToTargetCharges(
+ state.preciseCurrentCharges,
+ state.cooldownChargeThreshold,
+ state.cooldown
+ );
+ });
+ }
+ }
+ else {
+ if (state.displayCharges < state.cooldownChargeThreshold && !state.stopFlag) {
+ console.log("⚠️ Charges too low, swapping to next account...");
- if (state.displayCharges < state.cooldownChargeThreshold && !state.stopFlag) {
- await Utils.dynamicSleep(() => {
- if (state.displayCharges >= state.cooldownChargeThreshold) {
- NotificationManager.maybeNotifyChargesReached(true);
- return 0;
+ const accounts = JSON.parse(localStorage.getItem("accounts")) || [];
+ if (accounts.length === 0) {
+ console.warn("❌ No accounts available, stopping painting.");
+ state.stopFlag = true;
+ return;
}
- if (state.stopFlag) return 0;
- return getMsToTargetCharges(
- state.preciseCurrentCharges,
- state.cooldownChargeThreshold,
- state.cooldown
- );
- });
+
+ state.accountIndex = (state.accountIndex + 1) % accounts.length;
+ console.log("🔄 Switching to account index:", state.accountIndex);
+
+ const nextToken = accounts[state.accountIndex];
+ console.log("🔑 Next token:", nextToken);
+
+ if (!nextToken) {
+ console.warn("⚠️ Invalid token, skipping...");
+ return;
+ }
+
+ swapAccountTrigger(nextToken);
+
+ let maxRetries = 20;
+ let retryCount = 0;
+ let swapSuccess = false;
+
+ while (!swapSuccess && retryCount < maxRetries) {
+ console.log(`⏳ Waiting for account swap... (Attempt ${retryCount + 1}/${maxRetries})`);
+
+ // Wait for a short period before checking.
+ await new Promise(resolve => setTimeout(resolve, 1000));
+
+ try {
+ await fetchAccount();
+
+ console.log("✅ Account swap confirmed.");
+ swapSuccess = true;
+ } catch (error) {
+ console.warn("❌ Account swap not yet successful. Retrying...", error);
+ retryCount++;
+ }
+ }
+
+ if (swapSuccess) {
+
+ const { charges, cooldown } = await WPlaceService.getCharges();
+ state.displayCharges = Math.floor(charges);
+ state.cooldown = cooldown;
+ Utils.performSmartSave();
+ updateStats();
+ } else {
+ console.error("❌ Failed to swap account after multiple retries. Stopping loop.");
+ state.stopFlag = true;
+ }
+ }
}
if (state.stopFlag) {
@@ -7998,7 +8075,7 @@ localStorage.removeItem("lp");
async function createWasmToken(regionX, regionY, payload) {
try {
// Load the Pawtect module and WASM
- const mod = await import(new URL('/_app/immutable/chunks/'+pawtect_chunk, location.origin).href);
+ const mod = await import(new URL('/_app/immutable/chunks/' + pawtect_chunk, location.origin).href);
let wasm;
try {
wasm = await mod._();
@@ -8130,6 +8207,97 @@ localStorage.removeItem("lp");
console.error(`❌ Could not find Pawtect chunk: `, error);
}
+ async function purchase(type) {
+ // loadThemePreference()
+ let id;
+ let chargeMultiplier;
+ if (type === "max_charges") {
+ id = 70;
+ chargeMultiplier = 5;
+ } else if (type === "paint_charges") {
+ id = 80;
+ chargeMultiplier = 30;
+ } else {
+ console.error("Error: Invalid purchase type provided.");
+ return;
+ }
+ const { droplets } = await WPlaceService.getCharges();
+ console.log("There are currently : ", droplets, "droplets.");
+ try {
+ const amounts = Math.floor(droplets / 500);
+ if (amounts < 1) {
+ console.log("Not enough droplets to purchase.");
+ return;
+ }
+ const payload = {
+ "product": {
+ "id": id,
+ "amount": amounts
+ }
+ };
+ const res = fetch("https://backend.wplace.live/purchase", {
+ method: "POST",
+ headers: {
+ "Content-Type": "text/plain;charset=UTF-8"
+ },
+ body: JSON.stringify(payload),
+ credentials: "include"
+ });
+ console.log("Fetch POST return :", res);
+ const { droplets: newDroplets } = await WPlaceService.getCharges();
+ if (droplets != newDroplets) {
+ console.log("Successfully bought", amounts * chargeMultiplier, type.replace('_', ' '), ".");
+ }
+ else {
+ console.log("Failed to buy charges");
+ }
+ } catch (e) {
+ console.error("An error occurred during the purchase:", e);
+ }
+ }
+ function swapAccountTrigger(token) {
+ localStorage.removeItem("lp");
+ if (!token) return;
+ console.log("Sending token to extension...");
+ window.postMessage({
+ source: 'my-userscript',
+ type: 'setCookie',
+ value: token
+ }, '*');
+ }
+ async function getAccounts() {
+ return new Promise((resolve, reject) => {
+ console.log("Requesting accounts from extension...");
+ // Ask extension for accounts
+ window.postMessage({
+ source: "my-userscript",
+ type: "getAccounts"
+ }, "*");
+ function handler(event) {
+ if (event.source !== window) return;
+ if (event.data.source !== "extension") return;
+ if (event.data.type === "accountsData") {
+ window.removeEventListener("message", handler);
+ // Save to localStorage
+ try {
+ localStorage.setItem("accounts", JSON.stringify(event.data.accounts));
+ console.log("✅ Accounts saved to localStorage:", event.data.accounts);
+ } catch (e) {
+ console.error("❌ Failed to save accounts:", e);
+ }
+ resolve(event.data.accounts);
+ }
+ }
+ window.addEventListener("message", handler);
+ });
+ }
+ async function fetchAccount() {
+ const { ID, Charges, Max, Droplets } = await WPlaceService.fetchCheck();
+ console.log("User's ID :", ID);
+ console.log("User's Charges :", Charges, "/", Max);
+ console.log("User's Droplets :", Droplets);
+ }
+
createUI().then(() => {
// Generate token automatically after UI is ready
setTimeout(initializeTokenGenerator, 1000);
diff --git a/Extension/scripts/Auto-Repair.js b/Extension/scripts/Auto-Repair.js
index bbcbd92..7cd9677 100644
--- a/Extension/scripts/Auto-Repair.js
+++ b/Extension/scripts/Auto-Repair.js
@@ -17,6 +17,11 @@
TOKEN_PRELOAD_BUFFER: 60000, // Preload tokens 1 minute before expiry
MAX_RETRIES: 10,
RETRY_DELAY_BASE: 1000,
+ // Auto-batch configuration
+ AUTO_BATCH_ENABLED: true, // Enable auto pixel batch sizing
+ MAX_BATCH_SIZE: 500, // Maximum pixels per batch
+ MIN_BATCH_SIZE: 1, // Minimum pixels per batch
+ BATCH_OPTIMIZATION: true, // Optimize batch size based on detected damage
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 } },
@@ -100,7 +105,7 @@
startPosition: null,
region: null,
paintWhitePixels: true,
- paintTransparentPixels: false,
+ paintTransparentPixels: true, // Changed to true to fix transparent pixel detection
autoRepairEnabled: false,
autoRepairInterval: 30,
autoRepairTimer: null,
@@ -117,6 +122,10 @@
windowMinimized: false,
lastAttackState: null,
initialSetupComplete: false,
+ // Auto-batch state
+ autoBatchEnabled: CONFIG.AUTO_BATCH_ENABLED,
+ currentBatchSize: CONFIG.MIN_BATCH_SIZE,
+ batchOptimization: CONFIG.BATCH_OPTIMIZATION,
};
// Random string generator
@@ -125,7 +134,7 @@
const fpStr32 = randStr(32);
- // Enhanced Turnstile token handling - Actualizado con la lógica de new.txt
+ // Enhanced Turnstile token handling - Actualizado con la lógica de new.txt lol
let turnstileToken = null;
let tokenExpiryTime = 0;
let tokenGenerationInProgress = false;
@@ -1352,9 +1361,7 @@
const d = cached.data;
const a = d[idx + 3];
- if (!state.paintTransparentPixels && a < alphaThresh) {
- return null;
- }
+ // Always return pixel data, let caller decide about transparency
return [d[idx], d[idx + 1], d[idx + 2], a];
}
@@ -1385,10 +1392,7 @@
const data = ctx.getImageData(x, y, 1, 1).data;
const a = data[3];
- if (!state.paintTransparentPixels && a < alphaThresh) {
- return null;
- }
-
+ // Always return pixel data, let caller decide about transparency
return [data[0], data[1], data[2], a];
} catch (e) {
Utils.addDebugLog(`Error reading pixel from tile ${tileKey}: ${e.message}`, 'error');
@@ -1457,7 +1461,7 @@
const overlayManager = new OverlayManager();
- // Enhanced WPlace API Service with autonomous capabilities - ACTUALIZADO CON NUEVA LÓGICA
+ // Enhanced WPlace API Service with auto-batch functionality
const WPlaceService = {
async paintPixelInRegion(regionX, regionY, pixelX, pixelY, color, retryCount = 0) {
try {
@@ -1524,6 +1528,83 @@
}
},
+ // NEW: Auto-batch pixel painting functionality
+ async paintPixelBatchInRegion(regionX, regionY, pixelBatch, retryCount = 0) {
+ try {
+ await ensureToken();
+ if (!turnstileToken) {
+ Utils.addDebugLog('No valid token available for batch paint request', 'error');
+ return 'token_error';
+ }
+
+ // Prepare batch coordinates and colors
+ const coords = [];
+ const colors = [];
+
+ for (const pixel of pixelBatch) {
+ coords.push(pixel.pixelX, pixel.pixelY);
+ colors.push(pixel.color);
+ }
+
+ const payload = {
+ coords: coords,
+ colors: colors,
+ t: turnstileToken,
+ fp: fpStr32,
+ };
+
+ const wasmToken = await createWasmToken(regionX, regionY, payload);
+ if (!wasmToken) {
+ Utils.addDebugLog('Failed to generate WASM token for batch', 'error');
+ return false;
+ }
+
+ const res = await fetch(`https://backend.wplace.live/s0/pixel/${regionX}/${regionY}`, {
+ method: 'POST',
+ headers: {
+ 'Content-Type': 'text/plain;charset=UTF-8',
+ 'x-pawtect-token': wasmToken
+ },
+ credentials: 'include',
+ body: JSON.stringify(payload),
+ });
+
+ if (res.status === 403) {
+ Utils.addDebugLog(`403 Forbidden for batch (${pixelBatch.length} pixels). Token invalid/expired.`, 'error');
+ invalidateToken();
+
+ if (retryCount < 2 && state.autonomousMode) {
+ Utils.addDebugLog(`Autonomous batch retry ${retryCount + 1}/2 for ${pixelBatch.length} pixels`, 'warning');
+ await Utils.sleep(1000);
+ return await this.paintPixelBatchInRegion(regionX, regionY, pixelBatch, retryCount + 1);
+ }
+
+ return 'token_error';
+ }
+
+ if (!res.ok) {
+ Utils.addDebugLog(`Batch paint request failed with status ${res.status}`, 'error');
+ return false;
+ }
+
+ const data = await res.json();
+ const successCount = data?.painted || 0;
+
+ Utils.addDebugLog(`Batch paint result: ${successCount}/${pixelBatch.length} pixels painted successfully`,
+ successCount === pixelBatch.length ? 'success' : 'warning');
+
+ return {
+ success: successCount > 0,
+ painted: successCount,
+ total: pixelBatch.length
+ };
+
+ } catch (e) {
+ Utils.addDebugLog(`Batch paint request error for ${pixelBatch.length} pixels: ${e.message}`, 'error');
+ return false;
+ }
+ },
+
async getCharges() {
try {
const res = await fetch('https://backend.wplace.live/me', {
@@ -1543,14 +1624,14 @@
},
};
- // Anti-grief repair system with autonomous capabilities
+ // Enhanced anti-grief repair system with improved transparent pixel detection
async function scanForDamage() {
if (!state.imageData || !state.startPosition || !state.region) {
Utils.addDebugLog('No image data or position for scanning', 'warning');
return [];
}
- Utils.addDebugLog('Starting autonomous damage scan...', 'info');
+ Utils.addDebugLog('Starting enhanced damage scan with transparent pixel detection...', 'info');
updateStatus(Utils.t('scanningForDamage'));
const damagedPixels = [];
@@ -1579,9 +1660,11 @@
return [];
}
- Utils.addDebugLog(`Scanning ${width}x${height} image for damage (autonomous: ${state.autonomousMode})...`, 'info');
+ Utils.addDebugLog(`Scanning ${width}x${height} image for damage (transparent detection: ${state.paintTransparentPixels})...`, 'info');
let scannedPixels = 0;
+ let transparentPixelsDetected = 0;
+ let wrongColorPixelsDetected = 0;
let lastProgressUpdate = Date.now();
for (let y = 0; y < height; y++) {
@@ -1594,10 +1677,10 @@
const originalB = pixels[idx + 2];
const originalA = pixels[idx + 3];
- if (!state.paintTransparentPixels && originalA < CONFIG.TRANSPARENCY_THRESHOLD) {
- continue;
- }
-
+ // Enhanced transparent pixel detection
+ const isOriginalTransparent = originalA < state.customTransparencyThreshold;
+
+ // Skip white pixels if configured
if (!state.paintWhitePixels && Utils.isWhitePixel(originalR, originalG, originalB)) {
continue;
}
@@ -1614,30 +1697,93 @@
try {
const currentPixel = await overlayManager.getTilePixelColor(tileX, tileY, pixelX, pixelY);
+ // Handle missing current pixel data (could indicate transparent area)
if (!currentPixel) {
- if (!state.autonomousMode) {
- Utils.addDebugLog(`No current pixel data for (${x},${y}) - tile ${tileX},${tileY}`, 'warning');
+ if (state.paintTransparentPixels && !isOriginalTransparent) {
+ // Original pixel is not transparent but current is missing/transparent
+ const targetColor = Utils.resolveColor([originalR, originalG, originalB], state.availableColors);
+ damagedPixels.push({
+ x,
+ y,
+ originalColor: targetColor,
+ currentColor: { id: 63, name: 'Transparent', rgb: [0, 0, 0] }, // Transparent color
+ originalRgb: [originalR, originalG, originalB],
+ currentRgb: [0, 0, 0],
+ isDamagedTransparent: true
+ });
+ transparentPixelsDetected++;
+
+ if (!state.autonomousMode || transparentPixelsDetected <= 10) {
+ Utils.addDebugLog(`Missing/Transparent pixel at (${x},${y}): expected color ${targetColor.id}, found transparent/missing`, 'warning');
+ }
}
continue;
}
- const targetColor = Utils.resolveColor([originalR, originalG, originalB], state.availableColors);
- const currentColor = Utils.resolveColor(currentPixel.slice(0, 3), state.availableColors);
+ const currentIsTransparent = currentPixel[3] < state.customTransparencyThreshold;
- if (targetColor.id !== currentColor.id) {
+ // Case 1: Original is not transparent, but current is transparent (damaged)
+ if (!isOriginalTransparent && currentIsTransparent && state.paintTransparentPixels) {
+ const targetColor = Utils.resolveColor([originalR, originalG, originalB], state.availableColors);
damagedPixels.push({
x,
y,
originalColor: targetColor,
- currentColor: currentColor,
+ currentColor: { id: 63, name: 'Transparent', rgb: [0, 0, 0] },
originalRgb: [originalR, originalG, originalB],
- currentRgb: currentPixel.slice(0, 3)
+ currentRgb: currentPixel.slice(0, 3),
+ isDamagedTransparent: true
});
+ transparentPixelsDetected++;
+
+ if (!state.autonomousMode || transparentPixelsDetected <= 10) {
+ Utils.addDebugLog(`Transparent damage at (${x},${y}): expected color ${targetColor.id}, found transparent`, 'warning');
+ }
+ continue;
+ }
- if (!state.autonomousMode || damagedPixels.length <= 10) {
- Utils.addDebugLog(`Damage at (${x},${y}): expected color ${targetColor.id}, found color ${currentColor.id}`, 'warning');
+ // Case 2: Original is transparent, current is not transparent (wrong placement)
+ if (isOriginalTransparent && !currentIsTransparent && state.paintTransparentPixels) {
+ damagedPixels.push({
+ x,
+ y,
+ originalColor: { id: 63, name: 'Transparent', rgb: null },
+ currentColor: Utils.resolveColor(currentPixel.slice(0, 3), state.availableColors),
+ originalRgb: [originalR, originalG, originalB],
+ currentRgb: currentPixel.slice(0, 3),
+ isDamagedTransparent: true
+ });
+ transparentPixelsDetected++;
+
+ if (!state.autonomousMode || transparentPixelsDetected <= 10) {
+ Utils.addDebugLog(`Wrong placement at (${x},${y}): expected transparent, found color`, 'warning');
+ }
+ continue;
+ }
+
+ // Case 3: Both are not transparent, check color match (normal damage detection)
+ if (!isOriginalTransparent && !currentIsTransparent) {
+ const targetColor = Utils.resolveColor([originalR, originalG, originalB], state.availableColors);
+ const currentColor = Utils.resolveColor(currentPixel.slice(0, 3), state.availableColors);
+
+ if (targetColor.id !== currentColor.id) {
+ damagedPixels.push({
+ x,
+ y,
+ originalColor: targetColor,
+ currentColor: currentColor,
+ originalRgb: [originalR, originalG, originalB],
+ currentRgb: currentPixel.slice(0, 3),
+ isDamagedTransparent: false
+ });
+ wrongColorPixelsDetected++;
+
+ if (!state.autonomousMode || wrongColorPixelsDetected <= 10) {
+ Utils.addDebugLog(`Color damage at (${x},${y}): expected color ${targetColor.id}, found color ${currentColor.id}`, 'warning');
+ }
}
}
+
} catch (e) {
if (!state.autonomousMode) {
Utils.addDebugLog(`Error checking pixel (${x},${y}): ${e.message}`, 'error');
@@ -1646,7 +1792,7 @@
}
if (state.autonomousMode && Date.now() - lastProgressUpdate > 5000) {
- Utils.addDebugLog(`Scan progress: ${y}/${height} rows (${Math.round((y / height) * 100)}%), found ${damagedPixels.length} damaged`, 'info');
+ Utils.addDebugLog(`Scan progress: ${y}/${height} rows (${Math.round((y / height) * 100)}%), found ${damagedPixels.length} damaged (${transparentPixelsDetected} transparent, ${wrongColorPixelsDetected} wrong color)`, 'info');
lastProgressUpdate = Date.now();
} else if (!state.autonomousMode && y % 10 === 0) {
Utils.addDebugLog(`Scan progress: ${y}/${height} rows (${Math.round((y / height) * 100)}%)`, 'info');
@@ -1654,7 +1800,17 @@
}
const logLevel = damagedPixels.length > 0 ? 'warning' : 'success';
- Utils.addDebugLog(`Scan complete. Checked ${scannedPixels} pixels, found ${damagedPixels.length} damaged`, logLevel);
+ Utils.addDebugLog(`Enhanced scan complete. Checked ${scannedPixels} pixels, found ${damagedPixels.length} damaged (${transparentPixelsDetected} transparent issues, ${wrongColorPixelsDetected} wrong colors)`, logLevel);
+
+ // Smart auto-adjust batch size: if 6 pixels detected, batch size = 6
+ if (state.autoBatchEnabled && state.batchOptimization && damagedPixels.length > 0) {
+ const newBatchSize = Math.min(CONFIG.MAX_BATCH_SIZE, Math.max(CONFIG.MIN_BATCH_SIZE, damagedPixels.length));
+ if (newBatchSize !== state.currentBatchSize) {
+ state.currentBatchSize = newBatchSize;
+ Utils.addDebugLog(`Smart batch: Auto-adjusted batch size to ${newBatchSize} to match ${damagedPixels.length} detected damaged pixels`, 'info');
+ updateBatchInfo(); // Update UI display
+ }
+ }
return damagedPixels;
}
@@ -1665,98 +1821,174 @@
return 0;
}
- Utils.addDebugLog(`Starting autonomous repair of ${damagedPixels.length} pixels`, 'info');
+ Utils.addDebugLog(`Starting enhanced repair of ${damagedPixels.length} pixels with auto-batch (batch size: ${state.currentBatchSize})`, 'info');
updateStatus(Utils.t('repairingPixels', { count: damagedPixels.length }));
let repairedCount = 0;
let consecutiveFailures = 0;
const maxConsecutiveFailures = state.autonomousMode ? 5 : 3;
- for (let i = 0; i < damagedPixels.length; i++) {
- const pixel = damagedPixels[i];
+ // Group pixels by region for batch processing
+ const pixelsByRegion = new Map();
+
+ for (const pixel of damagedPixels) {
+ const absX = state.startPosition.x + pixel.x;
+ const absY = state.startPosition.y + pixel.y;
+ const regionX = state.region.x + Math.floor(absX / 1000);
+ const regionY = state.region.y + Math.floor(absY / 1000);
+ const regionKey = `${regionX},${regionY}`;
+ if (!pixelsByRegion.has(regionKey)) {
+ pixelsByRegion.set(regionKey, []);
+ }
+
+ pixelsByRegion.get(regionKey).push({
+ ...pixel,
+ regionX,
+ regionY,
+ pixelX: absX % 1000,
+ pixelY: absY % 1000,
+ color: pixel.isDamagedTransparent && pixel.originalColor.id === 63 ? 63 : pixel.originalColor.id
+ });
+ }
+
+ Utils.addDebugLog(`Grouped ${damagedPixels.length} pixels into ${pixelsByRegion.size} regions for batch processing`, 'info');
+
+ // Process each region
+ for (const [regionKey, regionPixels] of pixelsByRegion) {
if (state.stopFlag) {
Utils.addDebugLog('Repair stopped by user request', 'warning');
break;
}
- // Enhanced charge management for autonomous mode
- await updateCharges();
- let chargeWaitAttempts = 0;
- const maxChargeWaitAttempts = state.autonomousMode ? 20 : 10;
-
- while (state.displayCharges < 1 && !state.stopFlag && chargeWaitAttempts < maxChargeWaitAttempts) {
- chargeWaitAttempts++;
- const waitTime = state.autonomousMode ? Math.min(state.cooldown, 10000) : state.cooldown;
+ const [regionX, regionY] = regionKey.split(',').map(Number);
+ Utils.addDebugLog(`Processing region ${regionKey} with ${regionPixels.length} pixels`, 'info');
+
+ // Process pixels in batches
+ for (let i = 0; i < regionPixels.length; i += state.currentBatchSize) {
+ if (state.stopFlag) break;
+
+ const batch = regionPixels.slice(i, i + state.currentBatchSize);
+ const actualBatchSize = Math.min(batch.length, state.currentBatchSize);
- if (chargeWaitAttempts === 1) {
- Utils.addDebugLog(`Waiting for charges... (${state.displayCharges}/${state.maxCharges})`, 'info');
- }
-
- await Utils.dynamicSleep(() => {
- if (state.displayCharges >= 1) return 0;
- if (state.stopFlag) return 0;
- return waitTime;
- });
+ // Enhanced charge management for batch processing
await updateCharges();
- }
-
- if (state.stopFlag) break;
-
- if (state.displayCharges < 1) {
- Utils.addDebugLog(`No charges available after waiting, skipping pixel (${pixel.x},${pixel.y})`, 'warning');
- continue;
- }
-
- const success = await repairSinglePixel(pixel);
- if (success) {
- repairedCount++;
- consecutiveFailures = 0;
- if (!state.autonomousMode || repairedCount <= 10 || repairedCount % 10 === 0) {
- Utils.addDebugLog(`Repaired pixel (${pixel.x},${pixel.y}) with color ${pixel.originalColor.id} [${repairedCount}/${damagedPixels.length}]`, 'success');
- }
- } else {
- consecutiveFailures++;
- Utils.addDebugLog(`Failed to repair pixel (${pixel.x},${pixel.y}) [${consecutiveFailures}/${maxConsecutiveFailures} consecutive failures]`, 'error');
+ let chargeWaitAttempts = 0;
+ const maxChargeWaitAttempts = state.autonomousMode ? 20 : 10;
- if (consecutiveFailures >= maxConsecutiveFailures) {
- Utils.addDebugLog(`Too many consecutive failures (${consecutiveFailures}), pausing repair`, 'error');
- if (state.autonomousMode) {
- Utils.addDebugLog('Autonomous mode: will retry repair in 60 seconds', 'warning');
- setTimeout(() => {
- if (state.autoRepairEnabled && !state.stopFlag) {
- performRepairCheck();
- }
- }, 60000);
+ while (state.displayCharges < actualBatchSize && !state.stopFlag && chargeWaitAttempts < maxChargeWaitAttempts) {
+ chargeWaitAttempts++;
+ const waitTime = state.autonomousMode ? Math.min(state.cooldown, 10000) : state.cooldown;
+
+ if (chargeWaitAttempts === 1) {
+ Utils.addDebugLog(`Waiting for charges... (need ${actualBatchSize}, have ${state.displayCharges}/${state.maxCharges})`, 'info');
}
- break;
+
+ await Utils.dynamicSleep(() => {
+ if (state.displayCharges >= actualBatchSize) return 0;
+ if (state.stopFlag) return 0;
+ return waitTime;
+ });
+ await updateCharges();
}
- }
- await updateCharges();
+ if (state.stopFlag) break;
+
+ if (state.displayCharges < actualBatchSize) {
+ Utils.addDebugLog(`Insufficient charges for batch (need ${actualBatchSize}, have ${state.displayCharges}), falling back to single pixel repair`, 'warning');
+
+ // Fall back to single pixel repair
+ for (const pixel of batch) {
+ if (state.stopFlag) break;
+
+ await updateCharges();
+ if (state.displayCharges < 1) {
+ Utils.addDebugLog(`No charges available, skipping pixel (${pixel.x},${pixel.y})`, 'warning');
+ continue;
+ }
+
+ const success = await repairSinglePixel(pixel);
+ if (success) {
+ repairedCount++;
+ consecutiveFailures = 0;
+ } else {
+ consecutiveFailures++;
+ }
+
+ if (consecutiveFailures >= maxConsecutiveFailures) break;
+ await Utils.sleep(100);
+ }
+ } else {
+ // Use batch repair
+ const batchResult = await repairPixelBatch(regionX, regionY, batch);
+
+ if (batchResult && batchResult.success) {
+ repairedCount += batchResult.painted;
+ consecutiveFailures = 0;
+
+ Utils.addDebugLog(`Batch repair: ${batchResult.painted}/${batchResult.total} pixels repaired successfully [Total: ${repairedCount}/${damagedPixels.length}]`, 'success');
+ } else {
+ consecutiveFailures++;
+ Utils.addDebugLog(`Batch repair failed for ${batch.length} pixels [${consecutiveFailures}/${maxConsecutiveFailures} consecutive failures]`, 'error');
+
+ if (consecutiveFailures >= maxConsecutiveFailures) {
+ Utils.addDebugLog(`Too many consecutive batch failures (${consecutiveFailures}), pausing repair`, 'error');
+ if (state.autonomousMode) {
+ Utils.addDebugLog('Autonomous mode: will retry repair in 60 seconds', 'warning');
+ setTimeout(() => {
+ if (state.autoRepairEnabled && !state.stopFlag) {
+ performRepairCheck();
+ }
+ }, 60000);
+ }
+ break;
+ }
+ }
+ }
+
+ await updateCharges();
+
+ // Dynamic delay based on mode and success rate
+ const baseDelay = state.autonomousMode ? 100 : 200;
+ const adaptiveDelay = consecutiveFailures > 0 ? baseDelay * (consecutiveFailures + 1) : baseDelay;
+ await Utils.sleep(adaptiveDelay);
+ }
- // Dynamic delay based on mode and success rate
- const baseDelay = state.autonomousMode ? 100 : 200;
- const adaptiveDelay = consecutiveFailures > 0 ? baseDelay * (consecutiveFailures + 1) : baseDelay;
- await Utils.sleep(adaptiveDelay);
+ if (consecutiveFailures >= maxConsecutiveFailures) break;
}
const message = Utils.t('repairComplete', { repaired: repairedCount });
updateStatus(message);
- Utils.addDebugLog(`${message} (${damagedPixels.length - repairedCount} remaining)`, 'success');
+ Utils.addDebugLog(`${message} (${damagedPixels.length - repairedCount} remaining) - Used auto-batch with size ${state.currentBatchSize}`, 'success');
return repairedCount;
}
- async function repairSinglePixel(pixel) {
- const { x, y, originalColor } = pixel;
+ async function repairPixelBatch(regionX, regionY, pixelBatch) {
+ try {
+ Utils.addDebugLog(`Attempting batch repair of ${pixelBatch.length} pixels in region ${regionX},${regionY}`, 'info');
+
+ const result = await WPlaceService.paintPixelBatchInRegion(regionX, regionY, pixelBatch);
- const absX = state.startPosition.x + x;
- const absY = state.startPosition.y + y;
- const regionX = state.region.x + Math.floor(absX / 1000);
- const regionY = state.region.y + Math.floor(absY / 1000);
- const pixelX = absX % 1000;
- const pixelY = absY % 1000;
+ if (result === 'token_error') {
+ Utils.addDebugLog('Token error during batch repair, refreshing token...', 'warning');
+ await ensureToken(true);
+ await Utils.sleep(state.autonomousMode ? 500 : 1000);
+
+ // Retry once with new token
+ const retryResult = await WPlaceService.paintPixelBatchInRegion(regionX, regionY, pixelBatch);
+ return retryResult;
+ }
+
+ return result;
+ } catch (e) {
+ Utils.addDebugLog(`Error during batch repair: ${e.message}`, 'error');
+ return false;
+ }
+ }
+
+ async function repairSinglePixel(pixel) {
+ const { x, y, originalColor, regionX, regionY, pixelX, pixelY, color } = pixel;
try {
const result = await WPlaceService.paintPixelInRegion(
@@ -1764,7 +1996,7 @@
regionY,
pixelX,
pixelY,
- originalColor.id
+ color || originalColor.id
);
if (result === 'token_error') {
@@ -1773,10 +2005,16 @@
await Utils.sleep(state.autonomousMode ? 500 : 1000);
// Retry once with new token
- const retryResult = await WPlaceService.paintPixelInRegion(regionX, regionY, pixelX, pixelY, originalColor.id);
+ const retryResult = await WPlaceService.paintPixelInRegion(regionX, regionY, pixelX, pixelY, color || originalColor.id);
return retryResult === true;
}
+ if (result === true) {
+ if (pixel.isDamagedTransparent) {
+ Utils.addDebugLog(`Repaired transparent pixel (${x},${y}) with color ${color || originalColor.id}`, 'success');
+ }
+ }
+
return result === true;
} catch (e) {
Utils.addDebugLog(`Error repairing pixel (${x},${y}): ${e.message}`, 'error');
@@ -1993,6 +2231,42 @@
}
}
+ // Update batch information display for new UI
+ function updateBatchInfo() {
+ const batchSizeEl = document.getElementById('batchSize');
+ const batchStatusEl = document.getElementById('batchStatus');
+ const smartModeEl = document.getElementById('smartBatchMode');
+ const manualModeEl = document.getElementById('manualBatchMode');
+ const manualControlsEl = document.getElementById('manualBatchControls');
+
+ // Update radio button states
+ if (smartModeEl && manualModeEl) {
+ smartModeEl.checked = state.autoBatchEnabled && state.batchOptimization;
+ manualModeEl.checked = !state.autoBatchEnabled || !state.batchOptimization;
+ }
+
+ // Update manual controls visibility
+ if (manualControlsEl) {
+ manualControlsEl.style.display = (!state.autoBatchEnabled || !state.batchOptimization) ? 'flex' : 'none';
+ }
+
+ // Update batch size input
+ if (batchSizeEl) {
+ batchSizeEl.value = state.currentBatchSize;
+ }
+
+ // Update status display
+ if (batchStatusEl) {
+ if (state.autoBatchEnabled && state.batchOptimization) {
+ batchStatusEl.textContent = `🧠 Smart Batch: AUTO (Current: ${state.currentBatchSize})`;
+ batchStatusEl.className = 'status-info';
+ } else {
+ batchStatusEl.textContent = `✋ Manual Batch: ${state.currentBatchSize} pixels fixed`;
+ batchStatusEl.className = 'status-warning';
+ }
+ }
+ }
+
function updateSystemStatus() {
const systemEl = document.getElementById('systemInfo');
if (systemEl) {
@@ -2182,6 +2456,149 @@
margin-right: 8px !important;
}
+ /* Enhanced Batch Control Styles */
+ .batch-control-group {
+ display: flex !important;
+ align-items: center !important;
+ gap: 8px !important;
+ margin: 5px 0 !important;
+ padding: 8px !important;
+ background: rgba(0, 255, 65, 0.05) !important;
+ border: 1px solid rgba(0, 255, 65, 0.3) !important;
+ border-radius: 3px !important;
+ }
+
+ .batch-control-header {
+ text-align: center !important;
+ margin-bottom: 10px !important;
+ padding: 5px !important;
+ border-bottom: 1px solid rgba(0, 255, 65, 0.3) !important;
+ }
+
+ .batch-control-label {
+ color: #00ff41 !important;
+ font-family: 'Press Start 2P', monospace !important;
+ font-size: 6px !important;
+ text-shadow: 0 0 3px #00ff41 !important;
+ white-space: nowrap !important;
+ }
+
+ .batch-mode-selection {
+ margin: 10px 0 !important;
+ }
+
+ .batch-mode-option {
+ margin: 8px 0 !important;
+ padding: 6px !important;
+ background: rgba(0, 255, 65, 0.03) !important;
+ border: 1px solid rgba(0, 255, 65, 0.2) !important;
+ border-radius: 3px !important;
+ transition: all 0.3s ease !important;
+ }
+
+ .batch-mode-option:hover {
+ background: rgba(0, 255, 65, 0.08) !important;
+ border-color: rgba(0, 255, 65, 0.4) !important;
+ }
+
+ .batch-mode-label {
+ color: #00ff41 !important;
+ font-family: 'Press Start 2P', monospace !important;
+ font-size: 6px !important;
+ text-shadow: 0 0 3px #00ff41 !important;
+ margin-left: 8px !important;
+ }
+
+ .neon-radio {
+ accent-color: #00ff41 !important;
+ transform: scale(1.2) !important;
+ margin-right: 8px !important;
+ }
+
+ .batch-manual-controls {
+ display: flex !important;
+ align-items: center !important;
+ gap: 8px !important;
+ margin: 8px 0 !important;
+ padding: 8px !important;
+ background: rgba(0, 255, 65, 0.05) !important;
+ border: 1px solid rgba(0, 255, 65, 0.3) !important;
+ border-radius: 3px !important;
+ }
+
+ .batch-size-control {
+ display: flex !important;
+ align-items: center !important;
+ gap: 2px !important;
+ border: 1px solid #00ff41 !important;
+ border-radius: 3px !important;
+ background: #16213e !important;
+ overflow: hidden !important;
+ }
+
+ .batch-adjust-btn {
+ width: 20px !important;
+ height: 24px !important;
+ background: #16213e !important;
+ border: none !important;
+ color: #00ff41 !important;
+ font-family: 'Press Start 2P', monospace !important;
+ font-size: 8px !important;
+ cursor: pointer !important;
+ transition: all 0.3s ease !important;
+ text-shadow: 0 0 3px #00ff41 !important;
+ display: flex !important;
+ align-items: center !important;
+ justify-content: center !important;
+ }
+
+ .batch-adjust-btn:hover {
+ background: rgba(0, 255, 65, 0.1) !important;
+ box-shadow: 0 0 5px rgba(0, 255, 65, 0.3) !important;
+ }
+
+ .batch-adjust-btn:active {
+ background: rgba(0, 255, 65, 0.2) !important;
+ transform: scale(0.95) !important;
+ }
+
+ .batch-size-input {
+ width: 50px !important;
+ height: 24px !important;
+ background: #16213e !important;
+ border: none !important;
+ color: #00ff41 !important;
+ padding: 0 4px !important;
+ font-family: 'Press Start 2P', monospace !important;
+ font-size: 6px !important;
+ text-align: center !important;
+ text-shadow: 0 0 3px #00ff41 !important;
+ outline: none !important;
+ /* Hide browser default spinner buttons */
+ -webkit-appearance: none !important;
+ -moz-appearance: textfield !important;
+ }
+
+ /* Hide Chrome spinner buttons */
+ .batch-size-input::-webkit-outer-spin-button,
+ .batch-size-input::-webkit-inner-spin-button {
+ -webkit-appearance: none !important;
+ margin: 0 !important;
+ }
+
+ /* Hide Firefox spinner buttons */
+ .batch-size-input[type=number] {
+ -moz-appearance: textfield !important;
+ }
+
+ .status-info {
+ color: #00ff41 !important;
+ }
+
+ .status-warning {
+ color: #ff6b35 !important;
+ }
+
.neon-panel {
background: rgba(22, 33, 62, 0.3) !important;
border: 1px solid #00ff41 !important;
@@ -2312,6 +2729,9 @@
System: AUTONOMOUS | Token: GENERATOR | Auto-refresh: ON
+
+ Auto-Batch: ON | Size: 5 | Opt: ON
+
@@ -2354,11 +2774,54 @@
+
+
+
+
+
+
+
+
+
+
+
Batch Size:
+
+
+
+
+
+
pixels
+
+
+
+
+
Image: Not loaded
Position: Not set
Colors: 0 available
+
Batch Mode: Auto (${state.currentBatchSize} pixels)
@@ -2414,6 +2877,9 @@
updateSystemStatus();
setInterval(updateSystemStatus, 15000);
+ // Update batch information
+ updateBatchInfo();
+
// Initialize token generator
initializeTokenGenerator();
}
@@ -2648,6 +3114,69 @@
}
});
+ // NEW: Enhanced batch mode controls
+ document.getElementById('smartBatchMode').addEventListener('change', (e) => {
+ if (e.target.checked) {
+ state.autoBatchEnabled = true;
+ state.batchOptimization = true;
+ document.getElementById('manualBatchControls').style.display = 'none';
+ Utils.addDebugLog('Smart batch mode enabled - will auto-adjust to damage count', 'info');
+ updateBatchInfo();
+ }
+ });
+
+ document.getElementById('manualBatchMode').addEventListener('change', (e) => {
+ if (e.target.checked) {
+ state.autoBatchEnabled = false;
+ state.batchOptimization = false;
+ document.getElementById('manualBatchControls').style.display = 'flex';
+ Utils.addDebugLog('Manual batch mode enabled - using fixed batch size', 'info');
+ updateBatchInfo();
+ }
+ });
+
+ // Manual batch size controls with +/- buttons
+ document.getElementById('batchSizeIncrease').addEventListener('click', () => {
+ const newSize = Math.min(CONFIG.MAX_BATCH_SIZE, state.currentBatchSize + 1);
+ if (newSize !== state.currentBatchSize) {
+ state.currentBatchSize = newSize;
+ document.getElementById('batchSize').value = state.currentBatchSize;
+ Utils.addDebugLog(`Increased batch size to ${state.currentBatchSize} pixels`, 'info');
+ updateBatchInfo();
+ }
+ });
+
+ document.getElementById('batchSizeDecrease').addEventListener('click', () => {
+ const newSize = Math.max(CONFIG.MIN_BATCH_SIZE, state.currentBatchSize - 1);
+ if (newSize !== state.currentBatchSize) {
+ state.currentBatchSize = newSize;
+ document.getElementById('batchSize').value = state.currentBatchSize;
+ Utils.addDebugLog(`Decreased batch size to ${state.currentBatchSize} pixels`, 'info');
+ updateBatchInfo();
+ }
+ });
+
+ // Manual input allows direct typing for precise control
+ document.getElementById('batchSize').addEventListener('input', (e) => {
+ const value = parseInt(e.target.value);
+ if (!isNaN(value)) {
+ const clampedValue = Math.max(CONFIG.MIN_BATCH_SIZE, Math.min(CONFIG.MAX_BATCH_SIZE, value));
+ if (clampedValue !== state.currentBatchSize) {
+ state.currentBatchSize = clampedValue;
+ if (clampedValue !== value) {
+ e.target.value = state.currentBatchSize;
+ }
+ Utils.addDebugLog(`Manual batch size set to ${state.currentBatchSize} pixels`, 'info');
+ updateBatchInfo();
+ }
+ }
+ });
+
+ document.getElementById('paintTransparentPixels').addEventListener('change', (e) => {
+ state.paintTransparentPixels = e.target.checked;
+ Utils.addDebugLog(`Transparent pixel repair ${state.paintTransparentPixels ? 'enabled' : 'disabled'}`, 'info');
+ });
+
// Clear debug
document.getElementById('clearDebugBtn').addEventListener('click', () => {
state.debugLogs = [];
diff --git a/Extension/themes/acrylic.css b/Extension/themes/acrylic.css
index f94db2a..10995a5 100644
--- a/Extension/themes/acrylic.css
+++ b/Extension/themes/acrylic.css
@@ -8,6 +8,32 @@
src: url('https://raw.githubusercontent.com/GORAlexComp/AppleColorEmojiFont/refs/heads/main/fonts/AppleColorEmoji.ttf');
}
+/* Multi-account panel styling */
+.wplace-theme-acrylic #wplace-accounts-container {
+ background: var(--wplace-primary);
+ color: var(--wplace-text);
+ border-radius: var(--wplace-radius);
+ box-shadow: none;
+ backdrop-filter: var(--wplace-backdrop);
+ border: var(--wplace-border-width) var(--wplace-border-style)
+ var(--wplace-border-color);
+ min-width: 250px;
+}
+
+.wplace-theme-acrylic #wplace-accounts-container * {
+ font-size: 15px;
+ line-height: normal;
+}
+
+/* Active account styles */
+.wplace-theme-acrylic .wplace-accounts-container .active-account {
+ --active-bg: color-mix(in srgb, white 15%, transparent);
+ --active-color: var(--wplace-success);
+ --inactive-color: var(--wplace-text);
+ --progress-bg: rgb(255 255 255 / 10%);
+ --progress-fill: white;
+}
+
/* Main Container */
.wplace-theme-acrylic #wplace-image-bot-container {
background: var(--wplace-primary);
diff --git a/Extension/themes/classic-light.css b/Extension/themes/classic-light.css
index 424ad36..b4d78b1 100644
--- a/Extension/themes/classic-light.css
+++ b/Extension/themes/classic-light.css
@@ -73,6 +73,26 @@
/* === COMPONENT STYLING === */
+/* Multi-account panel styling */
+.wplace-theme-classic-light #wplace-accounts-container {
+ background: linear-gradient(135deg, #fff 0%, #f8f9fa 100%) !important;
+ color: var(--wplace-text) !important;
+ border: 1px solid rgb(0 0 0 / 15%) !important;
+ border-radius: 12px !important;
+ box-shadow: 0 8px 32px rgb(0 0 0 / 15%), 0 0 0 1px rgb(0 0 0 / 8%) !important;
+ font-family: 'Segoe UI', Roboto, sans-serif !important;
+ backdrop-filter: blur(10px) !important;
+}
+
+/* Active account styles */
+.wplace-theme-classic-light .wplace-accounts-container .active-account {
+ --active-bg: var(--wplace-secondary) !important;
+ --active-color: var(--wplace-success) !important;
+ --inactive-color: var(--wplace-text) !important;
+ --progress-bg: rgb(0 0 0 / 10%) !important;
+ --progress-fill: var(--wplace-highlight) !important;
+}
+
/* Main container with clean light styling */
.wplace-theme-classic-light #wplace-image-bot-container {
background: linear-gradient(135deg, #fff 0%, #f8f9fa 100%) !important;
diff --git a/Extension/themes/classic.css b/Extension/themes/classic.css
index 634675e..cd1fb39 100644
--- a/Extension/themes/classic.css
+++ b/Extension/themes/classic.css
@@ -34,6 +34,29 @@
}
/* Classic theme container styling */
+/* Multi-account panel styling */
+:root #wplace-accounts-container,
+.wplace-theme-classic #wplace-accounts-container {
+ background: var(--wplace-primary);
+ color: var(--wplace-text);
+ border-radius: var(--wplace-radius);
+ box-shadow: var(--wplace-shadow);
+ font-family: var(--wplace-font);
+ backdrop-filter: var(--wplace-backdrop);
+ border: var(--wplace-border-width) var(--wplace-border-style) var(--wplace-border-color);
+}
+
+/* Active account styles */
+:root .wplace-accounts-container .active-account,
+.wplace-theme-classic .wplace-accounts-container .active-account {
+ --active-bg: var(--wplace-secondary);
+ --active-color: var(--wplace-success);
+ --inactive-color: var(--wplace-text);
+ --progress-bg: rgb(255 255 255 / 10%);
+ --progress-fill: var(--wplace-highlight);
+}
+
+/* Base container styling */
:root #wplace-image-bot-container,
.wplace-theme-classic #wplace-image-bot-container {
background: var(--wplace-primary);
diff --git a/Extension/themes/neon.css b/Extension/themes/neon.css
index a6d5795..184cb64 100644
--- a/Extension/themes/neon.css
+++ b/Extension/themes/neon.css
@@ -84,6 +84,122 @@
font-family: var(--wplace-font) !important;
}
+/* Multi-Account Panel Styles */
+.wplace-theme-neon #wplace-multi-account-container {
+ position: fixed !important;
+ top: 180px !important;
+ right: 20px !important;
+ width: 320px !important;
+ background: rgb(22 33 62 / 50%) !important;
+ border: 2px solid var(--wplace-text) !important;
+ border-radius: 0 !important;
+ box-shadow: 0 0 20px var(--wplace-text), inset 0 0 20px rgb(0 255 65 / 10%) !important;
+ font-family: var(--wplace-font) !important;
+ z-index: 9997 !important;
+ color: var(--wplace-text) !important;
+ text-shadow: 0 0 5px var(--wplace-text) !important;
+}
+
+.wplace-theme-neon #wplace-multi-account-container .wplace-panel-header {
+ display: flex !important;
+ justify-content: space-between !important;
+ align-items: center !important;
+ padding: 10px !important;
+ background: var(--wplace-secondary) !important;
+ border-bottom: 1px solid var(--wplace-text) !important;
+ text-transform: uppercase !important;
+ font-size: 9px !important;
+}
+
+.wplace-theme-neon #wplace-multi-account-container .wplace-header-btn {
+ background: transparent !important;
+ border: 1px solid var(--wplace-text) !important;
+ color: var(--wplace-text) !important;
+ font-size: 8px !important;
+ padding: 4px 8px !important;
+ cursor: pointer !important;
+ text-shadow: 0 0 5px var(--wplace-text) !important;
+ font-family: var(--wplace-font) !important;
+ text-transform: uppercase !important;
+}
+
+.wplace-theme-neon #wplace-multi-account-container .wplace-header-btn:hover {
+ box-shadow: 0 0 15px var(--wplace-text) !important;
+ animation: pixel-blink 0.5s infinite !important;
+}
+
+.wplace-theme-neon .wplace-multi-account-content {
+ padding: 10px !important;
+}
+
+.wplace-theme-neon .wplace-multi-account-info {
+ font-size: 9px !important;
+ text-transform: uppercase !important;
+ margin-bottom: 10px !important;
+ color: var(--wplace-text) !important;
+ text-shadow: 0 0 5px var(--wplace-text) !important;
+ font-family: var(--wplace-font) !important;
+}
+
+.wplace-theme-neon .wplace-multi-account-current,
+.wplace-theme-neon .wplace-multi-account-stats {
+ margin-bottom: 8px !important;
+ font-size: 8px !important;
+}
+
+.wplace-theme-neon .wplace-multi-account-toggle {
+ margin-bottom: 12px !important;
+ display: flex !important;
+ align-items: center !important;
+}
+
+.wplace-theme-neon .wplace-multi-account-switch {
+ margin-right: 8px !important;
+}
+
+.wplace-theme-neon .wplace-multi-account-list {
+ max-height: 200px !important;
+ overflow-y: auto !important;
+}
+
+.wplace-theme-neon .wplace-multi-account-item {
+ display: flex !important;
+ justify-content: space-between !important;
+ align-items: center !important;
+ padding: 8px !important;
+ margin-bottom: 4px !important;
+ background: rgb(22 33 62 / 50%) !important;
+ border: 1px solid var(--wplace-text) !important;
+ font-size: 8px !important;
+ text-transform: uppercase !important;
+ font-family: var(--wplace-font) !important;
+}
+
+.wplace-theme-neon .wplace-multi-account-item.active {
+ background: rgb(0 255 65 / 10%) !important;
+ border-color: var(--wplace-success) !important;
+ box-shadow: 0 0 15px var(--wplace-success) !important;
+}
+
+.wplace-theme-neon .wplace-multi-account-list::-webkit-scrollbar {
+ width: 6px !important;
+}
+
+.wplace-theme-neon .wplace-multi-account-list::-webkit-scrollbar-track {
+ background: rgb(22 33 62 / 50%) !important;
+}
+
+.wplace-theme-neon .wplace-multi-account-list::-webkit-scrollbar-thumb {
+ background: var(--wplace-text) !important;
+ border-radius: 0 !important;
+ box-shadow: 0 0 10px var(--wplace-text) !important;
+}
+
+.wplace-theme-neon .wplace-multi-account-list::-webkit-scrollbar-thumb:hover {
+ background: var(--wplace-success) !important;
+ box-shadow: 0 0 15px var(--wplace-success) !important;
+}
+
.wplace-theme-neon #wplace-stats-container {
background: var(--wplace-primary) !important;
border: 2px solid var(--wplace-text) !important;