From ad71cbd43099cee75e829865fb9b761122a2f13a Mon Sep 17 00:00:00 2001 From: MiraclePatrickLumowa Date: Sun, 21 Sep 2025 15:47:09 +0800 Subject: [PATCH] feat: move artwork feature -some bug fixes -extracted artwork reprocessing --- Extensions/Script-manager.js | 642 ++ Extensions/auto-image-styles.css | 2194 +++++++ Extensions/background.js | 921 +++ Extensions/content-script.js | 380 ++ Extensions/icons/icon128.png | Bin 0 -> 112618 bytes Extensions/icons/icon16.png | Bin 0 -> 112618 bytes Extensions/icons/icon256.png | Bin 0 -> 61717 bytes Extensions/icons/icon32.png | Bin 0 -> 112618 bytes Extensions/icons/icon48.png | Bin 0 -> 112618 bytes Extensions/icons/icon64.png | Bin 0 -> 61717 bytes Extensions/lang/de.json | 74 + Extensions/lang/en.json | 154 + Extensions/lang/fr.json | 144 + Extensions/lang/id.json | 144 + Extensions/lang/ja.json | 144 + Extensions/lang/ko.json | 144 + Extensions/lang/lang/es-MX.json | 144 + Extensions/lang/pt.json | 143 + Extensions/lang/ru.json | 143 + Extensions/lang/tr.json | 144 + Extensions/lang/uk.json | 144 + Extensions/lang/vi.json | 144 + Extensions/lang/zh-CN.json | 144 + Extensions/lang/zh-TW.json | 144 + Extensions/manifest.json | 68 + Extensions/popup/popup.css | 917 +++ Extensions/popup/popup.html | 116 + Extensions/popup/popup.js | 701 +++ Extensions/scripts/Art-Extractor.js | 1671 +++++ Extensions/scripts/Auto-Farm.js | 650 ++ Extensions/scripts/Auto-Image.js | 8199 +++++++++++++++++++++++++ Extensions/scripts/Auto-Repair.js | 3331 ++++++++++ Extensions/scripts/image-processor.js | 1315 ++++ Extensions/scripts/overlay-manager.js | 837 +++ Extensions/scripts/token-manager.js | 846 +++ Extensions/scripts/utils-manager.js | 1258 ++++ Extensions/themes/acrylic.css | 883 +++ Extensions/themes/classic-light.css | 877 +++ Extensions/themes/classic.css | 1028 ++++ Extensions/themes/neon-cyan.css | 815 +++ Extensions/themes/neon-light.css | 881 +++ Extensions/themes/neon.css | 929 +++ 42 files changed, 31413 insertions(+) create mode 100644 Extensions/Script-manager.js create mode 100644 Extensions/auto-image-styles.css create mode 100644 Extensions/background.js create mode 100644 Extensions/content-script.js create mode 100644 Extensions/icons/icon128.png create mode 100644 Extensions/icons/icon16.png create mode 100644 Extensions/icons/icon256.png create mode 100644 Extensions/icons/icon32.png create mode 100644 Extensions/icons/icon48.png create mode 100644 Extensions/icons/icon64.png create mode 100644 Extensions/lang/de.json create mode 100644 Extensions/lang/en.json create mode 100644 Extensions/lang/fr.json create mode 100644 Extensions/lang/id.json create mode 100644 Extensions/lang/ja.json create mode 100644 Extensions/lang/ko.json create mode 100644 Extensions/lang/lang/es-MX.json create mode 100644 Extensions/lang/pt.json create mode 100644 Extensions/lang/ru.json create mode 100644 Extensions/lang/tr.json create mode 100644 Extensions/lang/uk.json create mode 100644 Extensions/lang/vi.json create mode 100644 Extensions/lang/zh-CN.json create mode 100644 Extensions/lang/zh-TW.json create mode 100644 Extensions/manifest.json create mode 100644 Extensions/popup/popup.css create mode 100644 Extensions/popup/popup.html create mode 100644 Extensions/popup/popup.js create mode 100644 Extensions/scripts/Art-Extractor.js create mode 100644 Extensions/scripts/Auto-Farm.js create mode 100644 Extensions/scripts/Auto-Image.js create mode 100644 Extensions/scripts/Auto-Repair.js create mode 100644 Extensions/scripts/image-processor.js create mode 100644 Extensions/scripts/overlay-manager.js create mode 100644 Extensions/scripts/token-manager.js create mode 100644 Extensions/scripts/utils-manager.js create mode 100644 Extensions/themes/acrylic.css create mode 100644 Extensions/themes/classic-light.css create mode 100644 Extensions/themes/classic.css create mode 100644 Extensions/themes/neon-cyan.css create mode 100644 Extensions/themes/neon-light.css create mode 100644 Extensions/themes/neon.css diff --git a/Extensions/Script-manager.js b/Extensions/Script-manager.js new file mode 100644 index 0000000..3c3c391 --- /dev/null +++ b/Extensions/Script-manager.js @@ -0,0 +1,642 @@ +// ==UserScript== +// @name WPlace AutoBOT Script Manager +// @namespace http://tampermonkey.net/ +// @version 2025-09-08.1 +// @description Script manager and launcher for WPlace AutoBOT +// @author TH3C0D3R +// @match https://wplace.live/* +// @grant none +// @icon +// ==/UserScript==l + +; (async () => { + console.log('%cšŸš€ WPlace AutoBOT Script Manager Loading...', 'color: #00ff41; font-weight: bold; font-size: 16px;'); + + // Available scripts configuration + const AVAILABLE_SCRIPTS = [ + { + name: 'Auto-Farm.js', + displayName: '🌾 Auto Farm', + description: 'Automated farming and pixel painting', + icon: '🌾', + category: 'automation' + }, + { + name: 'Auto-Image.js', + displayName: 'šŸ–¼ļø Auto Image', + description: 'Automated image processing and placement', + icon: 'šŸ–¼ļø', + category: 'automation' + }, + { + name: 'Auto-Repair.js', + displayName: 'šŸ”§ Auto Repair', + description: 'Automated repair and maintenance tasks', + icon: 'šŸ”§', + category: 'utility' + }, + { + name: 'Art-Extractor.js', + displayName: 'šŸŽØ Art Extractor', + description: 'Extract artwork areas to JSON for auto-repair', + icon: 'šŸŽØ', + category: 'utility' + } + ]; // Neon theme styling + const NEON_STYLES = ` + @import url('https://fonts.googleapis.com/css2?family=Press+Start+2P&display=swap'); + + .script-manager-container { + position: fixed !important; + top: 50% !important; + left: 50% !important; + transform: translate(-50%, -50%) !important; + background: #1a1a2e; + border: 3px solid #00ff41; + border-radius: 0; + box-shadow: + 0 0 30px rgb(0 255 65 / 50%), + inset 0 0 30px rgb(0 255 65 / 10%), + 0 0 0 1px #00ff41; + font-family: 'Press Start 2P', monospace, 'Courier New'; + z-index: 10001 !important; + min-width: 600px; + max-width: 800px; + max-height: 80vh; + overflow: hidden; + color: #00ff41; + animation: neon-pulse 2s ease-in-out infinite alternate; + } + + .script-manager-container::before { + content: ''; + position: absolute; + top: 0; + left: 0; + right: 0; + height: 2px; + background: linear-gradient(90deg, transparent, #00ff41, transparent); + z-index: 1; + pointer-events: none; + animation: scanline 3s linear infinite; + opacity: 0.7; + } + + .script-manager-header { + background: #16213e; + border-bottom: 2px solid #00ff41; + padding: 15px 20px; + position: relative; + display: flex; + justify-content: space-between; + align-items: center; + } + + .header-content { + display: flex; + align-items: center; + gap: 15px; + } + + .header-icon { + width: 32px; + height: 32px; + border-radius: 6px; + box-shadow: 0 0 15px rgba(0, 255, 65, 0.4); + transition: all 0.3s ease; + animation: pixel-blink 3s infinite; + } + + .header-icon:hover { + transform: scale(1.1); + box-shadow: 0 0 25px rgba(0, 255, 65, 0.6); + } + + .script-manager-title { + color: #00ff41; + font-size: 14px; + text-shadow: 0 0 15px #00ff41; + margin: 0; + text-transform: uppercase; + letter-spacing: 2px; + animation: text-glow 2s ease-in-out infinite alternate; + } + + .script-manager-close { + background: #16213e; + border: 2px solid #ff073a; + border-radius: 0; + color: #ff073a; + width: 30px; + height: 30px; + cursor: pointer; + font-family: 'Press Start 2P', monospace; + font-size: 12px; + display: flex; + align-items: center; + justify-content: center; + transition: all 0.3s ease; + text-shadow: 0 0 10px #ff073a; + } + + .script-manager-close:hover { + background: #ff073a; + color: #1a1a2e; + box-shadow: 0 0 20px #ff073a; + animation: pixel-blink 0.5s infinite; + } + + .script-manager-content { + padding: 20px; + max-height: 60vh; + overflow-y: auto; + background: linear-gradient(45deg, rgba(0,255,65,0.03) 0%, rgba(22,33,62,0.05) 100%); + } + + .script-grid { + display: grid; + grid-template-columns: repeat(auto-fit, minmax(250px, 1fr)); + gap: 15px; + margin-bottom: 20px; + } + + .script-card { + background: #16213e; + border: 2px solid #00ff41; + border-radius: 0; + padding: 15px; + cursor: pointer; + transition: all 0.3s ease; + position: relative; + overflow: hidden; + } + + .script-card::before { + content: ''; + position: absolute; + top: -50%; + left: -50%; + width: 200%; + height: 200%; + background: linear-gradient(45deg, transparent, rgba(0,255,65,0.1), transparent); + transform: rotate(45deg); + transition: all 0.5s ease; + opacity: 0; + } + + .script-card:hover::before { + animation: neon-sweep 1s ease-in-out; + } + + .script-card:hover { + background: rgba(0,255,65,0.1); + box-shadow: + 0 0 25px rgb(0 255 65 / 60%), + inset 0 0 25px rgb(0 255 65 / 15%); + transform: translateY(-3px); + animation: card-glow 0.5s ease-in-out infinite alternate; + } + + .script-card-header { + display: flex; + align-items: center; + margin-bottom: 10px; + } + + .script-icon { + font-size: 24px; + margin-right: 10px; + filter: drop-shadow(0 0 10px #00ff41); + } + + .script-title { + color: #00ff41; + font-size: 11px; + text-shadow: 0 0 10px #00ff41; + text-transform: uppercase; + letter-spacing: 1px; + margin: 0; + } + + .script-description { + color: #00ff41dd; + font-size: 8px; + line-height: 1.4; + text-shadow: 0 0 5px #00ff41; + margin-bottom: 15px; + } + + .script-category { + background: rgba(255, 107, 53, 0.2); + border: 1px solid #ff6b35; + color: #ff6b35; + padding: 3px 8px; + font-size: 7px; + text-transform: uppercase; + letter-spacing: 1px; + text-shadow: 0 0 5px #ff6b35; + display: inline-block; + } + + .script-manager-footer { + background: #16213e; + border-top: 2px solid #00ff41; + padding: 15px 20px; + display: flex; + justify-content: space-between; + align-items: center; + } + + .status-text { + color: #00ff41dd; + font-size: 8px; + text-shadow: 0 0 5px #00ff41; + } + + .action-buttons { + display: flex; + gap: 10px; + } + + .neon-btn { + background: #16213e; + border: 2px solid #00ff41; + border-radius: 0; + color: #00ff41; + padding: 8px 15px; + font-family: 'Press Start 2P', monospace; + font-size: 8px; + text-transform: uppercase; + cursor: pointer; + transition: all 0.3s ease; + text-shadow: 0 0 8px #00ff41; + letter-spacing: 1px; + } + + .neon-btn:hover { + background: rgba(0,255,65,0.1); + box-shadow: 0 0 20px rgb(0 255 65 / 60%); + animation: pixel-blink 0.5s infinite; + } + + .neon-btn.secondary { + border-color: #ff6b35; + color: #ff6b35; + text-shadow: 0 0 8px #ff6b35; + } + + .neon-btn.secondary:hover { + background: rgba(255, 107, 53, 0.1); + box-shadow: 0 0 20px rgb(255 107 53 / 60%); + } + + .script-manager-backdrop { + position: fixed !important; + top: 0 !important; + left: 0 !important; + width: 100% !important; + height: 100% !important; + background: rgba(0, 0, 0, 0.8); + z-index: 10000 !important; + backdrop-filter: blur(5px); + animation: backdrop-fade-in 0.3s ease-out; + } + + /* Loading animation */ + .loading-container { + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + height: 200px; + } + + .loading-spinner { + width: 40px; + height: 40px; + border: 3px solid #16213e; + border-top: 3px solid #00ff41; + border-radius: 0; + animation: neon-spin 1s linear infinite; + margin-bottom: 15px; + box-shadow: 0 0 20px rgb(0 255 65 / 50%); + } + + .loading-text { + color: #00ff41; + font-size: 8px; + text-shadow: 0 0 10px #00ff41; + text-transform: uppercase; + letter-spacing: 2px; + animation: text-pulse 1.5s ease-in-out infinite; + } + + /* Custom scrollbar */ + .script-manager-content::-webkit-scrollbar { + width: 12px; + } + + .script-manager-content::-webkit-scrollbar-track { + background: #16213e; + border: 1px solid #00ff41; + } + + .script-manager-content::-webkit-scrollbar-thumb { + background: #00ff41; + border-radius: 0; + box-shadow: 0 0 10px #00ff41; + } + + .script-manager-content::-webkit-scrollbar-thumb:hover { + background: #39ff14; + box-shadow: 0 0 15px #39ff14; + } + + /* Animations */ + @keyframes neon-pulse { + 0% { box-shadow: 0 0 30px rgb(0 255 65 / 50%), inset 0 0 30px rgb(0 255 65 / 10%), 0 0 0 1px #00ff41; } + 100% { box-shadow: 0 0 40px rgb(0 255 65 / 70%), inset 0 0 40px rgb(0 255 65 / 15%), 0 0 0 1px #00ff41; } + } + + @keyframes text-glow { + 0% { text-shadow: 0 0 15px #00ff41; } + 100% { text-shadow: 0 0 25px #00ff41, 0 0 35px #00ff41; } + } + + @keyframes pixel-blink { + 0%, 50% { opacity: 1; } + 51%, 100% { opacity: 0.7; } + } + + @keyframes scanline { + 0% { transform: translateY(-100%); } + 100% { transform: translateY(400px); } + } + + @keyframes neon-sweep { + 0% { opacity: 0; transform: translateX(-100%) translateY(-100%) rotate(45deg); } + 50% { opacity: 1; } + 100% { opacity: 0; transform: translateX(100%) translateY(100%) rotate(45deg); } + } + + @keyframes card-glow { + 0% { box-shadow: 0 0 25px rgb(0 255 65 / 60%), inset 0 0 25px rgb(0 255 65 / 15%); } + 100% { box-shadow: 0 0 35px rgb(0 255 65 / 80%), inset 0 0 35px rgb(0 255 65 / 20%); } + } + + @keyframes neon-spin { + 0% { transform: rotate(0deg); } + 100% { transform: rotate(360deg); } + } + + @keyframes text-pulse { + 0%, 100% { opacity: 1; } + 50% { opacity: 0.6; } + } + + @keyframes backdrop-fade-in { + 0% { opacity: 0; } + 100% { opacity: 1; } + } + + /* Responsive design */ + @media (max-width: 768px) { + .script-manager-container { + min-width: 90vw; + max-width: 95vw; + } + + .script-grid { + grid-template-columns: 1fr; + } + + .script-manager-title { + font-size: 10px; + } + } + `; + + // Execute script function - Fixed to work like extension popup + async function executeScript(scriptName) { + console.group(`%cšŸš€ Executing ${scriptName}`, 'color: #00ff41; font-weight: bold;'); + + try { + // Show loading in the UI + showLoading(`Launching ${scriptName}...`); + + // The Script Manager runs in MAIN world context and doesn't have direct Chrome API access + // Instead, we need to communicate back to the content script which can use Chrome APIs + console.log('%cšŸ”„ Script Manager context - delegating to content script', 'color: #ff6b35;'); + + // Create a custom event to communicate with the content script + const executeEvent = new CustomEvent('autobot-execute-script', { + detail: { scriptName: scriptName } + }); + + // Dispatch the event to the content script + window.dispatchEvent(executeEvent); + + // Show success immediately since we're delegating + console.log(`%cāœ… ${scriptName} execution delegated to content script`, 'color: #39ff14; font-weight: bold;'); + showSuccess(`${scriptName} execution started!`); + + // Auto-close after success + setTimeout(() => { + closeScriptManager(); + }, 1500); + + } catch (error) { + console.error(`%cāŒ Failed to execute ${scriptName}:`, 'color: #ff073a; font-weight: bold;', error); + showError(`Failed to launch ${scriptName}: ${error.message}`); + } finally { + console.groupEnd(); + } + } + + // UI Management functions + function showLoading(message) { + const container = document.getElementById('script-manager-content'); + if (!container) return; + + container.innerHTML = ` +
+
+
${message}
+
+ `; + } + + function showSuccess(message) { + const statusText = document.querySelector('.status-text'); + if (statusText) { + statusText.textContent = `āœ… ${message}`; + statusText.style.color = '#39ff14'; + statusText.style.textShadow = '0 0 10px #39ff14'; + } + } + + function showError(message) { + const statusText = document.querySelector('.status-text'); + if (statusText) { + statusText.textContent = `āŒ ${message}`; + statusText.style.color = '#ff073a'; + statusText.style.textShadow = '0 0 10px #ff073a'; + } + + // Reset the content to show scripts again + setTimeout(() => { + renderScripts(); + }, 3000); + } + + function renderScripts() { + const container = document.getElementById('script-manager-content'); + if (!container) return; + + const scriptGrid = AVAILABLE_SCRIPTS.map(script => ` +
+
+
${script.icon}
+

${script.displayName}

+
+

${script.description}

+ ${script.category} +
+ `).join(''); + + container.innerHTML = ` +
+ ${scriptGrid} +
+ `; + } + + // Close script manager + function closeScriptManager() { + const container = document.getElementById('script-manager-container'); + const backdrop = document.getElementById('script-manager-backdrop'); + + if (container) { + container.style.animation = 'neon-fade-out 0.3s ease-in forwards'; + setTimeout(() => { + container.remove(); + }, 300); + } + + if (backdrop) { + backdrop.style.animation = 'backdrop-fade-out 0.3s ease-in forwards'; + setTimeout(() => { + backdrop.remove(); + }, 300); + } + + // Remove ESC key listener + document.removeEventListener('keydown', handleEscKey); + + console.log('%cšŸ‘‹ Script Manager closed', 'color: #ff6b35;'); + } + + // ESC key handler + function handleEscKey(event) { + if (event.key === 'Escape') { + closeScriptManager(); + } + } + + // Main function to show script manager + function showScriptManager() { + // Remove any existing manager + const existing = document.getElementById('script-manager-container'); + if (existing) existing.remove(); + + const existingBackdrop = document.getElementById('script-manager-backdrop'); + if (existingBackdrop) existingBackdrop.remove(); + + console.log('%cšŸŽ® Opening Script Manager with Neon Theme', 'color: #00ff41; font-weight: bold;'); + + // Get icon URL for display + let iconUrl = ''; + try { + if (chrome && chrome.runtime && chrome.runtime.getURL) { + iconUrl = chrome.runtime.getURL('icons/icon32.png'); + console.log('šŸ“· Icon URL:', iconUrl); + } + } catch (e) { + console.log('Extension context not available for icon'); + } + + // Inject styles + if (!document.getElementById('script-manager-styles')) { + const styleElement = document.createElement('style'); + styleElement.id = 'script-manager-styles'; + styleElement.textContent = NEON_STYLES; + document.head.appendChild(styleElement); + } + + // Create backdrop + const backdrop = document.createElement('div'); + backdrop.id = 'script-manager-backdrop'; + backdrop.className = 'script-manager-backdrop'; + backdrop.addEventListener('click', closeScriptManager); + + // Create container + const container = document.createElement('div'); + container.id = 'script-manager-container'; + container.className = 'script-manager-container'; + + container.innerHTML = ` +
+
+ ${iconUrl ? `AutoBOT` : ''} +

⚔ WPlace AutoBOT Script Manager ⚔

+
+ +
+
+ +
+ + `; + + // Add to page + document.body.appendChild(backdrop); + document.body.appendChild(container); + + // Debug: Check positioning + console.log('%cšŸ” Script Manager Positioning Debug:', 'color: #ff6b35; font-weight: bold;'); + console.log(` - Container position: ${getComputedStyle(container).position}`); + console.log(` - Container top: ${getComputedStyle(container).top}`); + console.log(` - Container left: ${getComputedStyle(container).left}`); + console.log(` - Container transform: ${getComputedStyle(container).transform}`); + console.log(` - Container z-index: ${getComputedStyle(container).zIndex}`); + console.log(` - Backdrop z-index: ${getComputedStyle(backdrop).zIndex}`); + + // Render scripts + renderScripts(); + + // Add ESC key listener + document.addEventListener('keydown', handleEscKey); + + // Focus container for accessibility + container.focus(); + + console.log('%cāœ… Script Manager opened successfully', 'color: #39ff14; font-weight: bold;'); + } + + // Make functions globally available + window.executeScript = executeScript; + window.closeScriptManager = closeScriptManager; + window.showScriptManager = showScriptManager; + + // Auto-start the script manager + console.log('%cšŸŽÆ Auto-launching Script Manager...', 'color: #00ff41; font-weight: bold;'); + showScriptManager(); + + console.log('%cšŸš€ WPlace AutoBOT Script Manager Ready!', 'color: #39ff14; font-weight: bold; font-size: 16px;'); +})(); diff --git a/Extensions/auto-image-styles.css b/Extensions/auto-image-styles.css new file mode 100644 index 0000000..dff366b --- /dev/null +++ b/Extensions/auto-image-styles.css @@ -0,0 +1,2194 @@ +/* WPlace Auto-Image Bot - Unified CSS Styles (decoupled from JS) + Why: bring external CSS in-sync with the UI that Auto-Image.js renders, + fix layout (positions, widths, z-index), and ensure class names match JS + (e.g., .wplace-dragging) so buttons and panels behave correctly. */ + +/* ========================= */ +/* Theme tokens (CSS vars) */ +/* ========================= */ + +/* NOTE: Themes are now loaded dynamically from extension local resources + instead of using @import from CDN. The extension will inject theme CSS + directly when needed using window.applyTheme() function. + + Previous CDN imports (now removed): + - https://wplace-autobot.github.io/WPlace-AutoBOT/main/themes/classic.css + - https://wplace-autobot.github.io/WPlace-AutoBOT/main/themes/classic-light.css + - https://wplace-autobot.github.io/WPlace-AutoBOT/main/themes/acrylic.css + - https://wplace-autobot.github.io/WPlace-AutoBOT/main/themes/neon.css + - https://wplace-autobot.github.io/WPlace-AutoBOT/main/themes/neon-cyan.css + - https://wplace-autobot.github.io/WPlace-AutoBOT/main/themes/neon-light.css + + These are now loaded from local extension files for better performance + and offline functionality. */ + +/* Default :root CSS variables for 100% classic theme compliance */ +/* These ensure the bot works perfectly even if theme files fail to load */ +:root { + /* Classic theme colors - exact upstream main values */ + --wplace-primary: linear-gradient(135deg, #000 0%, #1a1a1a 100%); + --wplace-secondary: linear-gradient(135deg, #111 0%, #2a2a2a 100%); + --wplace-accent: #222; + --wplace-text: #fff; + --wplace-highlight: #775ce3; + --wplace-highlight-secondary: #d3a4ff; + --wplace-success: #0f0; + --wplace-error: #f00; + --wplace-warning: #fa0; + + /* UI properties */ + --wplace-radius: 12px; + --wplace-btn-radius: 16px; + --wplace-border-style: solid; + --wplace-border-width: 1px; + --wplace-border-color: #222; + --wplace-shadow: 0 8px 32px rgb(0 0 0 / 60%), 0 0 0 1px rgb(255 255 255 / 10%); + --wplace-backdrop: blur(10px); + --wplace-font: 'Segoe UI', roboto, sans-serif; + + /* Z-index layers */ + --wplace-z-overlay: 10000; + --wplace-z-alert: 10002; + --wplace-z-settings: 10002; + + /* Feature toggles */ + --wplace-scanline: 0; + --wplace-pixel-blink: 0; + + /* Icon colors */ + --wplace-icon-primary: #4facfe; + --wplace-icon-secondary: #00f2fe; + --wplace-icon-palette: #f093fb; + + /* Additional UI colors */ + --wplace-danger: #ff6a6a; + --wplace-danger-dark: #ff4757; + --wplace-muted-text: #fffb; + + /* Text variants */ + --wplace-text-secondary: rgb(255 255 255 / 90%); + --wplace-text-muted: rgb(255 255 255 / 70%); + --wplace-text-dim: rgb(255 255 255 / 60%); + --wplace-text-faded: rgb(255 255 255 / 80%); + + /* Background variants */ + --wplace-bg-input: rgb(255 255 255 / 15%); + --wplace-bg-subtle: rgb(255 255 255 / 10%); + --wplace-bg-faint: rgb(255 255 255 / 8%); + --wplace-bg-ghost: rgb(255 255 255 / 6%); + --wplace-bg-whisper: rgb(255 255 255 / 5%); + + /* Border variants */ + --wplace-border-subtle: rgb(255 255 255 / 20%); + --wplace-border-faint: rgb(255 255 255 / 15%); + --wplace-border-ghost: rgb(255 255 255 / 10%); + --wplace-border-ultra-faint: rgb(255 255 255 / 5%); + + /* Shadow variants */ + --wplace-shadow-drag: 0 12px 40px rgb(0 0 0 / 80%), 0 0 0 2px rgb(255 255 255 / 20%); + --wplace-shadow-notification: 0 4px 12px rgb(0 0 0 / 30%); + --wplace-shadow-slider-thumb: 0 3px 6px rgb(0 0 0 / 30%), 0 0 0 2px var(--wplace-icon-primary); + --wplace-shadow-slider-hover: 0 4px 8px rgb(0 0 0 / 40%), 0 0 0 3px var(--wplace-icon-primary); + + /* Animation colors */ + --wplace-pulse-start: rgb(0 255 0 / 70%); + --wplace-pulse-mid: rgb(0 255 0 / 0%); + --wplace-pulse-end: rgb(0 255 0 / 0%); + + /* Slider colors - defaults for classic compatibility */ + --wplace-slider-thumb-bg: white; + --wplace-slider-track-bg: linear-gradient(to right, + var(--wplace-icon-primary) 0%, + var(--wplace-icon-secondary) 100%); +} + +/* Theme classes are now defined in separate files */ +/* Classic theme: ./themes/classic.css */ +/* Neon theme: ./themes/neon.css */ + +/* ========================= */ +/* Core animations (shared) */ +/* ========================= */ +@keyframes neon-glow { + + 0%, + 100% { + text-shadow: + 0 0 5px currentcolor, + 0 0 10px currentcolor, + 0 0 15px currentcolor; + } + + 50% { + text-shadow: + 0 0 2px currentcolor, + 0 0 5px currentcolor, + 0 0 8px currentcolor; + } +} + +@keyframes pixel-blink { + + 0%, + 50% { + opacity: 1; + } + + 51%, + 100% { + opacity: 0.7; + } +} + +@keyframes scanline { + 0% { + transform: translateY(-100%); + } + + 100% { + transform: translateY(400px); + } +} + +@keyframes pulse { + 0% { + box-shadow: 0 0 0 0 var(--wplace-pulse-start); + } + + 70% { + box-shadow: 0 0 0 10px var(--wplace-pulse-mid); + } + + 100% { + box-shadow: 0 0 0 0 var(--wplace-pulse-end); + } +} + +@keyframes slide-in { + from { + transform: translateY(-10px); + opacity: 0; + } + + to { + transform: translateY(0); + opacity: 1; + } +} + +@keyframes slide-down { + from { + transform: translateX(-50%) translateY(-20px); + opacity: 0; + } + + to { + transform: translateX(-50%) translateY(0); + opacity: 1; + } +} + +@keyframes shimmer { + 0% { + transform: translateX(-100%); + } + + 100% { + transform: translateX(100%); + } +} + +/* ========================= */ +/* Main containers (fixed) */ +/* Align with JS structure */ +/* ========================= */ +#wplace-image-bot-container { + position: fixed; + top: 20px; + left: 20px; + width: 340px; + max-height: calc(100vh - 40px); + padding: 0; + z-index: 9998; + animation: slide-in 0.4s ease-out; + overflow: hidden; + transition: all 0.3s ease; + user-select: none; + + /* Default classic theme styling for 100% compliance */ + 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); +} + +#wplace-image-bot-container.wplace-dragging { + transition: none; + box-shadow: var(--wplace-shadow-drag); + transform: scale(1.02); + z-index: 9999; +} + +#wplace-image-bot-container.wplace-compact { + width: 300px; +} + +#wplace-image-bot-container.wplace-minimized { + width: 260px; + height: auto; + overflow: hidden; +} + +/* Stats container sits to the right of main (340 + 30 = 370) */ +#wplace-stats-container { + position: fixed; + top: 20px; + left: 370px; + width: 350px; + max-height: calc(100vh - 40px); + padding: 0; + z-index: 9997; + animation: slide-in 0.4s ease-out; + overflow-y: auto; + transition: all 0.3s ease; + user-select: none; + display: none; + + /* Default classic theme styling for 100% compliance */ + 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); +} + +#wplace-stats-container.wplace-dragging { + transition: none; +} + +/* Back-compat for legacy class (some earlier CSS used this) */ +.wplace-drag-active { + transition: none !important; + box-shadow: var(--wplace-shadow-drag) !important; + transform: scale(1.02) !important; + z-index: 9999 !important; +} + +/* ========================= */ +/* Header and content blocks */ +/* ========================= */ +.wplace-header { + padding: 8px 12px; + font-size: 13px; + font-weight: 700; + display: flex; + justify-content: space-between; + align-items: center; + cursor: move; + user-select: none; + border-bottom: 1px solid var(--wplace-border-ghost); + transition: background 0.2s ease; + position: relative; + z-index: 2; + + /* Default styling for 100% classic compliance */ + background: var(--wplace-secondary); + color: var(--wplace-highlight); + text-shadow: 0 1px 2px rgb(0 0 0 / 50%); +} + +.wplace-header-title { + display: flex; + align-items: center; + gap: 6px; +} + +.wplace-header-controls { + display: flex; + gap: 6px; +} + +.wplace-header-btn { + border: none; + cursor: pointer; + width: 18px; + height: 18px; + padding: 0; + font-size: 10px; + display: flex; + align-items: center; + justify-content: center; + transition: all 0.2s; + + /* Default styling for 100% classic compliance */ + background: rgb(255 255 255 / 10%); + color: var(--wplace-text); + border-radius: 4px; + font-family: var(--wplace-font); +} + +.wplace-header-btn:hover { + transform: scale(1.1); + + /* Default styling for 100% classic compliance */ + background: var(--wplace-accent); + color: var(--wplace-text); +} + +.wplace-content { + display: block; + position: relative; + z-index: 2; + padding: 12px; +} + +.wplace-content.wplace-hidden { + display: none; +} + +/* Sections */ +.wplace-status-section { + margin-bottom: 6px; + padding: 8px; + + /* Theme-specific styling applied via theme files */ +} + +.wplace-section { + margin-bottom: 6px; + padding: 8px; + + /* Theme-specific styling applied via theme files */ +} + +.wplace-section-title { + font-size: 11px; + font-weight: 600; + margin-bottom: 8px; + display: flex; + justify-content: space-between; + align-items: center; + cursor: pointer; + gap: 6px; + text-transform: uppercase; + letter-spacing: 1px; + + /* Theme-specific styling applied via theme files */ +} + +.wplace-section-title i.arrow { + transition: transform 0.3s ease; +} + +.wplace-section.collapsed .wplace-section-title i.arrow { + transform: rotate(-90deg); +} + +/* ========================= */ +/* Controls and buttons */ +/* ========================= */ +.wplace-controls { + display: flex; + flex-direction: column; + gap: 8px; +} + +.wplace-cooldown-control { + margin-top: 8px; +} + +.wplace-section.collapsed .wplace-controls, +.wplace-section.collapsed .wplace-cooldown-control { + max-height: 0; + opacity: 0; + pointer-events: none; +} + +.wplace-row { + display: grid; + grid-template-columns: 1fr 1fr; + gap: 8px; +} + +.wplace-row.single { + grid-template-columns: 1fr; +} + +.wplace-btn { + padding: 8px 12px; + border: none; + cursor: pointer; + display: flex; + align-items: center; + justify-content: center; + gap: 6px; + font-size: 11px; + transition: all 0.3s ease; + position: relative; + overflow: hidden; + font-weight: 500; + + /* Theme-specific styling applied via theme files */ +} + +.wplace-btn:disabled { + opacity: 0.5; + cursor: not-allowed; + transform: none !important; + + /* Theme-specific styling applied via theme files */ +} + +.wplace-btn::before { + content: ''; + position: absolute; + top: 0; + left: -100%; + width: 100%; + height: 100%; + transition: left 0.5s ease; + + /* Theme-specific styling applied via theme files */ +} + +.wplace-btn:disabled::before { + display: none; +} + +.wplace-btn:hover:not(:disabled)::before { + left: 100%; +} + +.wplace-btn:hover:not(:disabled) { + transform: translateY(-1px); + + /* Theme-specific styling applied via theme files */ +} + +.wplace-btn:active:not(:disabled) { + transform: translateY(0); +} + +/* Button variants moved to theme files */ + +/* ========================= */ +/* Stats and progress */ +/* ========================= */ +.wplace-stats { + margin-bottom: 8px; + padding: 8px; + + /* Theme-specific styling applied via theme files */ +} + +.wplace-stat-item { + display: flex; + justify-content: space-between; + padding: 4px 0; + font-size: 11px; + border-bottom: 1px solid var(--wplace-border-ultra-faint); +} + +.wplace-stat-item:last-child { + border-bottom: none; +} + +.wplace-stat-label { + display: flex; + align-items: center; + gap: 6px; + opacity: 0.9; + font-size: 10px; +} + +.wplace-stat-value { + font-weight: 600; + + /* Theme-specific styling applied via theme files */ +} + +.wplace-colors-section { + margin-top: 10px; + padding-top: 8px; + border-top: 1px solid var(--wplace-border-ultra-faint); +} + +.wplace-stat-colors-grid { + display: grid; + grid-template-columns: repeat(auto-fill, minmax(16px, 1fr)); + gap: 4px; + margin-top: 8px; + padding: 4px; + max-height: 80px; + overflow-y: auto; + + /* Theme-specific styling applied via theme files */ +} + +.wplace-colors-placeholder { + text-align: center; + color: #888; + padding: 20px; + font-style: italic; +} + +.wplace-cooldown-value { + font-weight: bold; + min-width: 20px; + text-align: center; + display: inline-block; +} + +.wplace-stat-color-swatch { + width: 16px; + height: 16px; + + /* Theme-specific styling applied via theme files */ +} + +/* Progress */ +.wplace-progress { + width: 100%; + margin: 8px 0; + overflow: hidden; + height: 6px; + position: relative; + + /* Theme-specific styling applied via theme files */ +} + +.wplace-progress-bar { + height: 6px; + transition: width 0.5s ease; + position: relative; + + /* Theme-specific styling applied via theme files */ +} + +.wplace-progress-bar::after { + content: ''; + position: absolute; + inset: 0; + animation: shimmer 2s infinite; + + /* Theme-specific styling applied via theme files */ +} + +/* ========================= */ +/* Status blocks */ +/* ========================= */ +.wplace-status { + text-align: center; + position: relative; + overflow: hidden; + padding: 6px; + border: 1px solid; + font-size: 11px; + + /* Theme-specific styling applied via theme files */ +} + +/* Status styling moved to theme files */ + +/* ========================= */ +/* Resize dialog */ +/* ========================= */ +.resize-container { + display: none; + position: fixed; + top: 50%; + left: 50%; + transform: translate(-50%, -50%); + padding: 20px; + z-index: 10000; + width: 90%; + max-width: 700px; + max-height: 90%; + overflow: auto; + + /* Theme-specific styling applied via theme files */ +} + +.resize-preview-wrapper { + display: flex; + justify-content: center; + align-items: center; + margin: 15px 0; + height: 300px; + overflow: hidden; + + /* Theme-specific styling applied via theme files */ +} + +.resize-canvas-stack { + position: relative; + transform-origin: center center; + display: inline-block; +} + +.resize-base-canvas, +.resize-mask-canvas { + position: absolute; + left: 0; + top: 0; + image-rendering: -moz-crisp-edges; + image-rendering: pixelated; +} + +.resize-mask-canvas { + pointer-events: auto; +} + +.resize-tools { + display: flex; + gap: 8px; + align-items: center; + margin-top: 8px; + font-size: 12px; +} + +/* Missing button hover styles */ +.resize-tools button { + padding: 6px 10px; + border-radius: 6px; + border: 1px solid var(--wplace-border-subtle); + background: var(--wplace-bg-ghost); + color: var(--wplace-text); + cursor: pointer; + + /* Theme-specific styling applied via theme files */ +} + +/* Button active states moved to theme files */ + +.resize-controls { + display: grid; + grid-template-columns: 1fr 1fr; + gap: 15px; + align-items: center; +} + +.resize-slider { + width: 100%; + height: 4px; + border: none; + outline: none; + -webkit-appearance: none; + + /* Theme-specific styling applied via theme files */ +} + +.resize-zoom-controls { + grid-column: 1 / -1; + display: flex; + align-items: center; + gap: 8px; + margin-top: 15px; + flex-wrap: wrap; +} + +.resize-buttons { + display: flex; + gap: 10px; + justify-content: center; + margin-top: 20px; +} + + +/* ========================= */ +/* Color grid */ +/* ========================= */ +.wplace-color-grid { + display: grid; + grid-template-columns: repeat(auto-fill, minmax(60px, 1fr)); + gap: 10px; + padding-top: 8px; + max-height: 300px; + overflow-y: auto; +} + +.wplace-color-item { + display: flex; + flex-direction: column; + align-items: center; + gap: 4px; +} + +.wplace-color-item-name { + font-size: 9px; + text-align: center; + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; + width: 100%; + + /* Theme-specific styling applied via theme files */ +} + +.wplace-color-swatch { + width: 22px; + height: 22px; + cursor: pointer; + transition: + transform 0.1s ease, + box-shadow 0.2s ease; + position: relative; + margin: 0 auto; + + /* Theme-specific styling applied via theme files */ +} + +.wplace-color-swatch.unavailable { + border-style: dashed; + cursor: not-allowed; + + /* Theme-specific styling applied via theme files */ +} + +.wplace-color-swatch:hover { + transform: scale(1.1); + z-index: 1; +} + +.wplace-color-swatch:not(.active) { + opacity: 0.3; + + /* Theme-specific styling applied via theme files */ +} + +.wplace-color-swatch.unavailable:not(.active) { + opacity: 0.2; + + /* Theme-specific styling applied via theme files */ +} + +.wplace-color-swatch.active::after { + content: 'āœ”'; + position: absolute; + top: 50%; + left: 50%; + transform: translate(-50%, -50%); + color: var(--wplace-text); + font-size: 12px; + font-weight: bold; + + /* Theme-specific styling applied via theme files */ +} + +.wplace-color-divider { + border: none; + height: 1px; + margin: 8px 0; + + /* Theme-specific styling applied via theme files */ +} + +/* ========================= */ +/* Cooldown controls */ +/* ========================= */ +.wplace-cooldown-control label { + font-size: 11px; + margin-bottom: 4px; + display: block; +} + +.wplace-slider-container { + display: flex; + align-items: center; + gap: 8px; +} + +.wplace-slider { + flex: 1; + -webkit-appearance: none; + appearance: none; + height: 4px; + outline: none; + + /* Theme-specific styling applied via theme files */ +} + +.wplace-slider::-webkit-slider-thumb { + -webkit-appearance: none; + width: 14px; + height: 14px; + cursor: pointer; + + /* Theme-specific styling applied via theme files */ +} + +/* ========================= */ +/* Settings container (base) */ +/* ========================= */ +#wplace-settings-container { + position: fixed; + top: 50%; + left: 50%; + transform: translate(-50%, -50%); + padding: 0; + z-index: 10002; + display: none; + min-width: 420px; + max-width: 480px; + overflow: hidden; + + /* Default classic theme styling for 100% compliance */ + 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); +} + +#wplace-settings-container.show { + display: block; + animation: settings-slide-in 0.4s ease-out; +} + +@keyframes settings-slide-in { + from { + opacity: 0; + transform: translate(-50%, -50%) scale(0.9); + } + + to { + opacity: 1; + transform: translate(-50%, -50%) scale(1); + } +} + +@keyframes settings-fade-out { + from { + opacity: 1; + transform: translate(-50%, -50%) scale(1); + } + + to { + opacity: 0; + transform: translate(-50%, -50%) scale(0.9); + } +} + +.wplace-settings { + padding: 16px; + max-height: 400px; + overflow-y: auto; +} + +.wplace-setting-section { + margin-bottom: 20px; + padding: 12px; +} + +/* ========================= */ +/* Form controls */ +/* ========================= */ +.wplace-select { + width: 100%; + padding: 8px 12px; + font-size: 14px; + margin-bottom: 10px; +} + +.wplace-select:focus { + outline: none; +} + +.wplace-description { + font-size: 12px; + opacity: 0.8; + line-height: 1.4; +} + +/* Speed controls */ +.wplace-speed-control { + margin-top: 12px; + padding: 12px; +} + +.wplace-speed-label { + display: flex; + align-items: center; + margin-bottom: 8px; + font-size: 13px; + font-weight: 600; +} + +.wplace-speed-label i { + margin-right: 6px; +} + + +.wplace-speed-slider { + flex: 1; + height: 8px; + outline: none; + -webkit-appearance: none; + appearance: none; + cursor: pointer; + background: var(--wplace-slider-track-bg); + border-radius: 4px; +} + + +.wplace-speed-slider::-webkit-slider-thumb { + -webkit-appearance: none; + appearance: none; + width: 18px; + height: 18px; + cursor: pointer; + + /* Theme-specific styling applied via theme files */ +} + +.wplace-speed-slider::-moz-range-thumb { + width: 18px; + height: 18px; + cursor: pointer; + + /* Theme-specific styling applied via theme files */ +} + +.wplace-speed-display { + display: flex; + align-items: center; + gap: 4px; + min-width: 90px; + justify-content: flex-end; +} + +/* ========================= */ +/* Turnstile overlay */ +/* ========================= */ +/* Hidden invisible widget host (token generator) */ +.wplace-turnstile-hidden { + position: fixed !important; + left: -99999px !important; + top: -99999px !important; + width: 1px !important; + height: 1px !important; + pointer-events: none !important; + opacity: 0 !important; + visibility: hidden !important; + z-index: -99999 !important; + overflow: hidden !important; +} + +/* Visible overlay (interactive fallback) */ +.wplace-turnstile-overlay { + position: fixed !important; + bottom: 20px !important; + right: 20px !important; + z-index: 99999 !important; + padding: 20px !important; + min-width: 300px !important; + max-width: 400px !important; + + /* Theme-specific styling applied via theme files */ +} + +.wplace-turnstile-title { + font: + 600 12px/1.3 'Segoe UI', + sans-serif !important; + margin-bottom: 8px !important; + opacity: 0.9 !important; + + /* Theme-specific styling applied via theme files */ +} + +.wplace-turnstile-host { + width: 100% !important; + min-height: 70px !important; +} + +.wplace-turnstile-hide-btn { + position: absolute !important; + top: 6px !important; + right: 6px !important; + font-size: 11px !important; + background: transparent !important; + padding: 2px 6px !important; + cursor: pointer !important; + transition: background 0.2s ease !important; + + /* Theme-specific styling applied via theme files */ +} + +.wplace-turnstile-hide-btn:hover { + /* Theme-specific styling applied via theme files */ +} + +/* ========================= */ +/* Alert system (used by JS) */ +/* ========================= */ +.wplace-alert-base { + position: fixed; + top: 20px; + left: 50%; + transform: translateX(-50%); + padding: 12px 20px; + color: var(--wplace-text); + font-weight: 600; + z-index: 10002; + max-width: 400px; + text-align: center; + animation: slide-down 0.3s ease-out; + box-shadow: var(--wplace-shadow-notification); + + /* Theme-specific styling applied via theme files */ +} + +/* Alert styling moved to theme files */ + +/* ========================= */ +/* Modal overlay helpers */ +/* ========================= */ +.wplace-overlay { + position: fixed; + top: 0; + left: 0; + width: 100%; + height: 100%; + z-index: 100002; + display: none; + + /* Theme-specific styling applied via theme files */ +} + +.wplace-overlay.show { + display: block; +} + +/* Overlay visibility helpers */ +.wplace-overlay-hidden { + display: none !important; +} + +.wplace-overlay-visible { + display: block !important; +} + +/* ========================= */ +/* Responsive tweaks */ +/* ========================= */ +@media (width <=768px) { + #wplace-image-bot-container { + left: 10px; + width: calc(100vw - 20px); + max-height: calc(100vh - 20px); + } + + #wplace-stats-container { + display: none !important; + /* hide secondary panel on small screens */ + } + + .wplace-alert-base { + max-width: 90vw; + margin: 0 5vw; + } + + .wplace-turnstile-overlay { + bottom: 10px !important; + right: 10px !important; + left: 10px !important; + min-width: auto !important; + } +} + +/* Auto light/dark support moved to theme files */ + +/* ===================================== */ +/* Settings container variants (optional) */ +/* ===================================== */ +.wplace-settings-container-base { + position: fixed; + top: 50%; + left: 50%; + transform: translate(-50%, -50%); + z-index: 100001; + display: none; + min-width: 400px; + max-width: 500px; + max-height: 100vh; + overflow-y: auto; + animation: slide-in 0.3s ease-out; + border: 1px solid var(--wplace-border-ghost); + padding: 0; + + /* Theme-specific styling applied via theme files */ +} + +.wplace-settings-container-base.show { + display: block; +} + +/* ===================================== */ +/* Utility styles */ +/* ===================================== */ +.wplace-paint-effect { + animation: pulse 0.5s ease-out; +} + +.wplace-settings-error { + /* Theme-specific styling applied via theme files */ +} + +.wplace-stats-container.hidden { + display: none; +} + +/* Theme-specific effects are now in separate theme files */ + +/* ===================================== */ +/* Settings Dialog Styles */ +/* ===================================== */ + +/* Settings content container */ +.wplace-settings-content { + padding: 25px 25px 0; + max-height: 67vh; + overflow-y: auto; +} + +/* Settings section containers */ +.wplace-settings-section { + margin-bottom: 25px; +} + +/* Section labels */ +.wplace-settings-section-label { + margin-bottom: 12px; + color: var(--wplace-text); + font-weight: 500; + font-size: 16px; + display: flex; + align-items: center; + gap: 8px; +} + +/* Icon colors */ +.wplace-icon-key { + font-size: 16px; +} + +.wplace-icon-robot { + font-size: 16px; +} + +.wplace-icon-speed { + font-size: 16px; +} + +.wplace-icon-bell { + font-size: 16px; +} + +.wplace-icon-palette { + font-size: 16px; +} + +.wplace-icon-globe { + font-size: 16px; +} + +.wplace-icon-paint { + font-size: 16px; +} + +/* Section wrapper styling */ +.wplace-settings-section-wrapper { + padding: 18px; + + /* Theme-specific styling applied via theme files */ +} + +/* Select dropdowns */ +.wplace-settings-select { + width: 100%; + padding: 12px 16px; + font-size: 14px; + outline: none; + cursor: pointer; + transition: all 0.3s ease; + font-family: inherit; + + /* Theme-specific styling applied via theme files */ +} + +.wplace-settings-option { + padding: 10px; + + /* Theme-specific styling applied via theme files */ +} + +/* Settings description text */ +.wplace-settings-description { + font-size: 12px; + color: var(--wplace-text-muted); + margin: 8px 0 0; +} + +/* Batch Controls */ +.wplace-batch-controls { + padding: 18px; + margin-bottom: 15px; + + /* Theme-specific styling applied via theme files */ +} + +.wplace-random-batch-controls { + display: none; +} + +/* Speed Slider Container */ +.wplace-speed-slider-container { + display: flex; + align-items: center; + gap: 15px; + margin-bottom: 10px; +} + + +.wplace-speed-value { + min-width: 100px; + text-align: center; + padding: 8px 12px; + font-weight: bold; + font-size: 13px; + + /* Theme-specific styling applied via theme files */ +} + +.wplace-speed-labels { + display: flex; + justify-content: space-between; + color: var(--wplace-text-muted); + font-size: 11px; + margin-top: 8px; +} + +/* Random Batch Controls */ +.wplace-random-batch-grid { + display: grid; + grid-template-columns: 1fr 1fr; + gap: 15px; +} + +.wplace-random-batch-label { + display: block; + color: var(--wplace-text-faded); + font-size: 12px; + margin-bottom: 8px; +} + +.wplace-icon-min { + color: var(--wplace-icon-primary); + margin-right: 4px; +} + +.wplace-icon-max { + color: var(--wplace-icon-secondary); + margin-right: 4px; +} + +.wplace-settings-number-input { + width: 100%; + padding: 10px 12px; + background: var(--wplace-bg-input); + color: var(--wplace-text); + border: 1px solid var(--wplace-border-subtle); + border-radius: 8px; + font-size: 13px; + outline: none; +} + +.wplace-random-batch-description { + font-size: 11px; + color: var(--wplace-text-dim); + margin: 8px 0 0; + text-align: center; +} + +/* Speed Control Toggle */ +.wplace-speed-control-toggle { + display: flex; + align-items: center; + gap: 8px; + color: var(--wplace-text); +} + +.wplace-speed-checkbox { + cursor: pointer; +} + +/* Overlay Settings */ +.wplace-overlay-opacity-control { + margin-bottom: 15px; +} + +.wplace-overlay-opacity-header { + display: flex; + justify-content: space-between; + align-items: center; + margin-bottom: 8px; +} + +.wplace-overlay-opacity-label { + font-weight: 500; + font-size: 13px; +} + +.wplace-overlay-opacity-value { + min-width: 40px; + text-align: center; + background: var(--wplace-accent); + color: var(--wplace-text); + padding: 4px 8px; + border-radius: 6px; + font-size: 12px; + border: var(--wplace-border-width) var(--wplace-border-style) var(--wplace-border-color); +} + +.wplace-overlay-opacity-slider { + width: 100%; + -webkit-appearance: none; + height: 8px; + outline: none; + cursor: pointer; +} + +.wplace-settings-toggle { + display: flex; + align-items: center; + justify-content: space-between; + cursor: pointer; +} + +.wplace-settings-toggle-title { + font-weight: 500; +} + +.wplace-settings-toggle-description { + font-size: 12px; + margin: 4px 0 0; + color: var(--wplace-muted-text); +} + +.wplace-settings-checkbox { + cursor: pointer; + width: 20px; + height: 20px; + flex-shrink: 0; + accent-color: var(--wplace-highlight); +} + +/* Notifications */ +.wplace-notifications-wrapper { + display: flex; + flex-direction: column; + gap: 10px; +} + +.wplace-notification-toggle { + display: flex; + align-items: center; + justify-content: space-between; +} + +.wplace-notification-checkbox { + width: 18px; + height: 18px; + cursor: pointer; +} + +.wplace-notification-interval { + display: flex; + align-items: center; + gap: 10px; +} + +.wplace-notification-interval-input { + width: 70px; + padding: 6px 8px; + border-radius: 6px; + border: 1px solid var(--wplace-border-subtle); + background: var(--wplace-bg-faint); + color: var(--wplace-text); +} + +.wplace-notification-buttons { + display: flex; + gap: 10px; +} + +.wplace-notification-perm-btn, +.wplace-notification-test-btn { + flex: 1; +} + +/* Settings Footer */ +.wplace-settings-footer { + border-top: 1px solid var(--wplace-border-ghost); + padding: 20px; + position: sticky; + bottom: 0; + background: var(--wplace-secondary); +} + +/* Settings select option styling moved to theme files */ +/* Settings description styling moved to theme files */ + +.wplace-settings-apply-btn { + width: 100%; + border: none; + padding: 10px 16px; + cursor: pointer; + font-weight: 500; + transition: all 0.3s ease; + display: flex; + align-items: center; + gap: 8px; + justify-content: center; + + /* Theme-specific styling applied via theme files */ +} + +/* ===================================== */ +/* Resize Dialog Styles */ +/* ===================================== */ + +/* Resize dialog title */ +.resize-dialog-title { + margin-top: 0; + color: var(--wplace-text); +} + +/* Resize control labels */ +.resize-control-label { + display: block; + margin-bottom: 8px; + font-size: 14px; +} + +.resize-checkbox-label { + display: flex; + align-items: center; + margin-bottom: 8px; + font-size: 14px; + gap: 8px; +} + +/* Zoom controls */ + +.resize-zoom-btn { + padding: 4px 8px; +} + +.resize-zoom-slider { + max-width: 220px; +} + +.resize-zoom-value { + margin-left: 6px; + min-width: 48px; + text-align: right; + opacity: 0.85; + font-size: 12px; +} + +.resize-camera-help { + font-size: 11px; + opacity: 0.75; + margin-left: auto; +} + +/* Canvas positioning */ +.resize-pan-stage { + position: relative; + width: 100%; + height: 100%; + overflow: hidden; +} + +.resize-canvas-positioned { + position: absolute; + left: 0; + top: 0; + transform-origin: top left; +} + +/* Resize tools */ +.resize-tools-container { + display: flex; + gap: 10px; + flex-wrap: wrap; + align-items: center; +} + +.resize-brush-controls { + display: flex; + flex-direction: column; + gap: 8px; +} + +.resize-brush-control { + display: flex; + align-items: center; + gap: 6px; + justify-content: space-between; +} + +.resize-tool-label { + font-size: 12px; + opacity: 0.85; +} + +.resize-tool-input-group { + display: flex; + align-items: center; + gap: 6px; +} + +.resize-tool-slider { + width: 120px; +} + +.resize-tool-value { + font-size: 12px; + opacity: 0.85; + min-width: 18px; + text-align: center; +} + +.resize-mode-controls { + display: flex; + align-items: center; + gap: 6px; +} + +.resize-mode-group { + display: flex; + gap: 6px; +} + +.resize-mode-btn, +.resize-clear-btn, +.resize-invert-btn { + padding: 4px 8px; + font-size: 12px; +} + +.resize-shortcut-help { + opacity: 0.8; + font-size: 12px; +} + +/* Color palette section */ +.resize-color-palette-section { + margin-top: 15px; +} + +.resize-color-toggle-label { + display: flex; + align-items: center; + gap: 8px; + font-size: 12px; +} + +.resize-color-checkbox { + cursor: pointer; +} + +/* Advanced color section */ +.resize-advanced-color-section { + margin-top: 15px; +} + +.resize-advanced-controls { + display: flex; + flex-direction: column; + gap: 10px; +} + +.resize-advanced-label { + display: flex; + flex-direction: column; + gap: 4px; + font-size: 12px; +} + +.resize-advanced-label-text { + font-weight: 600; +} + +.resize-advanced-select { + padding: 6px 8px; + border-radius: 6px; + border: 1px solid var(--wplace-border-faint); + background: var(--wplace-bg-whisper); + color: var(--wplace-text); +} + +.resize-advanced-toggle { + display: flex; + align-items: center; + justify-content: space-between; + font-size: 12px; +} + +.resize-advanced-toggle-content { + flex: 1; +} + +.resize-advanced-description { + margin-top: 2px; + opacity: 0.65; +} + +.resize-advanced-checkbox { + width: 18px; + height: 18px; + cursor: pointer; +} + +.resize-chroma-weight-control { + display: flex; + flex-direction: column; + gap: 4px; +} + +.resize-chroma-weight-header { + display: flex; + justify-content: space-between; + font-size: 11px; + margin-bottom: 4px; +} + +.resize-chroma-weight-value { + background: var(--wplace-bg-faint); + padding: 2px 6px; + border-radius: 4px; +} + +.resize-chroma-weight-slider { + width: 100%; +} + +.resize-threshold-controls { + display: grid; + grid-template-columns: 1fr 1fr; + gap: 10px; +} + +.resize-threshold-label { + display: flex; + flex-direction: column; + gap: 4px; + font-size: 12px; +} + +.resize-threshold-input { + padding: 6px 8px; + border-radius: 6px; + border: 1px solid var(--wplace-border-faint); + background: var(--wplace-bg-whisper); + color: var(--wplace-text); +} + +.resize-reset-advanced-btn { + background: linear-gradient(135deg, var(--wplace-danger), var(--wplace-danger-dark)); + font-size: 11px; +} + +/* ===================================== */ +/* Additional styles for exact upstream match */ +/* ===================================== */ + +/* Overlay styles exact match */ +.resize-overlay { + position: fixed; + top: 0; + left: 0; + width: 100%; + height: 100%; + z-index: 9999; + display: none; + + /* Theme-specific styling applied via theme files */ +} + +/* Settings animations - removed duplicate, handled above */ + +/* Settings Header Styling */ +.wplace-settings-header { + background: var(--wplace-accent); + padding: 20px; + border-bottom: 1px solid var(--wplace-border-color); + cursor: move; +} + +.wplace-settings-title-wrapper { + display: flex; + justify-content: space-between; + align-items: center; +} + +.wplace-settings-title { + margin: 0; + color: var(--wplace-text); + font-size: 20px; + font-weight: 300; + display: flex; + align-items: center; + gap: 10px; +} + +.wplace-settings-icon { + font-size: 18px; + color: var(--wplace-highlight); + animation: spin 2s linear infinite; +} + +.wplace-settings-close-btn { + background: rgb(34 34 34 / 40%); + color: var(--wplace-text); + border: 1px solid var(--wplace-border-color); + border-radius: 50%; + width: 32px; + height: 32px; + cursor: pointer; + display: flex; + align-items: center; + justify-content: center; + transition: all 0.3s ease; + font-size: 14px; + font-weight: 300; +} + +.wplace-settings-close-btn:hover { + background: rgb(255 0 0 / 40%); + transform: scale(1.1); +} + +@keyframes spin { + from { + transform: rotate(0deg); + } + + to { + transform: rotate(360deg); + } +} + +/* Font styling corrections */ +.resize-controls label { + font-size: 12px; + color: var(--wplace-text); +} + + +/* Additional precision fixes for exact upstream main matching */ +/* Status default styling moved to theme files */ + +/* Duplicate settings section removed - all settings styling moved to theme files */ + +/* Settings header styling moved to theme files */ +/* All remaining settings styling moved to theme files */ + + +/* Icon styling moved to theme files */ +.wplace-icon-eye { + font-size: 16px; +} + +/* Overlay Settings Controls */ +.wplace-overlay-opacity-slider::-webkit-slider-thumb { + -webkit-appearance: none; + appearance: none; + width: 18px; + height: 18px; + border-radius: 50%; + background: var(--wplace-slider-thumb-bg); + box-shadow: var(--wplace-shadow-slider-thumb); + cursor: pointer; + transition: all 0.2s ease; +} + +.wplace-overlay-opacity-slider::-webkit-slider-thumb:hover { + transform: scale(1.2); + box-shadow: var(--wplace-shadow-slider-hover); +} + +/* Coordinate Generation Controls */ +.wplace-icon-route { + color: var(--wplace-icon-primary); + font-size: 16px; +} + +.wplace-icon-table { + color: var(--wplace-icon-palette); + margin-right: 6px; +} + +.wplace-icon-compass { + color: var(--wplace-icon-secondary); + margin-right: 6px; +} + +/* Pixel Filter Controls */ +.wplace-pixel-filter-controls { + padding: 18px; + margin-bottom: 15px; + display: flex; + flex-direction: column; + gap: 12px; + + /* Theme-specific styling applied via theme files */ +} + +/* Snake Pattern Controls */ +.wplace-snake-pattern-controls { + padding: 18px; + margin-bottom: 15px; + + /* Theme-specific styling applied via theme files */ +} + +/* Block Size Controls */ +.wplace-block-size-controls { + padding: 18px; + margin-bottom: 15px; + + /* Theme-specific styling applied via theme files */ +} + +.wplace-block-size-grid { + display: grid; + grid-template-columns: 1fr 1fr; + gap: 15px; +} + +.wplace-block-size-label { + display: block; + color: var(--wplace-text-faded); + font-size: 12px; + margin-bottom: 8px; +} + +.wplace-icon-width { + color: var(--wplace-icon-primary); + margin-right: 4px; +} + +.wplace-icon-height { + color: var(--wplace-icon-secondary); + margin-right: 4px; +} + +.wplace-block-size-description { + font-size: 11px; + color: var(--wplace-text-dim); + margin: 8px 0 0; + text-align: center; +} + +/* Random Block Controls */ +.wplace-shuffle-block-size-controls { + display: none; +} + +/* Batch Mode Controls */ +.wplace-mode-selection { + margin-bottom: 15px; +} + +.wplace-mode-label { + display: block; + margin-bottom: 8px; + color: var(--wplace-text-secondary); + font-weight: 500; + font-size: 14px; +} + +.wplace-icon-dice { + 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.3s ease, box-shadow 0.5s 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); + box-shadow: 0 0 15px rgba(46, 204, 113, 0.6), + 0 0 25px rgba(46, 204, 113, 0.4), + 0 0 35px rgba(46, 204, 113, 0.2); + animation: currentAccountGlow 2s ease-in-out infinite alternate; +} + +@keyframes currentAccountGlow { + from { + box-shadow: 0 0 15px rgba(46, 204, 113, 0.6), + 0 0 25px rgba(46, 204, 113, 0.4), + 0 0 35px rgba(46, 204, 113, 0.2); + } + to { + box-shadow: 0 0 20px rgba(46, 204, 113, 0.8), + 0 0 30px rgba(46, 204, 113, 0.6), + 0 0 40px rgba(46, 204, 113, 0.4); + } +} + +@keyframes currentAccountNumberPulse { + from { + box-shadow: 0 0 10px rgba(46, 204, 113, 0.8); + transform: scale(1); + } + to { + box-shadow: 0 0 15px rgba(46, 204, 113, 1.0); + transform: scale(1.05); + } +} + +.wplace-account-item.next-in-sequence { + background: var(--wplace-warning)15; + border-left: 3px solid var(--wplace-warning); +} + +.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); +} + +/* ===================================== */ +/* Hide browser default spinner buttons */ +/* ===================================== */ + +/* Hide spinner buttons for all number inputs in the extension */ +input[type="number"]::-webkit-outer-spin-button, +input[type="number"]::-webkit-inner-spin-button { + -webkit-appearance: none; + margin: 0; +} + +/* Firefox support */ +input[type="number"] { + -moz-appearance: textfield; + appearance: textfield; +} + +/* Specifically target extension number inputs */ +.wplace-settings-number-input::-webkit-outer-spin-button, +.wplace-settings-number-input::-webkit-inner-spin-button { + -webkit-appearance: none; + margin: 0; +} + +.wplace-settings-number-input { + -moz-appearance: textfield; + appearance: textfield; +} + +/* Hide spinners on batch control inputs */ +.wplace-random-batch-grid input[type="number"]::-webkit-outer-spin-button, +.wplace-random-batch-grid input[type="number"]::-webkit-inner-spin-button { + -webkit-appearance: none; + margin: 0; +} + +.wplace-random-batch-grid input[type="number"] { + -moz-appearance: textfield; + appearance: textfield; +} + +/* Hide spinners on block size inputs */ +.wplace-block-size-grid input[type="number"]::-webkit-outer-spin-button, +.wplace-block-size-grid input[type="number"]::-webkit-inner-spin-button { + -webkit-appearance: none; + margin: 0; +} + +.wplace-block-size-grid input[type="number"] { + -moz-appearance: textfield; + appearance: textfield; +} + +/* Hide spinners on threshold inputs */ +.resize-threshold-input::-webkit-outer-spin-button, +.resize-threshold-input::-webkit-inner-spin-button { + -webkit-appearance: none; + margin: 0; +} + +.resize-threshold-input { + -moz-appearance: textfield; + appearance: textfield; +} + +/* Hide spinners on notification interval inputs */ +.wplace-notification-interval-input::-webkit-outer-spin-button, +.wplace-notification-interval-input::-webkit-inner-spin-button { + -webkit-appearance: none; + margin: 0; +} + +.wplace-notification-interval-input { + -moz-appearance: textfield; + appearance: textfield; +} \ No newline at end of file diff --git a/Extensions/background.js b/Extensions/background.js new file mode 100644 index 0000000..fa41233 --- /dev/null +++ b/Extensions/background.js @@ -0,0 +1,921 @@ +// Background service worker for WPlace AutoBOT Extension + +// Test resource loading on extension starto +chrome.runtime.onStartup.addListener(async () => { + console.log('šŸš€ WPlace AutoBOT Extension started'); + const resources = await loadExtensionResources(); + console.log('šŸ“¦ Initial resource test:', resources); +}); + +chrome.runtime.onInstalled.addListener(async () => { + console.log('šŸ”§ WPlace AutoBOT Extension installed'); + const resources = await loadExtensionResources(); + console.log('šŸ“¦ Installation resource test:', resources); +}); + +chrome.runtime.onMessage.addListener((request, sender, sendResponse) => { + if (request.action === 'executeScript') { + // Get tabId from sender or requests + const tabId = request.tabId || sender.tab?.id; + + if (!tabId) { + sendResponse({ success: false, error: 'Could not determine target tab' }); + return; + } + + // Use IIFE for async handlings + (async () => { + try { + await executeLocalScript(request.scriptName, tabId); + sendResponse({ success: true }); + } catch (error) { + sendResponse({ success: false, error: error.message }); + } + })(); + + return true; // Important: indicates async response + } +}); + +async function executeLocalScript(scriptName, tabId) { + try { + console.log(`Loading script: ${scriptName}`); + + // Check if we need to inject dependencies first for Auto-Image.js or Art-Extractor.js + if (scriptName === 'Auto-Image.js' || scriptName === 'Art-Extractor.js') { + console.log(`šŸ”‘ ${scriptName} detected, ensuring dependencies are loaded first...`); + + try { + // First inject token-manager.js + const tokenManagerUrl = chrome.runtime.getURL('scripts/token-manager.js'); + const tokenManagerResponse = await fetch(tokenManagerUrl); + + if (tokenManagerResponse.ok) { + const tokenManagerCode = await tokenManagerResponse.text(); + console.log('šŸ”‘ Token manager loaded, injecting first...'); + + // Execute token manager first + await chrome.scripting.executeScript({ + target: { tabId: tabId }, + world: "MAIN", + func: (code) => { + console.log('%cšŸ”‘ Loading WPlace Token Manager...', 'color: #4ade80; font-weight: bold;'); + const script = document.createElement('script'); + script.textContent = code; + document.head.appendChild(script); + script.remove(); + console.log('%cāœ… Token Manager loaded successfully', 'color: #4ade80; font-weight: bold;'); + }, + args: [tokenManagerCode] + }); + + console.log('āœ… Token manager injected successfully'); + } else { + console.warn('āš ļø Could not load token-manager.js, proceeding without it'); + } + + // Then inject image-processor.js + const imageProcessorUrl = chrome.runtime.getURL('scripts/image-processor.js'); + const imageProcessorResponse = await fetch(imageProcessorUrl); + + if (imageProcessorResponse.ok) { + const imageProcessorCode = await imageProcessorResponse.text(); + console.log('šŸ–¼ļø Image processor loaded, injecting second...'); + + // Execute image processor second + await chrome.scripting.executeScript({ + target: { tabId: tabId }, + world: "MAIN", + func: (code) => { + console.log('%cšŸ–¼ļø Loading WPlace Image Processor...', 'color: #3b82f6; font-weight: bold;'); + const script = document.createElement('script'); + script.textContent = code; + document.head.appendChild(script); + script.remove(); + console.log('%cāœ… Image Processor loaded successfully', 'color: #3b82f6; font-weight: bold;'); + }, + args: [imageProcessorCode] + }); + + console.log('āœ… Image processor injected successfully'); + } else { + console.warn('āš ļø Could not load image-processor.js, proceeding without it'); + } + + // Then inject overlay-manager.js + const overlayManagerUrl = chrome.runtime.getURL('scripts/overlay-manager.js'); + const overlayManagerResponse = await fetch(overlayManagerUrl); + + if (overlayManagerResponse.ok) { + const overlayManagerCode = await overlayManagerResponse.text(); + console.log('šŸŽØ Overlay manager loaded, injecting third...'); + + // Execute overlay manager third + await chrome.scripting.executeScript({ + target: { tabId: tabId }, + world: "MAIN", + func: (code) => { + console.log('%cšŸŽØ Loading WPlace Overlay Manager...', 'color: #8b5cf6; font-weight: bold;'); + const script = document.createElement('script'); + script.textContent = code; + document.head.appendChild(script); + script.remove(); + console.log('%cāœ… Overlay Manager loaded successfully', 'color: #8b5cf6; font-weight: bold;'); + }, + args: [overlayManagerCode] + }); + + console.log('āœ… Overlay manager injected successfully'); + } else { + console.warn('āš ļø Could not load overlay-manager.js, proceeding without it'); + } + + // Finally inject utils-manager.js + const utilsManagerUrl = chrome.runtime.getURL('scripts/utils-manager.js'); + const utilsManagerResponse = await fetch(utilsManagerUrl); + + if (utilsManagerResponse.ok) { + const utilsManagerCode = await utilsManagerResponse.text(); + console.log('šŸ› ļø Utils manager loaded, injecting fourth...'); + + // Execute utils manager fourth + await chrome.scripting.executeScript({ + target: { tabId: tabId }, + world: "MAIN", + func: (code) => { + console.log('%cšŸ› ļø Loading WPlace Utils Manager...', 'color: #f59e0b; font-weight: bold;'); + const script = document.createElement('script'); + script.textContent = code; + document.head.appendChild(script); + script.remove(); + console.log('%cāœ… Utils Manager loaded successfully', 'color: #f59e0b; font-weight: bold;'); + }, + args: [utilsManagerCode] + }); + + console.log('āœ… Utils manager injected successfully'); + } else { + console.warn('āš ļø Could not load utils-manager.js, proceeding without it'); + } + + // Small delay to ensure dependencies are fully initialized + await new Promise(resolve => setTimeout(resolve, 200)); + } catch (error) { + console.warn('āš ļø Error loading dependencies:', error); + } + } + + // Determine script path - Script-manager files are in root, others in scripts/ + let scriptUrl; + if (scriptName === 'Script-manager.js' || scriptName === 'Script-manager-fixed.js') { + scriptUrl = chrome.runtime.getURL(scriptName); + } else { + scriptUrl = chrome.runtime.getURL(`scripts/${scriptName}`); + } + + const response = await fetch(scriptUrl); + + if (!response.ok) { + throw new Error(`Failed to load script: ${response.status} ${response.statusText}`); + } + + const scriptCode = await response.text(); + console.log(`Script loaded: ${scriptCode.length} characters`); + + // Load theme and language resources + const resources = await loadExtensionResources(); + + // Execute in MAIN world context (bypasses CSP) + await chrome.scripting.executeScript({ + target: { tabId: 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']; + const styleElement = document.createElement('style'); + 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`); + console.log(` šŸŽÆ Target: as + + `; + + // Add event listeners + container.querySelector('#art-extractor-close').addEventListener('click', closeArtExtractor); + // Add event listeners + container.querySelector('#art-extractor-close').addEventListener('click', closeArtExtractor); + container.querySelector('#export-autofix').addEventListener('click', () => exportToJSON('autofix')); + container.querySelector('#export-autoimage').addEventListener('click', () => exportToJSON('autoimage')); + container.querySelector('#start-selecting').addEventListener('click', startPixelCapture); + container.querySelector('#clear-selection').addEventListener('click', clearSelection); + + // Set up filename input with .json enforcement + const filenameInput = container.querySelector('#filename-input'); + if (filenameInput) { + // Initialize with default value + filenameInput.value = '.json'; + + // Add event listeners for .json enforcement + filenameInput.addEventListener('input', handleFilenameInput); + filenameInput.addEventListener('keydown', preventJsonDeletion); + filenameInput.addEventListener('focus', function() { + // Position cursor before .json on focus + const nameLength = this.value.length - 5; + if (nameLength >= 0) { + this.setSelectionRange(nameLength, nameLength); + } + }); + + // Set initial cursor position + setTimeout(() => { + filenameInput.setSelectionRange(0, 0); + }, 100); + } + + // Add to page + document.body.appendChild(container); + state.ui = container; + + console.log('āœ… Art Extractor UI created'); + + // Auto-start pixel capture immediately + console.log('šŸŽÆ Auto-starting pixel capture...'); + setTimeout(() => { + startPixelCapture(); + }, 500); + updateUI(); + + } catch (error) { + console.error('āŒ Failed to create UI:', error); + throw error; + } + } + + // UI update function + function updateUI() { + if (!state.ui) return; + + const statusElement = state.ui.querySelector('#current-status'); + const areaElement = state.ui.querySelector('#area-info'); + const pixelElement = state.ui.querySelector('#pixel-count'); + const exportAutofixBtn = state.ui.querySelector('#export-autofix'); + const exportAutoimageBtn = state.ui.querySelector('#export-autoimage'); + + // Update status with Neon Cyan styling + if (state.isCapturing) { + statusElement.textContent = `CAPTURING (${state.capturedPixels.length}/${state.requiredPixels})`; + statusElement.style.color = '#EA9C00'; + statusElement.style.textShadow = '0 0 8px #EA9C00'; + } else if (state.isScanning) { + statusElement.textContent = 'SCANNING...'; + statusElement.style.color = '#39ff14'; + statusElement.style.textShadow = '0 0 8px #39ff14'; + } else if (state.selectionArea) { + statusElement.textContent = 'READY TO EXPORT'; + statusElement.style.color = '#39ff14'; + statusElement.style.textShadow = '0 0 8px #39ff14'; + } else { + statusElement.textContent = 'READY'; + statusElement.style.color = '#81DCF7'; + statusElement.style.textShadow = '0 0 5px #81DCF7'; + } + + // Update area info with uppercase styling + if (state.selectionArea) { + areaElement.textContent = `${state.selectionArea.width}Ɨ${state.selectionArea.height} PX`; + } else if (state.capturedPixels.length > 0) { + areaElement.textContent = `${state.capturedPixels.length}/${state.requiredPixels} CAPTURED`; + } else { + areaElement.textContent = 'NONE'; + } + + // Update pixel count with progress info + if (state.isScanning && state.totalPixels > 0) { + const progress = Math.round((state.scannedPixels.length / state.totalPixels) * 100); + pixelElement.textContent = `${state.scannedPixels.length}/${state.totalPixels} (${progress}%)`; + } else { + pixelElement.textContent = state.scannedPixels.length.toString(); + } + + // Update export button states + const hasScannedData = state.scannedPixels && state.scannedPixels.length > 0; + + if (exportAutofixBtn) { + exportAutofixBtn.disabled = !hasScannedData; + if (!exportAutofixBtn.disabled) { + exportAutofixBtn.style.opacity = '1'; + exportAutofixBtn.style.cursor = 'pointer'; + } else { + exportAutofixBtn.style.opacity = '0.5'; + exportAutofixBtn.style.cursor = 'not-allowed'; + } + } + + if (exportAutoimageBtn) { + exportAutoimageBtn.disabled = !hasScannedData; + if (!exportAutoimageBtn.disabled) { + exportAutoimageBtn.style.opacity = '1'; + exportAutoimageBtn.style.cursor = 'pointer'; + } else { + exportAutoimageBtn.style.opacity = '0.5'; + exportAutoimageBtn.style.cursor = 'not-allowed'; + } + } + } + + // Control functions + function startPixelCapture() { + console.log('šŸŽÆ Starting Auto-Guard style pixel capture mode...'); + + state.isCapturing = true; + state.capturedPixels = []; + state.selectionArea = null; + state.scannedPixels = []; + + // Clear any existing overlay + if (state.overlayManager) { + state.overlayManager.disable(); + } + + // Set up Auto-Guard style pixel paint monitoring with fetch interception + const setupSuccess = setupPixelCapture(); + + if (setupSuccess) { + updateUI(); + showAlert('šŸŽÆ Paint a pixel at the UPPER-LEFT corner of the area you want to extract', 'info'); + + // Set timeout for capture process + setTimeout(() => { + if (state.isCapturing && state.capturedPixels.length === 0) { + showAlert('ā° No pixels captured yet. Try painting a pixel or use "Enter Coordinates Manually"', 'warning'); + } + }, 15000); + } else { + showAlert('āš ļø Could not set up automatic capture. Use "Enter Coordinates Manually"', 'warning'); + updateUI(); + } + } + + function clearSelection() { + console.log('🧹 Clearing pixel capture...'); + + state.isCapturing = false; + state.capturedPixels = []; + state.selectionArea = null; + state.scannedPixels = []; + + // Restore original fetch function + restoreFetch(); + + // Disable overlay + if (state.overlayManager) { + state.overlayManager.disable(); + } + + updateUI(); + showAlert('Pixel capture cleared', 'info'); + } + + function closeArtExtractor() { + console.log('šŸ‘‹ Closing Art Extractor...'); + + // Stop capturing + state.isCapturing = false; + + // Restore original fetch function + restoreFetch(); + + // Clean up overlay + if (state.overlayManager) { + state.overlayManager.disable(); + } + + // Remove UI + if (state.ui) { + state.ui.remove(); + } + + // Reset state + Object.assign(state, { + isCapturing: false, + capturedPixels: [], + selectionArea: null, + ui: null, + overlayManager: null, + isScanning: false, + scannedPixels: [], + paintEventListener: null, + lastPaintEvent: null + }); + + console.log('āœ… Art Extractor closed'); + } + + // Main initialization + async function initialize() { + console.log('šŸš€ Initializing Art Extractor...'); + + try { + // Create UI immediately - don't wait for dependencies + console.log('šŸŽØ Creating UI first...'); + await createUI(); + + // Then try to set up dependencies in background + setTimeout(async () => { + try { + // Wait for Auto-Image overlay manager with timeout + if (!window.autoImageOverlayManager) { + console.log('ā³ Waiting for Auto-Image overlay manager...'); + await new Promise((resolve, reject) => { + let attempts = 0; + const maxAttempts = 50; // 5 seconds timeout + const check = () => { + attempts++; + if (window.autoImageOverlayManager) { + console.log('āœ… Auto-Image overlay manager found'); + resolve(); + } else if (attempts >= maxAttempts) { + console.warn('āš ļø Auto-Image overlay manager not found, proceeding without it'); + resolve(); // Continue anyway + } else { + setTimeout(check, 100); + } + }; + check(); + }); + } + + // Set up pixel capture monitoring + try { + const captureReady = setupPixelCapture(); + if (captureReady) { + console.log('āœ… Pixel capture monitoring ready'); + } else { + console.warn('āš ļø Pixel capture monitoring setup failed'); + } + } catch (error) { + console.warn('āš ļø Pixel capture error:', error); + } + + } catch (error) { + console.warn('āš ļø Background initialization error:', error); + } + }, 1000); + + console.log('āœ… Art Extractor initialized successfully'); + showAlert('Art Extractor ready! UI loaded - dependencies loading in background.', 'success'); + + } catch (error) { + console.error('āŒ Failed to initialize Art Extractor:', error); + + // Try to create UI anyway as fallback + try { + await createUI(); + showAlert(`Art Extractor loaded with limited functionality: ${error.message}`, 'warning'); + } catch (uiError) { + console.error('āŒ Failed to create UI:', uiError); + showAlert(`Critical initialization failure: ${error.message}`, 'error'); + } + } + } + + // Start initialization with additional error handling + try { + await initialize(); + } catch (error) { + console.error('āŒ Critical error during Art Extractor initialization:', error); + + // Final fallback - try to show minimal UI + try { + const container = document.createElement('div'); + container.innerHTML = ` +
+ āŒ Art Extractor failed to load: ${error.message} + +
+ `; + document.body.appendChild(container); + } catch (e) { + console.error('āŒ Even fallback UI failed:', e); + } + } + + // Expose globally for extension integration + window.WPlaceArtExtractor = { + state, + initialize, + closeArtExtractor, + exportToJSON, + scanSelectedArea + }; + + console.log('šŸŽØ WPlace Art Extractor loaded successfully!'); +})(); \ No newline at end of file diff --git a/Extensions/scripts/Auto-Farm.js b/Extensions/scripts/Auto-Farm.js new file mode 100644 index 0000000..ee1e5db --- /dev/null +++ b/Extensions/scripts/Auto-Farm.js @@ -0,0 +1,650 @@ +// eslint-disable-next-line prettier/prettier +; (async () => { + const CONFIG = { + START_X: 742, + START_Y: 1148, + PIXELS_PER_LINE: 100, + DELAY: 1000, + THEME: { + primary: '#000000', + secondary: '#111111', + accent: '#222222', + text: '#ffffff', + highlight: '#775ce3', + success: '#00ff00', + error: '#ff0000', + }, + }; + + const state = { + running: false, + paintedCount: 0, + charges: { count: 0, max: 80, cooldownMs: 30000 }, + userInfo: null, + lastPixel: null, + minimized: false, + menuOpen: false, + language: 'en', + autoRefresh: true, + pausedForManual: false, + }; + + const sleep = (ms) => new Promise((r) => setTimeout(r, ms)); + const waitForSelector = async (selector, interval = 200, timeout = 5000) => { + const start = Date.now(); + while (Date.now() - start < timeout) { + const el = document.querySelector(selector); + if (el) return el; + await sleep(interval); + } + return null; + }; + + const originalFetch = window.fetch; + let capturedCaptchaToken = null; + window.fetch = async (url, options = {}) => { + if (typeof url === 'string' && url.includes('https://backend.wplace.live/s0/pixel/')) { + try { + const payload = JSON.parse(options.body || '{}'); + if (payload.t) { + console.log('āœ… CAPTCHA Token Captured:', payload.t); + capturedCaptchaToken = payload.t; + if (state.pausedForManual) { + state.pausedForManual = false; + state.running = true; + updateUI( + state.language === 'pt' ? 'šŸš€ Pintura reiniciada!' : 'šŸš€ Farm resumed!', + 'success' + ); + paintLoop(); + } + } + } catch (e) {} + } + return originalFetch(url, options); + }; + + const fetchAPI = async (url, options = {}) => { + try { + const res = await fetch(url, { + credentials: 'include', + ...options, + }); + return await res.json(); + } catch (e) { + return null; + } + }; + + const getRandomPosition = () => ({ + x: Math.floor(Math.random() * CONFIG.PIXELS_PER_LINE), + y: Math.floor(Math.random() * CONFIG.PIXELS_PER_LINE), + }); + + const paintPixel = async (x, y) => { + const randomColor = Math.floor(Math.random() * 31) + 1; + const url = `https://backend.wplace.live/s0/pixel/${CONFIG.START_X}/${CONFIG.START_Y}`; + const payload = JSON.stringify({ + coords: [x, y], + colors: [randomColor], + t: capturedCaptchaToken, + }); + try { + const res = await originalFetch(url, { + method: 'POST', + headers: { 'Content-Type': 'text/plain;charset=UTF-8' }, + credentials: 'include', + body: payload, + }); + if (res.status === 403) { + console.error('āŒ 403 Forbidden. CAPTCHA token might be invalid or expired.'); + capturedCaptchaToken = null; + stoppedForToken = true; + return 'token_error'; + } + const data = await res.json(); + return data; + } catch (e) { + return null; + } + }; + + const getCharge = async () => { + const data = await fetchAPI('https://backend.wplace.live/me'); + if (data) { + state.userInfo = data; + state.charges = { + count: Math.floor(data.charges.count), + max: Math.floor(data.charges.max), + cooldownMs: data.charges.cooldownMs, + }; + if (state.userInfo.level) { + state.userInfo.level = Math.floor(state.userInfo.level); + } + } + return state.charges; + }; + + const detectUserLocation = async () => { + try { + const response = await fetch('https://ipapi.co/json/'); + const data = await response.json(); + if (data.country === 'BR') { + state.language = 'pt'; + } else if (data.country === 'US') { + state.language = 'en'; + } else { + state.language = 'en'; + } + } catch { + state.language = 'en'; + } + }; + + const paintLoop = async () => { + while (state.running) { + const { count, cooldownMs } = state.charges; + + if (count < 1) { + updateUI( + state.language === 'pt' + ? `āŒ› Sem cargas. Esperando ${Math.ceil(cooldownMs / 1000)}s...` + : `āŒ› No charges. Waiting ${Math.ceil(cooldownMs / 1000)}s...`, + 'status' + ); + await sleep(cooldownMs); + await getCharge(); + continue; + } + + const randomPos = getRandomPosition(); + const paintResult = await paintPixel(randomPos.x, randomPos.y); + if (paintResult === 'token_error') { + if (state.autoRefresh) { + await getCharge(); + if (state.charges.count < 2) { + if (!state.pausedForManual) { + updateUI( + state.language === 'pt' + ? '⚔ Aguardando pelo menos 2 cargas para auto-refresh...' + : 'Waiting for at least 2 charges for auto-refresh...', + 'status' + ); + state.pausedForManual = true; + } + while (state.charges.count < 2) { + await sleep(60000); + await getCharge(); + updateStats(); + } + state.pausedForManual = false; + } + updateUI( + state.language === 'pt' + ? 'āŒ Token expirado. Aguardando elemento Paint...' + : 'āŒ CAPTCHA token expired. Waiting for Paint button...', + 'error' + ); + const mainPaintBtn = await waitForSelector( + 'button.btn.btn-primary.btn-lg, button.btn-primary.sm\\:btn-xl' + ); + if (mainPaintBtn) mainPaintBtn.click(); + await sleep(500); + updateUI( + state.language === 'pt' ? 'Selecionando transparente...' : 'Selecting transparent...', + 'status' + ); + const transBtn = await waitForSelector('button#color-0'); + if (transBtn) transBtn.click(); + await sleep(500); + const canvas = await waitForSelector('canvas'); + if (canvas) { + canvas.setAttribute('tabindex', '0'); + canvas.focus(); + const rect = canvas.getBoundingClientRect(); + const centerX = Math.round(rect.left + rect.width / 2); + const centerY = Math.round(rect.top + rect.height / 2); + const moveEvt = new MouseEvent('mousemove', { + clientX: centerX, + clientY: centerY, + bubbles: true, + }); + canvas.dispatchEvent(moveEvt); + const keyDown = new KeyboardEvent('keydown', { + key: ' ', + code: 'Space', + bubbles: true, + }); + const keyUp = new KeyboardEvent('keyup', { key: ' ', code: 'Space', bubbles: true }); + canvas.dispatchEvent(keyDown); + canvas.dispatchEvent(keyUp); + } + await sleep(500); + updateUI( + state.language === 'pt' ? 'Confirmando pintura...' : 'Confirming paint...', + 'status' + ); + let confirmBtn = await waitForSelector( + 'button.btn.btn-primary.btn-lg, button.btn.btn-primary.sm\\:btn-xl' + ); + if (!confirmBtn) { + const allPrimary = Array.from(document.querySelectorAll('button.btn-primary')); + confirmBtn = allPrimary.length ? allPrimary[allPrimary.length - 1] : null; + } + confirmBtn?.click(); + } else { + // insufficient charges or auto-refresh disabled + if (state.autoRefresh && state.charges.count < 2) { + updateUI( + state.language === 'pt' + ? '⚔ Cargas insuficientes para auto-refresh. Por favor, clique manualmente.' + : 'Insufficient charges for auto-refresh. Please click manually.', + 'error' + ); + } + if (!state.pausedForManual) { + updateUI( + state.language === 'pt' + ? 'Auto-refresh desativado. Por favor, clique no botĆ£o pintura manualmente.' + : 'Auto-refresh disabled. Please click the Paint button manually.', + 'status' + ); + state.pausedForManual = true; + } + state.running = false; + return; + } + await sleep(1000); + continue; + } + + if (paintResult?.painted === 1) { + state.paintedCount++; + state.lastPixel = { + x: CONFIG.START_X + randomPos.x, + y: CONFIG.START_Y + randomPos.y, + time: new Date(), + }; + state.charges.count--; + + document.getElementById('paintEffect').style.animation = 'pulse 0.5s'; + setTimeout(() => { + document.getElementById('paintEffect').style.animation = ''; + }, 500); + + updateUI(state.language === 'pt' ? 'āœ… Pixel pintado!' : 'āœ… Pixel painted!', 'success'); + } else { + updateUI(state.language === 'pt' ? 'āŒ Falha ao pintar' : 'āŒ Failed to paint', 'error'); + } + + await sleep(CONFIG.DELAY); + updateStats(); + } + }; + + const createUI = () => { + if (state.menuOpen) return; + state.menuOpen = true; + + const fontAwesome = document.createElement('link'); + fontAwesome.rel = 'stylesheet'; + fontAwesome.href = 'https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css'; + document.head.appendChild(fontAwesome); + + const style = document.createElement('style'); + style.textContent = ` + @keyframes pulse { + 0% { box-shadow: 0 0 0 0 rgba(0, 255, 0, 0.7); }n + 70% { box-shadow: 0 0 0 10px rgba(0, 255, 0, 0); } + 100% { box-shadow: 0 0 0 0 rgba(0, 255, 0, 0); } + } + @keyframes slide-in { + from { transform: translateY(20px); opacity: 0; } + to { transform: translateY(0); opacity: 1; } + } + .wplace-bot-panel { + position: fixed; + top: 20px; + right: 20px; + width: 250px; + background: ${CONFIG.THEME.primary}; + border: 1px solid ${CONFIG.THEME.accent}; + border-radius: 8px; + padding: 0; + box-shadow: 0 5px 15px rgba(0,0,0,0.5); + z-index: 9999; + font-family: 'Segoe UI', Roboto, sans-serif; + color: ${CONFIG.THEME.text}; + animation: slide-in 0.4s ease-out; + overflow: hidden; + } + .wplace-header { + padding: 12px 15px; + background: ${CONFIG.THEME.secondary}; + color: ${CONFIG.THEME.highlight}; + font-size: 16px; + font-weight: 600; + display: flex; + justify-content: space-between; + align-items: center; + cursor: move; + user-select: none; + } + .wplace-header-title { + display: flex; + align-items: center; + gap: 8px; + } + .wplace-header-controls { + display: flex; + gap: 10px; + } + .wplace-header-btn { + background: none; + border: none; + color: ${CONFIG.THEME.text}; + cursor: pointer; + opacity: 0.7; + transition: opacity 0.2s; + } + .wplace-header-btn:hover { + opacity: 1; + } + .wplace-content { + padding: 15px; + display: ${state.minimized ? 'none' : 'block'}; + } + .wplace-controls { + display: flex; + gap: 10px; + margin-bottom: 15px; + } + .wplace-btn { + flex: 1; + padding: 10px; + border: none; + border-radius: 6px; + font-weight: 600; + cursor: pointer; + display: flex; + align-items: center; + justify-content: center; + gap: 8px; + transition: all 0.2s; + } + .wplace-btn:hover { + transform: translateY(-2px); + } + .wplace-btn-primary { + background: ${CONFIG.THEME.accent}; + color: white; + } + .wplace-btn-stop { + background: ${CONFIG.THEME.error}; + color: white; + } + .wplace-stats { + background: ${CONFIG.THEME.secondary}; + padding: 12px; + border-radius: 6px; + margin-bottom: 15px; + } + .wplace-stat-item { + display: flex; + justify-content: space-between; + padding: 6px 0; + font-size: 14px; + } + .wplace-stat-label { + display: flex; + align-items: center; + gap: 6px; + opacity: 0.8; + } + .wplace-status { + padding: 8px; + border-radius: 4px; + text-align: center; + font-size: 13px; + } + .status-default { + background: rgba(255,255,255,0.1); + } + .status-success { + background: rgba(0, 255, 0, 0.1); + color: ${CONFIG.THEME.success}; + } + .status-error { + background: rgba(255, 0, 0, 0.1); + color: ${CONFIG.THEME.error}; + } + #paintEffect { + position: absolute; + top: 0; + left: 0; + width: 100%; + height: 100%; + pointer-events: none; + border-radius: 8px; + } + `; + document.head.appendChild(style); + + const translations = { + pt: { + title: 'WPlace Auto-Farm', + start: 'Iniciar', + stop: 'Parar', + ready: 'Pronto para comeƧar', + user: 'UsuĆ”rio', + pixels: 'Pixels', + charges: 'Cargas', + level: 'Level', + }, + en: { + title: 'WPlace Auto-Farm', + start: 'Start', + stop: 'Stop', + ready: 'Ready to start', + user: 'User', + pixels: 'Pixels', + charges: 'Charges', + level: 'Level', + }, + }; + + const t = translations[state.language] || translations.en; + + const panel = document.createElement('div'); + panel.className = 'wplace-bot-panel'; + panel.innerHTML = ` +
+
+
+ + ${t.title} +
+
+ +
+
+
+
+ + +
+ +
+
+
+
${state.language === 'pt' ? 'Carregando...' : 'Loading...'}
+
+
+
+ +
+ ${t.ready} +
+
+ `; + + document.body.appendChild(panel); + + const header = panel.querySelector('.wplace-header'); + let pos1 = 0, + pos2 = 0, + pos3 = 0, + pos4 = 0; + + header.onmousedown = dragMouseDown; + + function dragMouseDown(e) { + if (e.target.closest('.wplace-header-btn')) return; + + e = e || window.event; + e.preventDefault(); + pos3 = e.clientX; + pos4 = e.clientY; + document.onmouseup = closeDragElement; + document.onmousemove = elementDrag; + } + + function elementDrag(e) { + e = e || window.event; + e.preventDefault(); + pos1 = pos3 - e.clientX; + pos2 = pos4 - e.clientY; + pos3 = e.clientX; + pos4 = e.clientY; + panel.style.top = panel.offsetTop - pos2 + 'px'; + panel.style.left = panel.offsetLeft - pos1 + 'px'; + } + + function closeDragElement() { + document.onmouseup = null; + document.onmousemove = null; + } + + const toggleBtn = panel.querySelector('#toggleBtn'); + const minimizeBtn = panel.querySelector('#minimizeBtn'); + const statusText = panel.querySelector('#statusText'); + const content = panel.querySelector('.wplace-content'); + const statsArea = panel.querySelector('#statsArea'); + + toggleBtn.addEventListener('click', () => { + state.running = !state.running; + + if (state.running && !capturedCaptchaToken) { + updateUI( + state.language === 'pt' + ? 'āŒ Token nĆ£o capturado. Clique em qualquer pixel primeiro.' + : 'āŒ CAPTCHA token not captured. Please click any pixel manually first.', + 'error' + ); + state.running = false; + return; + } + + if (state.running) { + toggleBtn.innerHTML = ` ${t.stop}`; + toggleBtn.classList.remove('wplace-btn-primary'); + toggleBtn.classList.add('wplace-btn-stop'); + updateUI( + state.language === 'pt' ? 'šŸš€ Pintura iniciada!' : 'šŸš€ Painting started!', + 'success' + ); + paintLoop(); + } else { + toggleBtn.innerHTML = ` ${t.start}`; + toggleBtn.classList.add('wplace-btn-primary'); + toggleBtn.classList.remove('wplace-btn-stop'); + statsArea.innerHTML = ''; + updateUI(state.language === 'pt' ? 'ā¹ļø Parado' : 'ā¹ļø Stopped', 'default'); + } + }); + + minimizeBtn.addEventListener('click', () => { + state.minimized = !state.minimized; + content.style.display = state.minimized ? 'none' : 'block'; + minimizeBtn.innerHTML = ``; + }); + + const autoRefreshCheckbox = panel.querySelector('#autoRefreshCheckbox'); + autoRefreshCheckbox.addEventListener('change', () => { + state.autoRefresh = autoRefreshCheckbox.checked; + }); + + window.addEventListener('beforeunload', () => { + state.menuOpen = false; + }); + }; + + window.updateUI = (message, type = 'default') => { + const statusText = document.querySelector('#statusText'); + if (statusText) { + statusText.textContent = message; + statusText.className = `wplace-status status-${type}`; + statusText.style.animation = 'none'; + void statusText.offsetWidth; + statusText.style.animation = 'slide-in 0.3s ease-out'; + } + }; + + window.updateStats = async () => { + await getCharge(); + const statsArea = document.querySelector('#statsArea'); + if (statsArea) { + const t = { + pt: { + user: 'UsuĆ”rio', + pixels: 'Pixels', + charges: 'Cargas', + level: 'Level', + }, + en: { + user: 'User', + pixels: 'Pixels', + charges: 'Charges', + level: 'Level', + }, + }[state.language] || { + user: 'User', + pixels: 'Pixels', + charges: 'Charges', + level: 'Level', + }; + + statsArea.innerHTML = ` +
+
${t.user}
+
${state.userInfo.name}
+
+
+
${t.pixels}
+
${state.paintedCount}
+
+
+
${t.charges}
+
${Math.floor(state.charges.count)}/${Math.floor(state.charges.max)}
+
+
+
${t.level}
+
${state.userInfo?.level || '0'}
+
+ `; + } + }; + + await detectUserLocation(); + createUI(); + await getCharge(); + updateStats(); +})(); diff --git a/Extensions/scripts/Auto-Image.js b/Extensions/scripts/Auto-Image.js new file mode 100644 index 0000000..91e7b23 --- /dev/null +++ b/Extensions/scripts/Auto-Image.js @@ -0,0 +1,8199 @@ +// ==UserScript== +// @name WPlaceBot +// @namespace http://tampermonkey.net/ +// @version 2025-08-08.3 +// @description Bot +// @author Wbot +// @match https://wplace.live/* +// @grant none +// @icon +// ==/UserScript== +localStorage.removeItem("lp"); + +// Fallback translation function for when utils manager isn't loaded +function getText(key, params) { + // Try to get translation from loadedTranslations + try { + if (window.loadedTranslations && window.loadedTranslations[key]) { + let text = window.loadedTranslations[key]; + if (params) { + Object.keys(params).forEach(paramKey => { + text = text.replace(new RegExp(`{{${paramKey}}}`, 'g'), params[paramKey]); + }); + } + return text; + } + + // Try with state.language if available + if (window.state && window.state.language && window.loadedTranslations && window.loadedTranslations[window.state.language] && window.loadedTranslations[window.state.language][key]) { + let text = window.loadedTranslations[window.state.language][key]; + if (params) { + Object.keys(params).forEach(paramKey => { + text = text.replace(new RegExp(`{{${paramKey}}}`, 'g'), params[paramKey]); + }); + } + return text; + } + } catch (error) { + console.warn('Error in getText fallback:', error); + } + + return key; // Fallback to key if no translation found +} + +; (async () => { + // Prevent multiple instances of this script from running + if (window.WPLACE_AUTO_IMAGE_LOADED) { + console.log('%cāš ļø Auto-Image script already loaded, skipping duplicate execution', 'color: #ff9800; font-weight: bold;'); + return; + } + window.WPLACE_AUTO_IMAGE_LOADED = true; + + console.log('%cšŸš€ WPlace AutoBot Auto-Image Starting...', 'color: #00ff41; font-weight: bold; font-size: 16px;'); + + // CONFIGURATION CONSTANTS + const CONFIG = { + COOLDOWN_DEFAULT: 31000, + TRANSPARENCY_THRESHOLD: 100, + WHITE_THRESHOLD: 250, + LOG_INTERVAL: 10, + PAINTING_SPEED: { + MIN: 1, // Minimum 1 pixel batch size + MAX: 1000, // Maximum 1000 pixels batch size + DEFAULT: 5, // Default 5 pixels batch size + }, + BATCH_MODE: 'normal', // "normal" or "random" - default to normal + RANDOM_BATCH_RANGE: { + MIN: 3, // Random range minimum + MAX: 20, // Random range maximum + }, + PAINTING_SPEED_ENABLED: true, // On by default + AUTO_CAPTCHA_ENABLED: true, // Turnstile generator enabled by default + TOKEN_SOURCE: 'generator', // "generator", "manual", or "hybrid" - default to generator + COOLDOWN_CHARGE_THRESHOLD: 1, // Default wait threshold + // Desktop Notifications (defaults) + NOTIFICATIONS: { + ENABLED: false, + ON_CHARGES_REACHED: true, + ONLY_WHEN_UNFOCUSED: true, + REPEAT_MINUTES: 5, // repeat reminder while threshold condition holds + }, + OVERLAY: { + OPACITY_DEFAULT: 0.6, + BLUE_MARBLE_DEFAULT: false, + ditheringEnabled: false, + }, // --- START: Color data from colour-converter.js --- + // New color structure with proper ID mapping + COLOR_MAP: { + 0: { id: 1, name: 'Black', rgb: { r: 0, g: 0, b: 0 } }, + 1: { id: 2, name: 'Dark Gray', rgb: { r: 60, g: 60, b: 60 } }, + 2: { id: 3, name: 'Gray', rgb: { r: 120, g: 120, b: 120 } }, + 3: { id: 4, name: 'Light Gray', rgb: { r: 210, g: 210, b: 210 } }, + 4: { id: 5, name: 'White', rgb: { r: 255, g: 255, b: 255 } }, + 5: { id: 6, name: 'Deep Red', rgb: { r: 96, g: 0, b: 24 } }, + 6: { id: 7, name: 'Red', rgb: { r: 237, g: 28, b: 36 } }, + 7: { id: 8, name: 'Orange', rgb: { r: 255, g: 127, b: 39 } }, + 8: { id: 9, name: 'Gold', rgb: { r: 246, g: 170, b: 9 } }, + 9: { id: 10, name: 'Yellow', rgb: { r: 249, g: 221, b: 59 } }, + 10: { id: 11, name: 'Light Yellow', rgb: { r: 255, g: 250, b: 188 } }, + 11: { id: 12, name: 'Dark Green', rgb: { r: 14, g: 185, b: 104 } }, + 12: { id: 13, name: 'Green', rgb: { r: 19, g: 230, b: 123 } }, + 13: { id: 14, name: 'Light Green', rgb: { r: 135, g: 255, b: 94 } }, + 14: { id: 15, name: 'Dark Teal', rgb: { r: 12, g: 129, b: 110 } }, + 15: { id: 16, name: 'Teal', rgb: { r: 16, g: 174, b: 166 } }, + 16: { id: 17, name: 'Light Teal', rgb: { r: 19, g: 225, b: 190 } }, + 17: { id: 20, name: 'Cyan', rgb: { r: 96, g: 247, b: 242 } }, + 18: { id: 44, name: 'Light Cyan', rgb: { r: 187, g: 250, b: 242 } }, + 19: { id: 18, name: 'Dark Blue', rgb: { r: 40, g: 80, b: 158 } }, + 20: { id: 19, name: 'Blue', rgb: { r: 64, g: 147, b: 228 } }, + 21: { id: 21, name: 'Indigo', rgb: { r: 107, g: 80, b: 246 } }, + 22: { id: 22, name: 'Light Indigo', rgb: { r: 153, g: 177, b: 251 } }, + 23: { id: 23, name: 'Dark Purple', rgb: { r: 120, g: 12, b: 153 } }, + 24: { id: 24, name: 'Purple', rgb: { r: 170, g: 56, b: 185 } }, + 25: { id: 25, name: 'Light Purple', rgb: { r: 224, g: 159, b: 249 } }, + 26: { id: 26, name: 'Dark Pink', rgb: { r: 203, g: 0, b: 122 } }, + 27: { id: 27, name: 'Pink', rgb: { r: 236, g: 31, b: 128 } }, + 28: { id: 28, name: 'Light Pink', rgb: { r: 243, g: 141, b: 169 } }, + 29: { id: 29, name: 'Dark Brown', rgb: { r: 104, g: 70, b: 52 } }, + 30: { id: 30, name: 'Brown', rgb: { r: 149, g: 104, b: 42 } }, + 31: { id: 31, name: 'Beige', rgb: { r: 248, g: 178, b: 119 } }, + 32: { id: 52, name: 'Light Beige', rgb: { r: 255, g: 197, b: 165 } }, + 33: { id: 32, name: 'Medium Gray', rgb: { r: 170, g: 170, b: 170 } }, + 34: { id: 33, name: 'Dark Red', rgb: { r: 165, g: 14, b: 30 } }, + 35: { id: 34, name: 'Light Red', rgb: { r: 250, g: 128, b: 114 } }, + 36: { id: 35, name: 'Dark Orange', rgb: { r: 228, g: 92, b: 26 } }, + 37: { id: 37, name: 'Dark Goldenrod', rgb: { r: 156, g: 132, b: 49 } }, + 38: { id: 38, name: 'Goldenrod', rgb: { r: 197, g: 173, b: 49 } }, + 39: { id: 39, name: 'Light Goldenrod', rgb: { r: 232, g: 212, b: 95 } }, + 40: { id: 40, name: 'Dark Olive', rgb: { r: 74, g: 107, b: 58 } }, + 41: { id: 41, name: 'Olive', rgb: { r: 90, g: 148, b: 74 } }, + 42: { id: 42, name: 'Light Olive', rgb: { r: 132, g: 197, b: 115 } }, + 43: { id: 43, name: 'Dark Cyan', rgb: { r: 15, g: 121, b: 159 } }, + 44: { id: 45, name: 'Light Blue', rgb: { r: 125, g: 199, b: 255 } }, + 45: { id: 46, name: 'Dark Indigo', rgb: { r: 77, g: 49, b: 184 } }, + 46: { id: 47, name: 'Dark Slate Blue', rgb: { r: 74, g: 66, b: 132 } }, + 47: { id: 48, name: 'Slate Blue', rgb: { r: 122, g: 113, b: 196 } }, + 48: { id: 49, name: 'Light Slate Blue', rgb: { r: 181, g: 174, b: 241 } }, + 49: { id: 53, name: 'Dark Peach', rgb: { r: 155, g: 82, b: 73 } }, + 50: { id: 54, name: 'Peach', rgb: { r: 209, g: 128, b: 120 } }, + 51: { id: 55, name: 'Light Peach', rgb: { r: 250, g: 182, b: 164 } }, + 52: { id: 50, name: 'Light Brown', rgb: { r: 219, g: 164, b: 99 } }, + 53: { id: 56, name: 'Dark Tan', rgb: { r: 123, g: 99, b: 82 } }, + 54: { id: 57, name: 'Tan', rgb: { r: 156, g: 132, b: 107 } }, + 55: { id: 36, name: 'Light Tan', rgb: { r: 214, g: 181, b: 148 } }, + 56: { id: 51, name: 'Dark Beige', rgb: { r: 209, g: 128, b: 81 } }, + 57: { id: 61, name: 'Dark Stone', rgb: { r: 109, g: 100, b: 63 } }, + 58: { id: 62, name: 'Stone', rgb: { r: 148, g: 140, b: 107 } }, + 59: { id: 63, name: 'Light Stone', rgb: { r: 205, g: 197, b: 158 } }, + 60: { id: 58, name: 'Dark Slate', rgb: { r: 51, g: 57, b: 65 } }, + 61: { id: 59, name: 'Slate', rgb: { r: 109, g: 117, b: 141 } }, + 62: { id: 60, name: 'Light Slate', rgb: { r: 179, g: 185, b: 209 } }, + 63: { id: 0, name: 'Transparent', rgb: null }, + }, // --- END: Color data --- + // Optimized CSS Classes for reuse + CSS_CLASSES: { + BUTTON_PRIMARY: ` + background: linear-gradient(135deg, #4facfe 0%, #00f2fe 100%); + color: white; border: none; border-radius: 8px; padding: 10px 16px; + cursor: pointer; font-weight: 500; transition: all 0.3s ease; + display: flex; align-items: center; gap: 8px; + `, + BUTTON_SECONDARY: ` + background: rgba(255,255,255,0.1); color: white; + border: 1px solid rgba(255,255,255,0.2); border-radius: 8px; + padding: 8px 12px; cursor: pointer; transition: all 0.3s ease; + `, + MODERN_CARD: ` + background: rgba(255,255,255,0.1); border-radius: 12px; + padding: 18px; border: 1px solid rgba(255,255,255,0.1); + backdrop-filter: blur(5px); + `, + GRADIENT_TEXT: ` + background: linear-gradient(135deg, #4facfe 0%, #00f2fe 100%); + -webkit-background-clip: text; -webkit-text-fill-color: transparent; + background-clip: text; font-weight: bold; + `, + }, + THEMES: { + 'Classic Autobot': { + primary: '#000000', + secondary: '#111111', + accent: '#222222', + text: '#ffffff', + highlight: '#775ce3', + success: '#00ff00', + error: '#ff0000', + warning: '#ffaa00', + fontFamily: "'Segoe UI', Roboto, sans-serif", + borderRadius: '12px', + borderStyle: 'solid', + borderWidth: '1px', + boxShadow: '0 8px 32px rgba(0,0,0,0.6), 0 0 0 1px rgba(255,255,255,0.1)', + backdropFilter: 'blur(10px)', + animations: { + glow: false, + scanline: false, + 'pixel-blink': false, + }, + }, + 'Classic Light': { + primary: '#E0E0E1', + secondary: '#FBFBFB', + accent: '#F3F3F3', + text: '#203C5D', + highlight: '#203C5D', + success: '#28a745', + error: '#dc3545', + warning: '#ffc107', + fontFamily: "'Segoe UI', Roboto, sans-serif", + borderRadius: '12px', + borderStyle: 'solid', + borderWidth: '1px', + boxShadow: '0 8px 32px rgba(0,0,0,0.15), 0 0 0 1px rgba(0,0,0,0.08)', + backdropFilter: 'blur(10px)', + animations: { + glow: false, + scanline: false, + 'pixel-blink': false, + }, + }, + 'Neon Retro': { + primary: '#1a1a2e', + secondary: '#16213e', + accent: '#0f3460', + text: '#00ff41', + highlight: '#ff6b35', + success: '#39ff14', + error: '#ff073a', + warning: '#ffff00', + neon: '#00ffff', + purple: '#bf00ff', + pink: '#ff1493', + fontFamily: "'Press Start 2P', monospace", + borderRadius: '0', + borderStyle: 'solid', + borderWidth: '3px', + boxShadow: '0 0 20px rgba(0, 255, 65, 0.3), inset 0 0 20px rgba(0, 255, 65, 0.1)', + backdropFilter: 'none', + animations: { + glow: true, + scanline: true, + 'pixel-blink': true, + }, + }, + 'Neon Retro Cyan': { + primary: '#1959A1', + secondary: '#3C74AF', + accent: '#538CC0', + text: '#81DCF7', + highlight: '#EA9C00', + success: '#39ff14', + error: '#ff073a', + warning: '#ffff00', + neon: '00ffff', + purple: '#bf00ff', + pink: '#ff1493', + fontFamily: "'Press Start 2P', monospace", + borderRadius: '0', + borderStyle: 'solid', + borderWidth: '3px', + boxShadow: '0 0 20px rgba(234 156 0, 0.3), inset 0 0 20px rgba(234 156 0, 0.1)', + backdropFilter: 'none', + animations: { + glow: true, + scanline: true, + 'pixel-blink': true, + }, + }, + 'Neon Retro Light': { + primary: '#E0E0E1', + secondary: '#FBFBFB', + accent: '#F3F3F3', + text: '#203C5D', + highlight: '#203C5D', + success: '#39ff14', + error: '#ff073a', + warning: '#ffff00', + neon: '#203C5D', + purple: '#bf00ff', + pink: '#ff1493', + fontFamily: "'Press Start 2P', monospace", + borderRadius: '0', + borderStyle: 'solid', + borderWidth: '3px', + boxShadow: '0 0 20px rgba(234 156 0, 0.3), inset 0 0 20px rgba(234 156 0, 0.1)', + backdropFilter: 'none', + animations: { + glow: true, + scanline: true, + 'pixel-blink': true, + }, + }, + 'Acrylic': { + primary: '#00000080', + secondary: '#00000040', + accent: 'rgba(0,0,0,0.75)', + text: '#ffffff', + highlight: '#ffffff', + success: '#00e500', + error: '#e50000', + warning: '#e5e500', + fontFamily: "'Inter', 'Apple Color Emoji'", + borderRadius: '10px', + borderStyle: 'solid', + borderWidth: '0px', + boxShadow: 'none', + backdropFilter: 'blur(20px)', + animations: { + glow: false, + scanline: false, + 'pixel-blink': false, + }, + }, + }, + currentTheme: 'Classic Autobot', + PAINT_UNAVAILABLE: true, + COORDINATE_MODE: 'rows', + COORDINATE_DIRECTION: 'top-left', + COORDINATE_SNAKE: true, + COORDINATE_BLOCK_WIDTH: 6, + COORDINATE_BLOCK_HEIGHT: 2, + autoSwap: true, + }; + + // Expose CONFIG globally for the utils manager and other modules + window.CONFIG = CONFIG; + + const getCurrentTheme = () => CONFIG.THEMES[CONFIG.currentTheme]; + + const switchTheme = (themeName) => { + if (CONFIG.THEMES[themeName]) { + CONFIG.currentTheme = themeName; + saveThemePreference(); + + // APPLY THEME VARS/CLASS (new) + applyTheme(); + + // Recreate UI (kept for now) + createUI(); + } + }; + + // Add this helper (place it after getCurrentTheme/switchTheme definitions) + function applyTheme() { + 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', + 'wplace-theme-classic-light', + 'wplace-theme-acrylic', + 'wplace-theme-neon', + 'wplace-theme-neon-cyan', + 'wplace-theme-neon-light' + ); + + let themeClass = 'wplace-theme-classic'; // default + let themeFileName = 'classic'; // corresponding file name + + if (CONFIG.currentTheme === 'Neon Retro') { + themeClass = 'wplace-theme-neon'; + themeFileName = 'neon'; + } else if (CONFIG.currentTheme === 'Classic Light') { + themeClass = 'wplace-theme-classic-light'; + themeFileName = 'classic-light'; + } else if (CONFIG.currentTheme === 'Neon Retro Cyan') { + themeClass = 'wplace-theme-neon-cyan'; + themeFileName = 'neon-cyan'; + } else if (CONFIG.currentTheme === 'Neon Retro Light') { + themeClass = 'wplace-theme-neon-light'; + themeFileName = 'neon-light'; + } else if (CONFIG.currentTheme === 'Acrylic') { + themeClass = 'wplace-theme-acrylic'; + themeFileName = 'acrylic'; + } + + document.documentElement.classList.add(themeClass); + console.log(`%cāœ… Applied theme class: ${themeClass}`, 'color: #8b5cf6;'); + + // Use extension's applyTheme helper if available (loads from local extension resources) + if (typeof window.applyTheme === 'function') { + console.log(`%cšŸ”§ Using extension's applyTheme() helper for: ${themeFileName}`, 'color: #10b981; font-weight: bold;'); + const success = window.applyTheme(themeFileName); + if (success) { + console.log(`%cāœ… Theme CSS loaded from extension local resources`, 'color: #10b981; font-weight: bold;'); + console.log(` šŸ“ Source: Extension local file (themes/${themeFileName}.css)`); + console.log(` šŸš€ Performance: Instant load (no network request)`); + } else { + console.warn(`%cāš ļø Extension theme loading failed, fallback to CSS classes only`, 'color: #f59e0b;'); + console.log(` šŸŽØ Using CSS class: ${themeClass}`); + console.log(` šŸ“ Note: Theme variables may be limited without CSS file`); + } + } else { + console.log(`%cšŸ“ Extension applyTheme() not available, using CSS class only`, 'color: #f59e0b;'); + console.log(` šŸŽØ Using CSS class: ${themeClass}`); + console.log(` āš ļø Note: Full theme styling requires extension helper function`); + } + + // Also set CSS variables explicitly in case you want runtime overrides + const root = document.documentElement; + const setVar = (k, v) => { + try { + root.style.setProperty(k, v); + } catch { } + }; + + setVar('--wplace-primary', theme.primary); + setVar('--wplace-secondary', theme.secondary); + setVar('--wplace-accent', theme.accent); + setVar('--wplace-text', theme.text); + setVar('--wplace-highlight', theme.highlight); + setVar('--wplace-success', theme.success); + setVar('--wplace-error', theme.error); + setVar('--wplace-warning', theme.warning); + + // Typography + look + setVar('--wplace-font', theme.fontFamily || "'Segoe UI', Roboto, sans-serif"); + setVar('--wplace-radius', '' + (theme.borderRadius || '12px')); + setVar('--wplace-border-style', '' + (theme.borderStyle || 'solid')); + 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(); + } + + const saveThemePreference = () => { + try { + localStorage.setItem('wplace-theme', CONFIG.currentTheme); + } catch (e) { + console.warn('Could not save theme preference:', e); + } + }; + + const loadThemePreference = () => { + try { + const saved = localStorage.getItem('wplace-theme'); + if (saved && CONFIG.THEMES[saved]) { + CONFIG.currentTheme = saved; + } + } catch (e) { + console.warn('Could not load theme preference:', e); + } + }; + + // Dynamically loaded translations + window.loadedTranslations = {}; + + // Available languages + const AVAILABLE_LANGUAGES = [ + 'en', + 'ru', + 'pt', + 'vi', + 'fr', + 'id', + 'tr', + 'zh-CN', + 'zh-TW', + 'ja', + 'ko', + 'uk', + ]; + + // Function to load translations from JSON file with retry mechanism + const loadTranslations = async (language, retryCount = 0) => { + if (window.loadedTranslations[language]) { + return window.loadedTranslations[language]; + } + + console.group(`%cšŸŒ Loading ${language.toUpperCase()} translations`, 'color: #06b6d4; font-weight: bold;'); + + // 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' && + translations !== null && + Object.keys(translations).length > 0 + ) { + window.loadedTranslations[language] = translations; + console.log(`%cāœ… Loaded ${language} translations from EXTENSION LOCAL FILES`, 'color: #10b981; font-weight: bold;'); + console.log(` šŸ“ Source: Extension local storage (chrome-extension://)`); + console.log(` šŸ“ Keys count: ${Object.keys(translations).length}`); + console.log(` šŸš€ Performance: Instant load (no network request)`); + console.groupEnd(); + return translations; + } + } else { + console.log(`%cšŸ“ ${langFile} not found in extension resources`, 'color: #f59e0b;'); + console.log(` šŸ“‹ Available in extension: ${Object.keys(window.AUTOBOT_LANGUAGES).join(', ')}`); + } + } else { + console.log(`%cšŸ“ No extension local resources available`, 'color: #f59e0b;'); + console.log(` šŸ” window.AUTOBOT_LANGUAGES: ${typeof window.AUTOBOT_LANGUAGES}`); + } + + // Second try: Use helper function if available + if (typeof window.getLanguage === 'function') { + console.log(`%cšŸ”§ Trying extension getLanguage() helper...`, 'color: #06b6d4;'); + try { + const translations = window.getLanguage(language); + if ( + typeof translations === 'object' && + translations !== null && + Object.keys(translations).length > 0 + ) { + window.loadedTranslations[language] = translations; + console.log(`%cāœ… Loaded ${language} translations via extension helper`, 'color: #10b981; font-weight: bold;'); + console.log(` šŸ“ Source: Extension getLanguage() function`); + console.log(` šŸ“ Keys count: ${Object.keys(translations).length}`); + console.groupEnd(); + return translations; + } + } catch (error) { + console.warn(`%cāš ļø Extension helper failed:`, 'color: #f59e0b;', error); + } + } + + // Fallback: Load from CDN (original behavior) + console.log(`%c🌐 Falling back to CDN loading...`, 'color: #8b5cf6;'); + const url = `https://wplace-autobot.github.io/WPlace-AutoBOT/main/lang/${language}.json`; + const maxRetries = 3; + const baseDelay = 1000; // 1 second + + try { + if (retryCount === 0) { + console.log(`šŸ”„ Loading ${language} translations from CDN...`); + } else { + console.log( + `šŸ”„ Retrying ${language} translations (attempt ${retryCount + 1}/${maxRetries + 1})...` + ); + } + + const response = await fetch(url); + if (response.ok) { + const translations = await response.json(); + + // Validate that translations is an object with keys + if ( + typeof translations === 'object' && + translations !== null && + Object.keys(translations).length > 0 + ) { + window.loadedTranslations[language] = translations; + console.log( + `%cšŸ“š Loaded ${language} translations from CDN (${Object.keys(translations).length} keys)`, 'color: #f59e0b; font-weight: bold;' + ); + console.log(` šŸ“ Source: CDN (wplace-autobot.github.io)`); + console.log(` 🌐 URL: ${url}`); + console.log(` āš ļø Performance: Network request required`); + console.groupEnd(); + return translations; + } else { + console.warn(`āŒ Invalid translation format for ${language}`); + throw new Error('Invalid translation format'); + } + } else { + console.warn( + `āŒ CDN returned HTTP ${response.status}: ${response.statusText} for ${language} translations` + ); + throw new Error(`HTTP ${response.status}: ${response.statusText}`); + } + } catch (error) { + console.error( + `āŒ Failed to load ${language} translations from CDN (attempt ${retryCount + 1}):`, + error + ); + + // Retry with exponential backoff + if (retryCount < maxRetries) { + const delay = baseDelay * Math.pow(2, retryCount); + console.log(`ā³ Retrying in ${delay}ms...`); + await new Promise((resolve) => setTimeout(resolve, delay)); + console.groupEnd(); + return loadTranslations(language, retryCount + 1); + } + } + + console.error(`%cšŸ’„ All translation loading methods failed for ${language}`, 'color: #ef4444; font-weight: bold;'); + console.groupEnd(); + return null; + }; + + const loadLanguagePreference = async () => { + const savedLanguage = localStorage.getItem('wplace_language'); + const browserLocale = navigator.language; + const browserLanguage = browserLocale.split('-')[0]; + + let selectedLanguage = 'en'; // Default fallback + + try { + // Check if we have the saved language available + if (savedLanguage && AVAILABLE_LANGUAGES.includes(savedLanguage)) { + selectedLanguage = savedLanguage; + console.log(`šŸ”„ Using saved language preference: ${selectedLanguage}`); + } + // Try full locale match (e.g. "zh-CN", "zh-TW" etc) + else if (AVAILABLE_LANGUAGES.includes(browserLocale)) { + selectedLanguage = browserLocale; + localStorage.setItem('wplace_language', browserLocale); + console.log(`šŸ”„ Using browser locale: ${selectedLanguage}`); + } + // Try base language match (e.g. "en" for "en-US" or "en-GB" etc) + else if (AVAILABLE_LANGUAGES.includes(browserLanguage)) { + selectedLanguage = browserLanguage; + localStorage.setItem('wplace_language', browserLanguage); + console.log(`šŸ”„ Using browser language: ${selectedLanguage}`); + } + // Use English as fallback + else { + console.log(`šŸ”„ No matching language found, using English fallback`); + } + + // Set the language in state first + state.language = selectedLanguage; + + // Only load translations if not already loaded and not English (which should already be loaded) + if (selectedLanguage !== 'en' && !window.loadedTranslations[selectedLanguage]) { + const loaded = await loadTranslations(selectedLanguage); + if (!loaded) { + console.warn( + `āš ļø Failed to load ${selectedLanguage} translations, falling back to English` + ); + state.language = 'en'; + localStorage.setItem('wplace_language', 'en'); + } + } + } catch (error) { + console.error(`āŒ Error in loadLanguagePreference:`, error); + state.language = 'en'; // Always ensure we have a valid language + } + }; + + // Simple user notification function for critical issues + const showTranslationWarning = (message) => { + try { + // Create a simple temporary notification banner + const warning = document.createElement('div'); + warning.style.cssText = ` + position: fixed; top: 10px; right: 10px; z-index: 10001; + background: rgba(255, 193, 7, 0.95); color: #212529; padding: 12px 16px; + border-radius: 8px; font-size: 14px; font-weight: 500; + box-shadow: 0 4px 12px rgba(0,0,0,0.3); border: 1px solid rgba(255, 193, 7, 0.8); + max-width: 300px; word-wrap: break-word; + `; + warning.textContent = message; + document.body.appendChild(warning); + + // Auto-remove after 8 seconds + setTimeout(() => { + if (warning.parentNode) { + warning.remove(); + } + }, 8000); + } catch (e) { + // If DOM manipulation fails, just log + console.warn('Failed to show translation warning UI:', e); + } + }; + + // Initialize translations function + const initializeTranslations = async () => { + try { + console.log('🌐 Initializing translation system...'); + + // Always ensure English is loaded as fallback first + if (!window.loadedTranslations['en']) { + const englishLoaded = await loadTranslations('en'); + if (!englishLoaded) { + console.warn('āš ļø Failed to load English translations from CDN, using fallback'); + showTranslationWarning('āš ļø Translation loading failed, using basic fallbacks'); + } + } + + // Then load user's language preference + await loadLanguagePreference(); + + console.log(`āœ… Translation system initialized. Active language: ${state.language}`); + } catch (error) { + console.error('āŒ Translation initialization failed:', error); + // Ensure state has a valid language even if loading fails + if (!state.language) { + state.language = 'en'; + } + console.warn('āš ļø Using fallback translations due to initialization failure'); + showTranslationWarning('āš ļø Translation system error, using basic English'); + } + }; + + // Emergency fallback TEXT (minimal) + const FALLBACK_TEXT = { + en: { + title: 'WPlace Auto-Image', + toggleOverlay: 'Toggle Overlay', + scanColors: 'Scan Colors', + uploadImage: 'Upload Image', + resizeImage: 'Resize Image', + selectPosition: 'Select Position', + startPainting: 'Start Painting', + stopPainting: 'Stop Painting', + progress: 'Progress', + pixels: 'Pixels', + charges: 'Charges', + batchSize: 'Batch Size', + cooldownSettings: 'Cooldown Settings', + waitCharges: 'Wait for Charges', + settings: 'Settings', + showStats: 'Show Statistics', + compactMode: 'Compact Mode', + minimize: 'Minimize', + tokenCapturedSuccess: 'Token captured successfully', + turnstileInstructions: 'Complete the verification', + hideTurnstileBtn: 'Hide', + notificationsNotSupported: 'Notifications not supported', + chargesReadyMessage: 'Charges are ready', + chargesReadyNotification: 'WPlace AutoBot', + initMessage: "Click 'Upload Image' to begin", + }, + }; + + // Safe translation function with multiple fallback levels + const getText = (key, replacements = {}) => { + // Try current language first + let text = window.loadedTranslations[state.language]?.[key]; + + // Fallback to English translations + if (!text && state.language !== 'en') { + text = window.loadedTranslations['en']?.[key]; + } + + // Fallback to hardcoded English + if (!text) { + text = FALLBACK_TEXT['en']?.[key]; + } + + // Last resort - return the key itself + if (!text) { + console.warn(`āš ļø Missing translation for key: ${key}`); + return key; + } + + // Handle string replacements like {count}, {time}, etc. + return Object.entries(replacements).reduce((result, [placeholder, value]) => { + return result.replace(new RegExp(`\\{${placeholder}\\}`, 'g'), value); + }, text); + }; + + // GLOBAL STATE + const state = { + running: false, + imageLoaded: false, + processing: false, + totalPixels: 0, + paintedPixels: 0, + preFilteringDone: false, // Track if pre-filtering detection has been done this session + availableColors: [], + activeColorPalette: [], // User-selected colors for conversion + paintWhitePixels: true, // Default to ON + fullChargeData: null, + fullChargeInterval: null, + paintTransparentPixels: false, // Default to OFF + displayCharges: 0, + preciseCurrentCharges: 0, + maxCharges: 1, // Default max charges + cooldown: CONFIG.COOLDOWN_DEFAULT, + imageData: null, + stopFlag: false, + colorsChecked: false, + startPosition: null, + selectingPosition: false, + region: null, + minimized: false, + lastPosition: { x: 0, y: 0 }, + estimatedTime: 0, + language: 'en', + paintingSpeed: CONFIG.PAINTING_SPEED.DEFAULT, // pixels batch size + batchMode: CONFIG.BATCH_MODE, // "normal" or "random" + randomBatchMin: CONFIG.RANDOM_BATCH_RANGE.MIN, // Random range minimum + randomBatchMax: CONFIG.RANDOM_BATCH_RANGE.MAX, // Random range maximum + cooldownChargeThreshold: CONFIG.COOLDOWN_CHARGE_THRESHOLD, + chargesThresholdInterval: null, + tokenSource: CONFIG.TOKEN_SOURCE, // "generator" or "manual" + initialSetupComplete: false, // Track if initial startup setup is complete (only happens once) + overlayOpacity: CONFIG.OVERLAY.OPACITY_DEFAULT, + blueMarbleEnabled: CONFIG.OVERLAY.BLUE_MARBLE_DEFAULT, + ditheringEnabled: true, + // Advanced color matching settings + colorMatchingAlgorithm: 'lab', + enableChromaPenalty: true, + chromaPenaltyWeight: 0.15, + customTransparencyThreshold: CONFIG.TRANSPARENCY_THRESHOLD, + customWhiteThreshold: CONFIG.WHITE_THRESHOLD, + resizeSettings: null, + originalImage: null, + resizeIgnoreMask: null, + paintUnavailablePixels: CONFIG.PAINT_UNAVAILABLE, + // Coordinate generation settings + coordinateMode: CONFIG.COORDINATE_MODE, + coordinateDirection: CONFIG.COORDINATE_DIRECTION, + coordinateSnake: CONFIG.COORDINATE_SNAKE, + blockWidth: CONFIG.COORDINATE_BLOCK_WIDTH, + blockHeight: CONFIG.COORDINATE_BLOCK_HEIGHT, + notificationsEnabled: CONFIG.NOTIFICATIONS.ENABLED, + notifyOnChargesReached: CONFIG.NOTIFICATIONS.ON_CHARGES_REACHED, + notifyOnlyWhenUnfocused: CONFIG.NOTIFICATIONS.ONLY_WHEN_UNFOCUSED, + notificationIntervalMinutes: CONFIG.NOTIFICATIONS.REPEAT_MINUTES, + _lastChargesNotifyAt: 0, + _lastChargesBelow: true, + // Smart save tracking + _lastSavePixelCount: 0, + _lastSaveTime: 0, + _saveInProgress: false, + paintedMap: null, + accountIndex: 0, + originalAccountOrder: [], // Store the original order of accounts + currentActiveIndex: 0, // Track current position in the original order + // Account switching state + allAccountsInfo: [], + isFetchingAllAccounts: false, + }; + + // Expose state globally for the utils manager and other modules + window.state = state; + + let _updateResizePreview = () => { }; + let _resizeDialogCleanup = null; + + // --- OVERLAY UPDATE: Optimized OverlayManager class with performance improvements --- + // OverlayManager class extracted to overlay-manager.js module + + // Overlay management is now handled by the OverlayManager module + // Check for overlay manager availability + if (!window.WPlaceOverlayManager) { + console.error('āŒ WPlaceOverlayManager not available - please ensure overlay-manager.js is loaded first'); + throw new Error('OverlayManager dependency not found'); + } + + // Ensure we only have ONE overlay manager instance globally + let overlayManager; + if (!window.autoImageOverlayManager) { + overlayManager = new window.WPlaceOverlayManager(); + window.autoImageOverlayManager = overlayManager; + console.log('šŸŽÆ Created NEW overlay manager instance'); + } else { + overlayManager = window.autoImageOverlayManager; + console.log('šŸŽÆ Using EXISTING overlay manager instance'); + } + + // Token management is now handled by the TokenManager module + // Global references for backward compatibility + if (!window.WPlaceTokenManager) { + console.error('āŒ WPlaceTokenManager not available - please ensure token-manager.js is loaded first'); + throw new Error('TokenManager dependency not found'); + } + + const tokenManager = window.WPlaceTokenManager; + const setTurnstileToken = (token) => tokenManager.setTurnstileToken(token); + const isTokenValid = () => tokenManager.isTokenValid(); + const invalidateToken = () => tokenManager.invalidateToken(); + const ensureToken = (forceRefresh = false) => tokenManager.ensureToken(forceRefresh); + + // Getter for the current token value + const getTurnstileToken = () => tokenManager.turnstileToken; + + // Token promise management + const createTokenPromise = () => { + tokenManager.tokenPromise = new Promise((resolve) => { + tokenManager._resolveToken = resolve; + }); + }; + + // Create global image processor instance for utility functions + if (!window.WPlaceImageProcessor) { + console.error('āŒ WPlaceImageProcessor not available - please ensure image-processor.js is loaded first'); + throw new Error('ImageProcessor dependency not found'); + } + + const globalImageProcessor = new window.WPlaceImageProcessor(); + + // Keep these constants for compatibility with other parts of the script + let retryCount = 0; + const MAX_RETRIES = 10; + const MAX_BATCH_RETRIES = 10; // Maximum attempts for batch sending + + function inject(callback) { + const script = document.createElement('script'); + script.textContent = `(${callback})();`; + document.documentElement?.appendChild(script); + script.remove(); + } + + inject(() => { + const fetchedBlobQueue = new Map(); + + window.addEventListener('message', (event) => { + const { source, blobID, blobData } = event.data; + if (source === 'auto-image-overlay' && blobID && blobData) { + const callback = fetchedBlobQueue.get(blobID); + if (typeof callback === 'function') { + callback(blobData); + } + fetchedBlobQueue.delete(blobID); + } + }); + + const originalFetch = window.fetch; + // Setup fetch interception + + window.fetch = async function (...args) { + const response = await originalFetch.apply(this, args); + const url = args[0] instanceof Request ? args[0].url : args[0]; + + // TILE REQUEST logging removed to reduce console spam + + if (typeof url === 'string') { + if (url.includes('https://backend.wplace.live/s0/pixel/')) { + try { + const payload = JSON.parse(args[1].body); + if (payload.t) { + console.log( + `šŸ”āœ… Turnstile Token Captured - Type: ${typeof payload.t}, Value: ${payload.t + ? typeof payload.t === 'string' + ? payload.t.length > 50 + ? payload.t.substring(0, 50) + '...' + : payload.t + : JSON.stringify(payload.t) + : 'null/undefined' + }, Length: ${payload.t?.length || 0}` + ); + window.postMessage({ source: 'turnstile-capture', token: payload.t }, '*'); + } + } catch (_) { + /* ignore */ + } + } + + const contentType = response.headers.get('content-type') || ''; + if (contentType.includes('image/png') && url.includes('.png')) { + const cloned = response.clone(); + return new Promise(async (resolve) => { + const blobUUID = crypto.randomUUID(); + const originalBlob = await cloned.blob(); + + fetchedBlobQueue.set(blobUUID, (processedBlob) => { + resolve( + new Response(processedBlob, { + headers: cloned.headers, + status: cloned.status, + statusText: cloned.statusText, + }) + ); + }); + + window.postMessage( + { + source: 'auto-image-tile', + endpoint: url, + blobID: blobUUID, + blobData: originalBlob, + }, + '*' + ); + }); + } + } + + return response; + }; + }); + + window.addEventListener('message', (event) => { + const { source, endpoint, blobID, blobData } = event.data; + + if (source === 'auto-image-tile' && endpoint && blobID && blobData) { + overlayManager.processAndRespondToTileRequest(event.data); + } + + // Token capture is now handled by TokenManager module + // No need to handle 'turnstile-capture' events here + }); + + async function detectLanguage() { + try { + const response = await fetch('https://backend.wplace.live/me', { + credentials: 'include', + }); + const data = await response.json(); + state.language = data.language === 'pt' ? 'pt' : 'en'; + } catch { + state.language = navigator.language.startsWith('pt') ? 'pt' : 'en'; + } + } + + // UTILITY FUNCTIONS WRAPPER - Uses WPlaceUtilsManager for modular functionality + const Utils = { + // Basic utilities + sleep: (ms) => window.globalUtilsManager ? window.globalUtilsManager.sleep(ms) : new Promise(r => setTimeout(r, ms)), + dynamicSleep: (tickAndGetRemainingMs) => window.globalUtilsManager ? window.globalUtilsManager.dynamicSleep(tickAndGetRemainingMs) : Promise.resolve(), + waitForSelector: (selector, interval, timeout) => window.globalUtilsManager ? window.globalUtilsManager.waitForSelector(selector, interval, timeout) : Promise.resolve(null), + msToTimeText: (ms) => window.globalUtilsManager ? window.globalUtilsManager.msToTimeText(ms) : `${Math.ceil(ms / 1000)}s`, + createScrollToAdjust: (element, updateCallback, min, max, step) => window.globalUtilsManager ? window.globalUtilsManager.createScrollToAdjust(element, updateCallback, min, max, step) : () => { }, + + // Tile calculation + calculateTileRange: (startRegionX, startRegionY, startPixelX, startPixelY, width, height, tileSize) => + window.globalUtilsManager ? window.globalUtilsManager.calculateTileRange(startRegionX, startRegionY, startPixelX, startPixelY, width, height, tileSize) : {}, + + // Token management + loadTurnstile: () => window.globalUtilsManager ? window.globalUtilsManager.loadTurnstile() : Promise.resolve(), + ensureTurnstileContainer: () => window.globalUtilsManager ? window.globalUtilsManager.ensureTurnstileContainer() : Promise.resolve(), + ensureTurnstileOverlayContainer: () => window.globalUtilsManager ? window.globalUtilsManager.ensureTurnstileOverlayContainer() : Promise.resolve(), + executeTurnstile: (sitekey, action) => window.globalUtilsManager ? window.globalUtilsManager.executeTurnstile(sitekey, action) : Promise.resolve(null), + createTurnstileWidget: (sitekey, action) => window.globalUtilsManager ? window.globalUtilsManager.createTurnstileWidget(sitekey, action) : Promise.resolve(), + createTurnstileWidgetInteractive: (sitekey, action) => window.globalUtilsManager ? window.globalUtilsManager.createTurnstileWidgetInteractive(sitekey, action) : Promise.resolve(), + cleanupTurnstile: () => window.globalUtilsManager ? window.globalUtilsManager.cleanupTurnstile() : Promise.resolve(), + obtainSitekeyAndToken: (fallback) => window.globalUtilsManager ? window.globalUtilsManager.obtainSitekeyAndToken(fallback) : Promise.resolve(fallback), + + // DOM utilities + createElement: (tag, props, children) => window.globalUtilsManager ? window.globalUtilsManager.createElement(tag, props, children) : document.createElement(tag), + createButton: (id, text, icon, onClick, style) => { + if (window.globalUtilsManager) { + return window.globalUtilsManager.createButton(id, text, icon, onClick, style); + } + const btn = document.createElement('button'); + btn.id = id; + btn.innerHTML = `${icon ? `` : ''}${text}`; + if (onClick) btn.addEventListener('click', onClick); + return btn; + }, + + // Translation with fallback + t: (key, params) => { + if (window.globalUtilsManager) { + return window.globalUtilsManager.t(key, params); + } + return getText(key, params); + }, + showAlert: (message, type) => { + if (window.globalUtilsManager) { + window.globalUtilsManager.showAlert(message, type); + } else { + console.log(`Alert [${type}]: ${message}`); + } + }, + + hideAlert: () => { + // Remove existing alerts immediately with safety checks + const alerts = document.querySelectorAll('.wplace-alert-base'); + alerts.forEach(alert => { + try { + if (alert && alert.parentNode) { + alert.parentNode.removeChild(alert); + } + } catch (error) { + // Ignore removeChild errors if element was already removed + console.debug('Alert already removed:', error); + } + }); + }, + + // Color utilities + colorDistance: (a, b) => window.globalUtilsManager ? window.globalUtilsManager.colorDistance(a, b) : Math.sqrt(Math.pow(a[0] - b[0], 2) + Math.pow(a[1] - b[1], 2) + Math.pow(a[2] - b[2], 2)), + findClosestPaletteColor: (r, g, b, palette) => window.globalUtilsManager ? window.globalUtilsManager.findClosestPaletteColor(r, g, b, palette) : [r, g, b], + isWhitePixel: (r, g, b) => window.globalUtilsManager ? window.globalUtilsManager.isWhitePixel(r, g, b) : (r > 240 && g > 240 && b > 240), + resolveColor: (targetRgb, availableColors, exactMatch) => window.globalUtilsManager ? window.globalUtilsManager.resolveColor(targetRgb, availableColors, exactMatch) : targetRgb, + + // CRITICAL FUNCTIONS - Direct implementations to ensure they work + createImageUploader: () => { + return new Promise((resolve, reject) => { + const input = document.createElement('input'); + input.type = 'file'; + input.accept = 'image/*'; + input.onchange = (e) => { + const file = e.target.files[0]; + if (file) { + const reader = new FileReader(); + reader.onload = () => resolve(reader.result); + reader.onerror = () => reject(new Error('File reading error')); + reader.readAsDataURL(file); + } else { + reject(new Error('No file selected')); + } + }; + input.click(); + }); + }, + + createFileUploader: () => { + return new Promise((resolve, reject) => { + const input = document.createElement('input'); + input.type = 'file'; + input.accept = '.json'; + input.onchange = (e) => { + const file = e.target.files[0]; + if (file) { + const reader = new FileReader(); + reader.onload = () => { + try { + const data = JSON.parse(reader.result); + resolve(data); + } catch (error) { + reject(new Error('Invalid JSON file')); + } + }; + reader.onerror = () => reject(new Error('File reading error')); + reader.readAsText(file); + } else { + reject(new Error('No file selected')); + } + }; + input.click(); + }); + }, + + createFileDownloader: (data, filename) => { + const blob = new Blob([data], { type: 'application/json' }); + const url = URL.createObjectURL(blob); + const a = document.createElement('a'); + a.href = url; + a.download = filename; + document.body.appendChild(a); + a.click(); + document.body.removeChild(a); + URL.revokeObjectURL(url); + }, + + loadProgress: () => { + try { + const saved = localStorage.getItem('wplace-bot-progress'); + if (!saved) return null; + return JSON.parse(saved); + } catch (error) { + console.error('Error loading progress:', error); + return null; + } + }, + + saveProgress: () => { + try { + // Use utils manager to build proper save structure like old version + const data = window.globalUtilsManager ? + window.globalUtilsManager.buildProgressData() : + { + timestamp: Date.now(), + version: '2.2', + state: { + totalPixels: window.state.totalPixels, + paintedPixels: window.state.paintedPixels, + lastPosition: window.state.lastPosition, + startPosition: window.state.startPosition, + region: window.state.region, + imageLoaded: window.state.imageLoaded, + colorsChecked: window.state.colorsChecked, + coordinateMode: window.state.coordinateMode, + coordinateDirection: window.state.coordinateDirection, + coordinateSnake: window.state.coordinateSnake, + blockWidth: window.state.blockWidth, + blockHeight: window.state.blockHeight, + availableColors: window.state.availableColors, + }, + imageData: window.state.imageData ? { + width: window.state.imageData.width, + height: window.state.imageData.height, + pixels: Array.from(window.state.imageData.pixels), + totalPixels: window.state.imageData.totalPixels, + } : null, + paintedMapPacked: window.globalUtilsManager ? + window.globalUtilsManager.buildPaintedMapPacked() : null + }; + + localStorage.setItem('wplace-bot-progress', JSON.stringify(data)); + return true; + } catch (error) { + console.error('Error saving progress:', error); + return false; + } + }, + + clearProgress: () => { + try { + localStorage.removeItem('wplace-bot-progress'); + if (window.state) { + window.state.paintedMap = null; + window.state._lastSavePixelCount = 0; + window.state._lastSaveTime = 0; + } + return true; + } catch (error) { + console.error('Error clearing progress:', error); + return false; + } + }, + + restoreProgress: (savedData) => { + try { + console.log('šŸ” [DEBUG] RestoreProgress: Starting restoration...'); + console.log('šŸ” [DEBUG] SavedData exists:', !!savedData); + console.log('šŸ” [DEBUG] SavedData.state exists:', !!savedData?.state); + + if (!savedData || !savedData.state) { + console.error('āŒ [DEBUG] RestoreProgress: Invalid savedData or missing state'); + return false; + } + + console.log('šŸ” [DEBUG] Current window.state keys before restore:', Object.keys(window.state || {})); + console.log('šŸ” [DEBUG] SavedData.state keys:', Object.keys(savedData.state)); + + console.log('šŸ” [DEBUG] Performing Object.assign...'); + Object.assign(window.state, savedData.state); + console.log('āœ… [DEBUG] Object.assign completed successfully'); + + // Restore available colors from old save files (backward compatibility) + if (savedData.state.availableColors && Array.isArray(savedData.state.availableColors)) { + console.log('šŸ” [DEBUG] Restoring availableColors, count:', savedData.state.availableColors.length); + window.state.availableColors = savedData.state.availableColors; + window.state.colorsChecked = true; // Mark colors as checked + console.log('āœ… [DEBUG] AvailableColors restored successfully'); + } else { + console.log('āš ļø [DEBUG] No availableColors found in savedData or not an array'); + } + + if (savedData.imageData) { + console.log('šŸ” [DEBUG] Restoring imageData...'); + console.log('šŸ” [DEBUG] ImageData type check - pixels is array:', Array.isArray(savedData.imageData.pixels)); + console.log('šŸ” [DEBUG] ImageData pixels length:', savedData.imageData.pixels?.length); + + window.state.imageData = { + ...savedData.imageData, + pixels: new Uint8ClampedArray(savedData.imageData.pixels), + }; + console.log('āœ… [DEBUG] ImageData restored successfully'); + console.log('šŸ” [DEBUG] Converted pixels to Uint8ClampedArray, length:', window.state.imageData.pixels.length); + } else { + console.log('āš ļø [DEBUG] No imageData found in savedData'); + } + + if (savedData.paintedMap) { + console.log('šŸ” [DEBUG] Restoring paintedMap...'); + window.state.paintedMap = savedData.paintedMap.map((row) => Array.from(row)); + console.log('āœ… [DEBUG] PaintedMap restored successfully'); + } else { + console.log('šŸ” [DEBUG] No paintedMap found in savedData (this is normal for extracted data)'); + } + + console.log('āœ… [DEBUG] RestoreProgress completed successfully'); + console.log('šŸ” [DEBUG] Final window.state.imageLoaded:', window.state.imageLoaded); + console.log('šŸ” [DEBUG] Final window.state.totalPixels:', window.state.totalPixels); + + return true; + } catch (error) { + console.error('āŒ [DEBUG] Error in restoreProgress:', error); + console.error('āŒ [DEBUG] Error stack:', error.stack); + console.error('āŒ [DEBUG] SavedData structure at error:', JSON.stringify(savedData, null, 2)); + return false; + } + }, + + saveProgressToFile: () => { + try { + // Use utils manager to build proper save structure like old version + const progressData = window.globalUtilsManager ? + window.globalUtilsManager.buildProgressData() : + { + timestamp: Date.now(), + version: '2.2', + state: { + totalPixels: window.state.totalPixels, + paintedPixels: window.state.paintedPixels, + lastPosition: window.state.lastPosition, + startPosition: window.state.startPosition, + region: window.state.region, + imageLoaded: window.state.imageLoaded, + colorsChecked: window.state.colorsChecked, + coordinateMode: window.state.coordinateMode, + coordinateDirection: window.state.coordinateDirection, + coordinateSnake: window.state.coordinateSnake, + blockWidth: window.state.blockWidth, + blockHeight: window.state.blockHeight, + availableColors: window.state.availableColors, + }, + imageData: window.state.imageData ? { + width: window.state.imageData.width, + height: window.state.imageData.height, + pixels: Array.from(window.state.imageData.pixels), + totalPixels: window.state.imageData.totalPixels, + } : null, + paintedMapPacked: window.globalUtilsManager ? + window.globalUtilsManager.buildPaintedMapPacked() : null + }; + + const filename = `wplace-bot-progress-${new Date() + .toISOString() + .slice(0, 19) + .replace(/:/g, '-')}.json`; + Utils.createFileDownloader(JSON.stringify(progressData, null, 2), filename); + return true; + } catch (error) { + console.error('Error saving to file:', error); + return false; + } + }, + + loadProgressFromFile: async () => { + try { + console.log('šŸ” [DEBUG] Starting file upload process...'); + const data = await Utils.createFileUploader(); + + console.log('šŸ” [DEBUG] File upload completed. Data type:', typeof data); + console.log('šŸ” [DEBUG] Data exists:', !!data); + + if (!data) { + console.error('āŒ [DEBUG] No data received from file uploader'); + throw new Error('No data received from file'); + } + + console.log('šŸ” [DEBUG] Data keys:', Object.keys(data)); + console.log('šŸ” [DEBUG] Data.state exists:', !!data.state); + console.log('šŸ” [DEBUG] Data.imageData exists:', !!data.imageData); + console.log('šŸ” [DEBUG] Data.version:', data.version); + console.log('šŸ” [DEBUG] Data.timestamp:', data.timestamp); + + if (!data.state) { + console.error('āŒ [DEBUG] Missing state object in loaded data'); + console.log('šŸ” [DEBUG] Available top-level keys:', Object.keys(data)); + throw new Error('Invalid file format - missing state object'); + } + + console.log('šŸ” [DEBUG] State object keys:', Object.keys(data.state)); + console.log('šŸ” [DEBUG] State.imageLoaded:', data.state.imageLoaded); + console.log('šŸ” [DEBUG] State.totalPixels:', data.state.totalPixels); + console.log('šŸ” [DEBUG] State.startPosition:', data.state.startPosition); + console.log('šŸ” [DEBUG] State.region:', data.state.region); + + if (data.imageData) { + console.log('šŸ” [DEBUG] ImageData width:', data.imageData.width); + console.log('šŸ” [DEBUG] ImageData height:', data.imageData.height); + console.log('šŸ” [DEBUG] ImageData pixels length:', data.imageData.pixels?.length); + console.log('šŸ” [DEBUG] ImageData totalPixels:', data.imageData.totalPixels); + } + + console.log('šŸ” [DEBUG] Calling restoreProgress...'); + const success = Utils.restoreProgress(data); + console.log('šŸ” [DEBUG] RestoreProgress result:', success); + + return success; + } catch (error) { + console.error('āŒ [DEBUG] Error in loadProgressFromFile:', error); + console.error('āŒ [DEBUG] Error stack:', error.stack); + return false; + } + }, + + loadExtractedFileData: async () => { + try { + console.log('šŸ” [DEBUG] Starting Art-Extractor file upload process...'); + const data = await Utils.createFileUploader(); + + console.log('šŸ” [DEBUG] File upload completed. Data type:', typeof data); + console.log('šŸ” [DEBUG] Data exists:', !!data); + console.log('šŸ“– Raw loaded data:', data); + + if (!data) { + console.error('āŒ [DEBUG] No data received from file uploader'); + return null; + } + + console.log('šŸ” [DEBUG] Data keys:', Object.keys(data)); + console.log('šŸ” [DEBUG] Data.state exists:', !!data.state); + console.log('šŸ” [DEBUG] Data.imageData exists:', !!data.imageData); + console.log('šŸ” [DEBUG] Data.version:', data.version); + console.log('šŸ” [DEBUG] Data.timestamp:', data.timestamp); + + console.log('šŸ” [DEBUG] Returning raw data object for Load Extracted feature'); + return data; + } catch (error) { + console.error('āŒ [DEBUG] Error in loadExtractedFileData:', error); + return null; + } + }, + + // Other utilities - delegate to globalUtilsManager or provide simple fallbacks + extractAvailableColors: () => { + if (window.globalUtilsManager) { + return window.globalUtilsManager.extractAvailableColors(); + } else if (window.globalImageProcessor) { + return window.globalImageProcessor.extractAvailableColors(window.CONFIG.COLOR_MAP); + } else { + return []; + } + }, + formatTime: (ms) => window.globalUtilsManager ? window.globalUtilsManager.formatTime(ms) : `${Math.floor(ms / 1000)}s`, + calculateEstimatedTime: (remainingPixels, charges, cooldown) => window.globalUtilsManager ? window.globalUtilsManager.calculateEstimatedTime(remainingPixels, charges, cooldown) : 0, + initializePaintedMap: (width, height) => window.globalUtilsManager ? window.globalUtilsManager.initializePaintedMap(width, height) : console.log('Painted map not available'), + markPixelPainted: (x, y, regionX, regionY) => window.globalUtilsManager ? window.globalUtilsManager.markPixelPainted(x, y, regionX, regionY) : false, + isPixelPainted: (x, y, regionX, regionY) => window.globalUtilsManager ? window.globalUtilsManager.isPixelPainted(x, y, regionX, regionY) : false, + shouldAutoSave: () => window.globalUtilsManager ? window.globalUtilsManager.shouldAutoSave() : false, + performSmartSave: () => window.globalUtilsManager ? window.globalUtilsManager.performSmartSave() : false, + packPaintedMapToBase64: (paintedMap, width, height) => window.globalUtilsManager ? window.globalUtilsManager.packPaintedMapToBase64(paintedMap, width, height) : null, + unpackPaintedMapFromBase64: (base64, width, height) => window.globalUtilsManager ? window.globalUtilsManager.unpackPaintedMapFromBase64(base64, width, height) : null, + migrateProgressToV2: (saved) => window.globalUtilsManager ? window.globalUtilsManager.migrateProgressToV2(saved) : saved, + migrateProgressToV21: (saved) => window.globalUtilsManager ? window.globalUtilsManager.migrateProgressToV21(saved) : saved, + migrateProgressToV22: (data) => window.globalUtilsManager ? window.globalUtilsManager.migrateProgressToV22(data) : data, + buildPaintedMapPacked: () => window.globalUtilsManager ? window.globalUtilsManager.buildPaintedMapPacked() : null, + buildProgressData: () => window.globalUtilsManager ? window.globalUtilsManager.buildProgressData() : {}, + migrateProgress: (saved) => window.globalUtilsManager ? window.globalUtilsManager.migrateProgress(saved) : saved, + restoreOverlayFromData: () => window.globalUtilsManager ? window.globalUtilsManager.restoreOverlayFromData() : Promise.resolve(false), + updateCoordinateUI: (config) => window.globalUtilsManager ? window.globalUtilsManager.updateCoordinateUI(config) : console.log('Coordinate UI update not available'), + }; + + // IMAGE PROCESSOR WRAPPER - Uses WPlaceImageProcessor for modular functionality + class ImageProcessor { + constructor(imageSrc) { + // Create instance of the modular ImageProcessor + this._processor = new window.WPlaceImageProcessor(imageSrc); + + // Legacy compatibility properties + this.imageSrc = imageSrc; + this.img = null; + this.canvas = null; + this.ctx = null; + } + + async load() { + await this._processor.load(); + + // Update legacy properties for compatibility + this.img = this._processor.img; + this.canvas = this._processor.canvas; + this.ctx = this._processor.ctx; + } + + getDimensions() { + return this._processor.getDimensions(); + } + + getPixelData() { + return this._processor.getPixelData(); + } + + resize(newWidth, newHeight) { + const result = this._processor.resize(newWidth, newHeight); + + // Update legacy properties + this.img = this._processor.img; + this.canvas = this._processor.canvas; + this.ctx = this._processor.ctx; + + return result; + } + + generatePreview(width, height) { + return this._processor.generatePreview(width, height); + } + + // Cleanup method + cleanup() { + if (this._processor) { + this._processor.cleanup(); + } + } + } + + // WPLACE API SERVICE + const WPlaceService = { + async paintPixelInRegion(regionX, regionY, pixelX, pixelY, color) { + try { + await ensureToken(); + if (!getTurnstileToken()) return 'token_error'; + const payload = { + coords: [pixelX, pixelY], + colors: [color], + t: getTurnstileToken(), + fp: fpStr32, + }; + var token = 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": token }, + credentials: 'include', + body: JSON.stringify(payload), + }); + if (res.status === 403) { + console.error('āŒ 403 Forbidden. Turnstile token might be invalid or expired.'); + setTurnstileToken(null); + createTokenPromise(); + return 'token_error'; + } + const data = await res.json(); + return data?.painted === 1; + } catch (e) { + console.error('Paint request failed:', e); + return false; + } + }, + + async getCharges() { + 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 || 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 { + 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, + Username: data.username || data.name || null, + Charges: data.charges.count, + Max: data.charges.max, + Droplets: data.droplets + } + } catch (e) { + console.error("Failed to get ID:", e) + return { + + } + } + } + }; + + // Desktop Notification Manager + const NotificationManager = { + pollTimer: null, + pollIntervalMs: 60_000, + icon() { + const link = document.querySelector("link[rel~='icon']"); + return link?.href || location.origin + '/favicon.ico'; + }, + async requestPermission() { + if (!('Notification' in window)) { + Utils.showAlert(Utils.t('notificationsNotSupported'), 'warning'); + return 'denied'; + } + if (Notification.permission === 'granted') return 'granted'; + try { + const perm = await Notification.requestPermission(); + return perm; + } catch { + return Notification.permission; + } + }, + canNotify() { + return ( + state.notificationsEnabled && + typeof Notification !== 'undefined' && + Notification.permission === 'granted' + ); + }, + notify(title, body, tag = 'wplace-charges', force = false) { + if (!this.canNotify()) return false; + if (!force && state.notifyOnlyWhenUnfocused && document.hasFocus()) return false; + try { + new Notification(title, { + body, + tag, + renotify: true, + icon: this.icon(), + badge: this.icon(), + silent: false, + }); + return true; + } catch { + // Graceful fallback + Utils.showAlert(body, 'info'); + return false; + } + }, + resetEdgeTracking() { + state._lastChargesBelow = state.displayCharges < state.cooldownChargeThreshold; + state._lastChargesNotifyAt = 0; + }, + maybeNotifyChargesReached(force = false) { + if (!state.notificationsEnabled || !state.notifyOnChargesReached) return; + const reached = state.displayCharges >= state.cooldownChargeThreshold; + const now = Date.now(); + const repeatMs = Math.max(1, Number(state.notificationIntervalMinutes || 5)) * 60_000; + if (reached) { + const shouldEdge = state._lastChargesBelow || force; + const shouldRepeat = now - (state._lastChargesNotifyAt || 0) >= repeatMs; + if (shouldEdge || shouldRepeat) { + const msg = Utils.t('chargesReadyMessage', { + current: state.displayCharges, + max: state.maxCharges, + threshold: state.cooldownChargeThreshold, + }); + this.notify(Utils.t('chargesReadyNotification'), msg, 'wplace-notify-charges'); + state._lastChargesNotifyAt = now; + } + state._lastChargesBelow = false; + } else { + state._lastChargesBelow = true; + } + }, + startPolling() { + this.stopPolling(); + if (!state.notificationsEnabled || !state.notifyOnChargesReached) return; + // lightweight background polling + this.pollTimer = setInterval(async () => { + try { + const { charges, cooldown, max } = await WPlaceService.getCharges(); + state.displayCharges = Math.floor(charges); + state.cooldown = cooldown; + state.maxCharges = Math.max(1, Math.floor(max)); + this.maybeNotifyChargesReached(); + } catch { + /* ignore */ + } + }, this.pollIntervalMs); + }, + stopPolling() { + if (this.pollTimer) { + clearInterval(this.pollTimer); + this.pollTimer = null; + } + }, + syncFromState() { + this.resetEdgeTracking(); + if (state.notificationsEnabled && state.notifyOnChargesReached) this.startPolling(); + else this.stopPolling(); + }, + }; + + // COLOR MATCHING FUNCTION - Optimized with caching + const colorCache = new Map(); + + // UI UPDATE FUNCTIONS (declared early to avoid reference errors) + let updateUI = () => { }; + let updateStats = async () => { }; + let updateDataButtons = () => { }; + + function updateActiveColorPalette() { + state.activeColorPalette = []; + const activeSwatches = document.querySelectorAll('.wplace-color-swatch.active'); + if (activeSwatches) { + activeSwatches.forEach((swatch) => { + const rgbStr = swatch.getAttribute('data-rgb'); + if (rgbStr) { + const rgb = rgbStr.split(',').map(Number); + state.activeColorPalette.push(rgb); + } + }); + } + if (document.querySelector('.resize-container')?.style.display === 'block') { + _updateResizePreview(); + } + } + + function toggleAllColors(select, showingUnavailable = false) { + const swatches = document.querySelectorAll('.wplace-color-swatch'); + if (swatches) { + swatches.forEach((swatch) => { + // Only toggle colors that are available or if we're showing unavailable colors + const isUnavailable = swatch.classList.contains('unavailable'); + if (!isUnavailable || showingUnavailable) { + // Don't try to select unavailable colors + if (!isUnavailable) { + swatch.classList.toggle('active', select); + } + } + }); + } + updateActiveColorPalette(); + } + + function unselectAllPaidColors() { + const swatches = document.querySelectorAll('.wplace-color-swatch'); + if (swatches) { + swatches.forEach((swatch) => { + const colorId = parseInt(swatch.getAttribute('data-color-id'), 10); + if (!isNaN(colorId) && colorId >= 32) { + swatch.classList.toggle('active', false); + } + }); + } + updateActiveColorPalette(); + } + + function initializeColorPalette(container) { + const colorsContainer = container.querySelector('#colors-container'); + const showAllToggle = container.querySelector('#showAllColorsToggle'); + if (!colorsContainer) return; + + // Use already captured colors from state (captured during upload) + // Don't re-fetch colors here, use what was captured when user clicked upload + if (!state.availableColors || state.availableColors.length === 0) { + // If no colors have been captured yet, show message + colorsContainer.innerHTML = `
${Utils.t( + 'uploadImageFirst' + )}
`; + return; + } + + function populateColors(showUnavailable = false) { + colorsContainer.innerHTML = ''; + let availableCount = 0; + let totalCount = 0; + + // Convert COLOR_MAP to array and filter out transparent + const allColors = Object.values(CONFIG.COLOR_MAP).filter((color) => color.rgb !== null); + + allColors.forEach((colorData) => { + const { id, name, rgb } = colorData; + const rgbKey = `${rgb.r},${rgb.g},${rgb.b}`; + totalCount++; + + // Check if this color is available in the captured colors + const isAvailable = state.availableColors.some( + (c) => c.rgb[0] === rgb.r && c.rgb[1] === rgb.g && c.rgb[2] === rgb.b + ); + + // If not showing all colors and this color is not available, skip it + if (!showUnavailable && !isAvailable) { + return; + } + + if (isAvailable) availableCount++; + + const colorItem = Utils.createElement('div', { + className: 'wplace-color-item', + }); + const swatch = Utils.createElement('button', { + className: `wplace-color-swatch ${!isAvailable ? 'unavailable' : ''}`, + title: `${name} (ID: ${id})${!isAvailable ? ' (Unavailable)' : ''}`, + 'data-rgb': rgbKey, + 'data-color-id': id, + }); + swatch.style.backgroundColor = `rgb(${rgb.r}, ${rgb.g}, ${rgb.b})`; + + // Make unavailable colors visually distinct + if (!isAvailable) { + swatch.style.opacity = '0.4'; + swatch.style.filter = 'grayscale(50%)'; + swatch.disabled = true; + } else { + // Select available colors by default + swatch.classList.add('active'); + } + + const nameLabel = Utils.createElement( + 'span', + { + className: 'wplace-color-item-name', + style: !isAvailable ? 'color: #888; font-style: italic;' : '', + }, + name + (!isAvailable ? ' (N/A)' : '') + ); + + // Only add click listener for available colors + if (isAvailable) { + swatch.addEventListener('click', () => { + swatch.classList.toggle('active'); + updateActiveColorPalette(); + }); + } + + colorItem.appendChild(swatch); + colorItem.appendChild(nameLabel); + colorsContainer.appendChild(colorItem); + }); + + updateActiveColorPalette(); + } + + // Initialize with only available colors + populateColors(false); + + // Add toggle functionality + if (showAllToggle) { + showAllToggle.addEventListener('change', (e) => { + populateColors(e.target.checked); + }); + } + + container + .querySelector('#selectAllBtn') + ?.addEventListener('click', () => toggleAllColors(true, showAllToggle?.checked)); + container + .querySelector('#unselectAllBtn') + ?.addEventListener('click', () => toggleAllColors(false, showAllToggle?.checked)); + container + .querySelector('#unselectPaidBtn') + ?.addEventListener('click', () => unselectAllPaidColors()); + } + + async function handleCaptcha(allowGeneration = true) { + const startTime = performance.now(); + + // Check user's token source preference + if (state.tokenSource === 'manual') { + console.log('šŸŽÆ Manual token source selected - using pixel placement automation'); + if (!allowGeneration) { + console.log('āŒ Token generation disabled during processing'); + return null; + } + return await tokenManager.handleCaptchaFallback(); + } + + // Generator mode (pure) or Hybrid mode - try generator first + try { + // Use optimized token generation with automatic sitekey detection + const { sitekey, token: preGeneratedToken } = await Utils.obtainSitekeyAndToken(); + + if (!sitekey) { + throw new Error('No valid sitekey found'); + } + + console.log('šŸ”‘ Generating Turnstile token for sitekey:', sitekey); + console.log( + '🧭 UA:', + navigator.userAgent.substring(0, 50) + '...', + 'Platform:', + navigator.platform + ); + + // Add additional checks before token generation + if (!window.turnstile) { + await Utils.loadTurnstile(); + } + + let token = null; + + // āœ… Reuse pre-generated token if available and valid + if ( + preGeneratedToken && + typeof preGeneratedToken === 'string' && + preGeneratedToken.length > 20 + ) { + console.log('ā™»ļø Reusing pre-generated token from sitekey detection phase'); + token = preGeneratedToken; + } + // āœ… Or use globally cached token if still valid + else if (isTokenValid()) { + console.log('ā™»ļø Using existing cached token (from previous operation)'); + token = getTurnstileToken(); + } + // āœ… Otherwise generate a new one (only if allowed) + else { + if (!allowGeneration) { + console.log('āŒ Token expired/missing but generation disabled during processing'); + return null; + } + console.log('šŸ” No valid pre-generated or cached token, creating new one...'); + token = await Utils.executeTurnstile(sitekey, 'paint'); + if (token) { + setTurnstileToken(token); + } + } + + console.log( + `šŸ” Token received - Type: ${typeof token}, Value: ${token + ? typeof token === 'string' + ? token.length > 50 + ? token.substring(0, 50) + '...' + : token + : JSON.stringify(token) + : 'null/undefined' + }, Length: ${token?.length || 0}` + ); + + // āœ… Final validation + if (typeof token === 'string' && token.length > 20) { + const duration = Math.round(performance.now() - startTime); + console.log(`āœ… Turnstile token generated successfully in ${duration}ms`); + return token; + } else { + throw new Error( + `Invalid or empty token received - Type: ${typeof token}, Value: ${JSON.stringify( + token + )}, Length: ${token?.length || 0}` + ); + } + } catch (error) { + const duration = Math.round(performance.now() - startTime); + console.error(`āŒ Turnstile token generation failed after ${duration}ms:`, error); + + // Fallback to manual pixel placement for hybrid mode + if (state.tokenSource === 'hybrid') { + console.log( + 'šŸ”„ Hybrid mode: Generator failed, automatically switching to manual pixel placement...' + ); + const fbToken = await tokenManager.handleCaptchaFallback(); + return fbToken; + } else { + // Pure generator mode - don't fallback, just fail + throw error; + } + } + } + + async function createUI() { + await detectLanguage(); + + const existingContainer = document.getElementById('wplace-image-bot-container'); + const existingStats = document.getElementById('wplace-stats-container'); + const existingSettings = document.getElementById('wplace-settings-container'); + const existingResizeContainer = document.querySelector('.resize-container'); + const existingResizeOverlay = document.querySelector('.resize-overlay'); + + if (existingContainer) existingContainer.remove(); + if (existingStats) existingStats.remove(); + if (existingSettings) existingSettings.remove(); + if (existingResizeContainer) existingResizeContainer.remove(); + if (existingResizeOverlay) existingResizeOverlay.remove(); + + loadThemePreference(); + await initializeTranslations(); + + const theme = getCurrentTheme(); + applyTheme(); // <- new: set CSS vars and theme class before building UI + + function appendLinkOnce(href, attributes = {}) { + // Check if a link with the same href already exists in the document head + const exists = Array.from(document.head.querySelectorAll('link')).some( + (link) => link.href === href + ); + if (exists) return; + + // Create a new link element + const link = document.createElement('link'); + link.rel = 'stylesheet'; + link.href = href; + + // Add any additional attributes (e.g., data-* attributes) + for (const [key, value] of Object.entries(attributes)) { + link.setAttribute(key, value); + } + + // Append the link element to the document head + document.head.appendChild(link); + } + + appendLinkOnce('https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css'); + + if (theme.fontFamily.includes('Press Start 2P')) { + appendLinkOnce('https://fonts.googleapis.com/css2?family=Press+Start+2P&display=swap'); + } + + // 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) { + console.log('%cāœ… Styles already injected by extension - using local version', 'color: #10b981; font-weight: bold;'); + console.log(' šŸ“ Source: Extension local file (already in DOM)'); + console.log(' šŸ†” Element ID: autobot-auto-image-styles'); + stylesLoaded = true; + } else { + // Inject it ourselves + const styleElement = document.createElement('style'); + styleElement.id = 'autobot-auto-image-styles-script'; + 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'); + console.log(' šŸ†” Element ID: autobot-auto-image-styles-script'); + stylesLoaded = true; + } + } else { + console.log('%cšŸ“ auto-image-styles.css not found in extension resources', 'color: #f59e0b;'); + console.log(' šŸ” window.AUTOBOT_THEMES:', typeof window.AUTOBOT_THEMES); + if (window.AUTOBOT_THEMES) { + 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;'); + appendLinkOnce( + 'https://wplace-autobot.github.io/WPlace-AutoBOT/main/auto-image-styles.css', + { 'data-wplace-theme': 'true' } + ); + console.log('%cšŸ“š Loaded auto-image-styles.css from CDN (fallback)', 'color: #f59e0b; font-weight: bold;'); + console.log(' šŸ“ Source: CDN (wplace-autobot.github.io)'); + 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') { + defaultTheme = 'neon'; + } else if (CONFIG.currentTheme === 'Neon Retro Cyan') { + defaultTheme = 'neon-cyan'; + } else if (CONFIG.currentTheme === 'Neon Retro Light') { + defaultTheme = 'neon-light'; + } else if (CONFIG.currentTheme === 'Classic Light') { + defaultTheme = 'classic-light'; + } 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)`); + console.log(` šŸš€ Performance: Instant load (no CDN request)`); + console.log(` šŸŽØ Theme: ${CONFIG.currentTheme}`); + } else { + 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;'); + console.log(` šŸ“ Themes will be limited to CSS classes only`); + } + + const container = document.createElement('div'); + container.id = 'wplace-image-bot-container'; + container.innerHTML = ` +
+
+ + ${Utils.t('title')} +
+
+ + + + +
+
+
+ +
+
+ ${Utils.t('initMessage')} +
+
+
+
+
+ + +
+
šŸ–¼ļø Image Management
+
+
+ + +
+
+ + +
+
+ +
+
+
+ + +
+
šŸŽ® Painting Control
+
+
+ + +
+
+ +
+
+
+ + +
+
ā±ļø ${Utils.t('cooldownSettings')}
+
+ +
+
+ +
+
+ + + + ${Utils.t('charges')} +
+
+
+
+ + +
+
šŸ’¾ Data Management
+
+
+ + +
+
+ + +
+
+
+
+ `; + + // Stats Window - Separate UI + const statsContainer = document.createElement('div'); + statsContainer.id = 'wplace-stats-container'; + statsContainer.style.display = 'none'; + statsContainer.innerHTML = ` +
+
+ + ${Utils.t('paintingStats')} +
+
+ + +
+
+
+
+
+
+
${Utils.t( + 'initMessage' + )}
+
+
+
+ +
+
+
+ + Account Swapper +
+ +
+
+ +
+
+ + All Accounts + +
+
+
Click the icon to load accounts.
+
+
+
+ `; + + // Modern Settings Container with Theme Support + // Use the theme variable already declared at the top of createUI function + const settingsContainer = document.createElement('div'); + settingsContainer.id = 'wplace-settings-container'; + + // Apply theme-based styling + const themeBackground = theme.primary + ? `linear-gradient(135deg, ${theme.primary} 0%, ${theme.secondary || theme.primary} 100%)` + : `linear-gradient(135deg, #667eea 0%, #764ba2 100%)`; + + settingsContainer.className = 'wplace-settings-container-base'; + // Apply theme-specific background + settingsContainer.style.background = themeBackground; + settingsContainer.style.cssText += ` + min-width: 420px; + max-width: 480px; + z-index: 99999; + color: ${theme.text || 'white'}; + font-family: ${theme.fontFamily || "'Segoe UI', Tahoma, Geneva, Verdana, sans-serif"}; + box-shadow: ${theme.boxShadow || '0 20px 40px rgba(0,0,0,0.3), 0 0 0 1px rgba(255,255,255,0.1)' + }; + backdrop-filter: ${theme.backdropFilter || 'blur(10px)'}; + overflow: hidden; + animation: settings-slide-in 0.4s ease-out; + ${theme.animations?.glow + ? ` + box-shadow: ${theme.boxShadow || '0 20px 40px rgba(0,0,0,0.3)'}, + 0 0 30px ${theme.highlight || theme.neon || '#00ffff'}; + ` + : '' + } + `; + + // noinspection CssInvalidFunction + settingsContainer.innerHTML = ` +
+
+

+ + ${Utils.t('settings')} +

+ +
+
+ +
+ + +
+ +
+ +

+ Generator mode creates tokens automatically. Hybrid mode falls back to manual when generator fails. Manual mode only uses pixel placement. +

+
+
+ + +
+ + +
+ + +
+ +
+ +
+
+ Overlay Opacity +
${Math.round(state.overlayOpacity * 100)}%
+
+ +
+ + +
+
+ + +
+ + +
+ + + + + + +
+
+ + +
+ + + +
+ + +
+ + +
+
+ ${Utils.t('batchSize')} +
+
+
+ +
+
+
+ + + + pixels +
+
+
+
+ ${CONFIG.PAINTING_SPEED.MIN} + ${CONFIG.PAINTING_SPEED.MAX} +
+
+ + +
+
+
+ + +
+
+ + +
+
+

+ šŸŽ² Random batch size between min and max values +

+
+ + + +
+ + +
+ + + +
+ + +
+ + +
+ + +
+ + +
+ +
+ + +
+
+
+ + +
+
+ + +
+
+

+ 🧱 Block dimensions for block-based generation modes +

+
+
+ + +
+ +
+ + + +
+ ${Utils.t('repeatEvery')} + + ${Utils.t('minutesPl')} +
+
+ + +
+
+
+ + +
+ +
+ +
+
+ + +
+ +
+ +
+
+
+ + + + + `; + + 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 +
+
+
+ +
+
+
+ + +
+
+
+
+
+
+
+ +
+ + 1 +
+
+
+ +
+ + 1 +
+
+
+
+ +
+ + + +
+
+ + + Shift = Row • Alt = Column +
+
+ +
+
+  Color Palette +
+
+
+ +
+
+ + + +
+
+
+
+ +
+
+  Advanced Color Matching +
+
+ + +
+
+ ${Utils.t('chromaWeight')} + ${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); + + // Show the main container after all elements are appended + container.style.display = 'block'; + + 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'); + + container.querySelectorAll('.wplace-section-title').forEach((title) => { + // Add a right-side arrow if it doesn't exist + if (!title.querySelector('i.arrow')) { + const arrow = document.createElement('i'); + arrow.className = 'fas fa-chevron-down arrow'; // FontAwesome down arrow + title.appendChild(arrow); + } + + // Click event to toggle collapse/expand of the section + title.addEventListener('click', () => { + const section = title.parentElement; + section.classList.toggle('collapsed'); + }); + }); + + // 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 cooldownInput = container.querySelector('#cooldownInput'); + const cooldownDecrease = container.querySelector('#cooldownDecrease'); + const cooldownIncrease = container.querySelector('#cooldownIncrease'); + 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) { + // Note: base CSS now aligns with this layout: main panel at left:20px (width 280), stats at left:330px. + } + + 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 = Utils.t('showStats'); + } else { + statsContainer.style.display = 'block'; + statsBtn.innerHTML = ''; + statsBtn.title = Utils.t('hideStats'); + } + }); + + closeStatsBtn.addEventListener('click', () => { + statsContainer.style.display = 'none'; + statsBtn.innerHTML = ''; + statsBtn.title = Utils.t('showStats'); + }); + + if (refreshChargesBtn) { + refreshChargesBtn.addEventListener('click', async () => { + refreshChargesBtn.innerHTML = ''; + refreshChargesBtn.disabled = true; + + try { + await updateStats(); + await updateCurrentAccountInList(); + } catch (error) { + console.error('Error refreshing charges:', error); + } finally { + refreshChargesBtn.innerHTML = ''; + refreshChargesBtn.disabled = false; + } + }); + } + + // Account-related event handlers + const autoSwapToggle = statsContainer.querySelector('#autoSwapToggle'); + const refreshAllAccountsBtn = statsContainer.querySelector('#refreshAllAccountsBtn'); + + if (autoSwapToggle) { + autoSwapToggle.checked = CONFIG.autoSwap; + autoSwapToggle.addEventListener('change', (e) => { + CONFIG.autoSwap = e.target.checked; + console.log(`šŸ”„ Auto-swap ${CONFIG.autoSwap ? 'enabled' : 'disabled'}`); + }); + } + + if (refreshAllAccountsBtn) { + refreshAllAccountsBtn.addEventListener('click', fetchAllAccountDetails); + } + } + if (statsContainer && statsBtn) { + // Stats container starts visible by default - user clicks button to hide + statsContainer.style.display = 'block'; + statsBtn.innerHTML = ''; + statsBtn.title = Utils.t('hideStats'); + } + + 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.classList.contains('show'); + if (isVisible) { + settingsContainer.style.animation = 'settings-fade-out 0.3s ease-out forwards'; + settingsContainer.classList.remove('show'); + setTimeout(() => { + settingsContainer.style.animation = ''; + }, 300); + } else { + settingsContainer.style.top = '50%'; + settingsContainer.style.left = '50%'; + settingsContainer.style.transform = 'translate(-50%, -50%)'; + settingsContainer.classList.add('show'); + settingsContainer.style.animation = 'settings-slide-in 0.4s ease-out'; + } + }); + + closeSettingsBtn.addEventListener('click', () => { + settingsContainer.style.animation = 'settings-fade-out 0.3s ease-out forwards'; + settingsContainer.classList.remove('show'); + setTimeout(() => { + 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( + Utils.t('tokenSourceSet', { source: 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( + Utils.t('batchModeSet', { + mode: + state.batchMode === 'random' ? Utils.t('randomRange') : Utils.t('normalFixedSize'), + }), + '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', async (e) => { + const newLanguage = e.target.value; + state.language = newLanguage; + localStorage.setItem('wplace_language', newLanguage); + + // Load the new language translations + await loadTranslations(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'); + const settingsPaintWhiteToggle = settingsContainer.querySelector('#settingsPaintWhiteToggle'); + const settingsPaintTransparentToggle = settingsContainer.querySelector( + '#settingsPaintTransparentToggle' + ); + + if (overlayOpacitySlider && overlayOpacityValue) { + const updateOpacity = (newValue) => { + const opacity = parseFloat(newValue); + state.overlayOpacity = opacity; + overlayOpacitySlider.value = opacity; + overlayOpacityValue.textContent = `${Math.round(opacity * 100)}%`; + }; + + overlayOpacitySlider.addEventListener('input', (e) => { + updateOpacity(e.target.value); + }); + + // Add scroll-to-adjust for overlay opacity slider + Utils.createScrollToAdjust(overlayOpacitySlider, updateOpacity, 0, 1, 0.05); + } + + if (settingsPaintWhiteToggle) { + settingsPaintWhiteToggle.checked = state.paintWhitePixels; + settingsPaintWhiteToggle.addEventListener('change', (e) => { + state.paintWhitePixels = e.target.checked; + saveBotSettings(); + console.log(`šŸŽØ Paint white pixels: ${state.paintWhitePixels ? 'ON' : 'OFF'}`); + const statusText = state.paintWhitePixels + ? 'White pixels in the template will be painted' + : 'White pixels will be skipped'; + Utils.showAlert(statusText, 'success'); + }); + } + + if (settingsPaintTransparentToggle) { + settingsPaintTransparentToggle.checked = state.paintTransparentPixels; + settingsPaintTransparentToggle.addEventListener('change', (e) => { + state.paintTransparentPixels = e.target.checked; + saveBotSettings(); + console.log( + `šŸŽØ Paint transparent pixels: ${state.paintTransparentPixels ? 'ON' : 'OFF'}` + ); + const statusText = state.paintTransparentPixels + ? 'Transparent pixels in the template will be painted with the closest available color' + : 'Transparent pixels will be skipped'; + Utils.showAlert(statusText, 'success'); + }); + } + + // Speed controls - both slider and input + const speedSlider = settingsContainer.querySelector('#speedSlider'); + const speedInput = settingsContainer.querySelector('#speedInput'); + const speedDecrease = settingsContainer.querySelector('#speedDecrease'); + const speedIncrease = settingsContainer.querySelector('#speedIncrease'); + const speedValue = settingsContainer.querySelector('#speedValue'); + + if (speedSlider && speedInput && speedValue && speedDecrease && speedIncrease) { + const updateSpeed = (newValue) => { + const speed = Math.max(CONFIG.PAINTING_SPEED.MIN, Math.min(CONFIG.PAINTING_SPEED.MAX, parseInt(newValue))); + state.paintingSpeed = speed; + + // Update both controls (value shows in input, label shows unit only) + speedSlider.value = speed; + speedInput.value = speed; + speedValue.textContent = `pixels`; + + saveBotSettings(); + }; + + // Slider event listener + speedSlider.addEventListener('input', (e) => { + updateSpeed(e.target.value); + }); + + // Number input event listener + speedInput.addEventListener('input', (e) => { + updateSpeed(e.target.value); + }); + + // Decrease button + speedDecrease.addEventListener('click', () => { + updateSpeed(parseInt(speedInput.value) - 1); + }); + + // Increase button + speedIncrease.addEventListener('click', () => { + updateSpeed(parseInt(speedInput.value) + 1); + }); + + // Add scroll-to-adjust for speed slider + Utils.createScrollToAdjust(speedSlider, updateSpeed, CONFIG.PAINTING_SPEED.MIN, CONFIG.PAINTING_SPEED.MAX, 1); + } + + if (enableBlueMarbleToggle) { + enableBlueMarbleToggle.addEventListener('click', async () => { + state.blueMarbleEnabled = enableBlueMarbleToggle.checked; + if (state.imageLoaded && overlayManager.imageBitmap) { + Utils.showAlert(Utils.t('reprocessingOverlay'), 'info'); + await overlayManager.processImageIntoChunks(); + Utils.showAlert(Utils.t('overlayUpdated'), '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(Utils.t('notificationsEnabled'), 'success'); + else Utils.showAlert(Utils.t('notificationsPermissionDenied'), 'warning'); + }); + } + if (notifTestBtn) { + notifTestBtn.addEventListener('click', () => { + NotificationManager.notify( + Utils.t('testNotificationTitle'), + Utils.t('testNotificationMessage'), + '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 paintTransparentToggle = resizeContainer.querySelector('#paintTransparentToggle'); + 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'); + + // Coordinate generation controls with smart visibility + const coordinateModeSelect = settingsContainer.querySelector('#coordinateModeSelect'); + const coordinateDirectionSelect = settingsContainer.querySelector('#coordinateDirectionSelect'); + const coordinateSnakeToggle = settingsContainer.querySelector('#coordinateSnakeToggle'); + const directionControls = settingsContainer.querySelector('#directionControls'); + const snakeControls = settingsContainer.querySelector('#snakeControls'); + const blockControls = settingsContainer.querySelector('#blockControls'); + const blockWidthInput = settingsContainer.querySelector('#blockWidthInput'); + const blockHeightInput = settingsContainer.querySelector('#blockHeightInput'); + const paintUnavailablePixelsToggle = settingsContainer.querySelector( + '#paintUnavailablePixelsToggle' + ); + + if (paintUnavailablePixelsToggle) { + paintUnavailablePixelsToggle.checked = state.paintUnavailablePixels; + paintUnavailablePixelsToggle.addEventListener('change', (e) => { + state.paintUnavailablePixels = e.target.checked; + saveBotSettings(); + console.log(`šŸŽØ Paint unavailable colors: ${state.paintUnavailablePixels ? 'ON' : 'OFF'}`); + const statusText = state.paintUnavailablePixels + ? 'Unavailable template colors will be painted with the closest available color' + : 'Unavailable template colors will be skipped'; + Utils.showAlert(statusText, 'success'); + }); + } + if (coordinateModeSelect) { + coordinateModeSelect.value = state.coordinateMode; + coordinateModeSelect.addEventListener('change', (e) => { + state.coordinateMode = e.target.value; + Utils.updateCoordinateUI({ + mode: state.coordinateMode, + directionControls, + snakeControls, + blockControls, + }); + saveBotSettings(); + console.log(`šŸ”„ Coordinate mode changed to: ${state.coordinateMode}`); + Utils.showAlert(`Coordinate mode set to: ${state.coordinateMode}`, 'success'); + }); + } + + if (coordinateDirectionSelect) { + coordinateDirectionSelect.value = state.coordinateDirection; + coordinateDirectionSelect.addEventListener('change', (e) => { + state.coordinateDirection = e.target.value; + saveBotSettings(); + console.log(`🧭 Coordinate direction changed to: ${state.coordinateDirection}`); + Utils.showAlert(`Coordinate direction set to: ${state.coordinateDirection}`, 'success'); + }); + } + + if (coordinateSnakeToggle) { + coordinateSnakeToggle.checked = state.coordinateSnake; + coordinateSnakeToggle.addEventListener('change', (e) => { + state.coordinateSnake = e.target.checked; + saveBotSettings(); + console.log(`šŸ Snake pattern ${state.coordinateSnake ? 'enabled' : 'disabled'}`); + Utils.showAlert( + `Snake pattern ${state.coordinateSnake ? 'enabled' : 'disabled'}`, + 'success' + ); + }); + } + + if (blockWidthInput) { + blockWidthInput.value = state.blockWidth; + blockWidthInput.addEventListener('input', (e) => { + const width = parseInt(e.target.value); + if (width >= 1 && width <= 50) { + state.blockWidth = width; + saveBotSettings(); + } + }); + } + + if (blockHeightInput) { + blockHeightInput.value = state.blockHeight; + blockHeightInput.addEventListener('change', (e) => { + const height = parseInt(e.target.value); + if (height >= 1 && height <= 50) { + state.blockHeight = height; + saveBotSettings(); + } + }); + } + + if (compactBtn) { + compactBtn.addEventListener('click', () => { + container.classList.toggle('wplace-compact'); + const isCompact = container.classList.contains('wplace-compact'); + + if (isCompact) { + compactBtn.innerHTML = ''; + compactBtn.title = Utils.t('expandMode'); + } else { + compactBtn.innerHTML = ''; + compactBtn.title = Utils.t('compactMode'); + } + }); + } + + 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 = Utils.t('restore'); + } else { + container.classList.remove('wplace-minimized'); + content.classList.remove('wplace-hidden'); + minimizeBtn.innerHTML = ''; + minimizeBtn.title = Utils.t('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(isEnabled ? Utils.t('overlayEnabled') : Utils.t('overlayDisabled'), 'info'); + }); + } + + if (state.minimized) { + container.classList.add('wplace-minimized'); + content.classList.add('wplace-hidden'); + if (minimizeBtn) { + minimizeBtn.innerHTML = ''; + minimizeBtn.title = Utils.t('restore'); + } + } else { + container.classList.remove('wplace-minimized'); + content.classList.remove('wplace-hidden'); + if (minimizeBtn) { + minimizeBtn.innerHTML = ''; + minimizeBtn.title = Utils.t('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(Utils.t('errorSavingProgress'), 'error'); + } + }); + } + + if (loadBtn) { + loadBtn.addEventListener('click', async () => { + const savedData = Utils.loadProgress(); + if (!savedData) { + updateUI('noSavedData', 'warning'); + Utils.showAlert(Utils.t('noSavedData'), 'warning'); + return; + } + + // CRITICAL FIX: If save file contains complete data, bypass initial setup check + // This matches old version behavior where save files could be loaded immediately + const hasCompleteData = savedData.state && savedData.imageData && + savedData.state.availableColors && + savedData.state.availableColors.length > 0; + + if (!state.initialSetupComplete && !hasCompleteData) { + Utils.showAlert(Utils.t('pleaseWaitInitialSetup'), '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) { + // CRITICAL FIX: Set initial setup complete if save had complete data + if (hasCompleteData) { + state.initialSetupComplete = true; + } + + updateUI('dataLoaded', 'success'); + Utils.showAlert(Utils.t('dataLoaded'), 'success'); + updateDataButtons(); + + // CRITICAL FIX: Force immediate stats update after save loading + await updateStats(); + + // Force stats display refresh by updating the stats container + const statsContainer = document.getElementById('wplace-stats-container'); + if (statsContainer && typeof updateStats === 'function') { + // Trigger a forced visual refresh of the stats + setTimeout(async () => { + await updateStats(); + }, 100); + } + + // 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; + const loadExtractedBtn = document.getElementById('loadExtractedBtn'); + if (loadExtractedBtn) loadExtractedBtn.disabled = false; + } else { + uploadBtn.disabled = false; + const loadExtractedBtn = document.getElementById('loadExtractedBtn'); + if (loadExtractedBtn) loadExtractedBtn.disabled = false; + selectPosBtn.disabled = false; + } + + if (state.imageLoaded && state.startPosition && state.region && state.colorsChecked) { + startBtn.disabled = false; + } + } else { + Utils.showAlert(Utils.t('errorLoadingProgress'), '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 () => { + console.log('šŸ” [DEBUG] Load from file button clicked'); + console.log('šŸ” [DEBUG] Initial setup complete:', state.initialSetupComplete); + + try { + // First check if we can load the file - this will tell us if it has complete data + console.log('šŸ” [DEBUG] Starting file upload...'); + const fileData = await Utils.createFileUploader(); + + console.log('šŸ” [DEBUG] File data received:', !!fileData); + console.log('šŸ” [DEBUG] File data keys:', fileData ? Object.keys(fileData) : 'N/A'); + + if (!fileData || !fileData.state) { + console.error('āŒ [DEBUG] Invalid file data or missing state'); + console.log('šŸ” [DEBUG] FileData:', fileData); + Utils.showAlert(Utils.t('invalidFileFormat'), 'error'); + return; + } + + // CRITICAL FIX: If file contains complete data, bypass initial setup check + const hasCompleteData = fileData.state && fileData.imageData && + fileData.state.availableColors && + fileData.state.availableColors.length > 0; + + console.log('šŸ” [DEBUG] Has complete data check:'); + console.log(' - fileData.state:', !!fileData.state); + console.log(' - fileData.imageData:', !!fileData.imageData); + console.log(' - fileData.state.availableColors:', !!fileData.state.availableColors); + console.log(' - availableColors length:', fileData.state.availableColors?.length); + console.log(' - hasCompleteData result:', hasCompleteData); + + if (!state.initialSetupComplete && !hasCompleteData) { + console.log('āš ļø [DEBUG] Setup not complete and no complete data, showing warning'); + Utils.showAlert(Utils.t('pleaseWaitFileSetup'), 'warning'); + return; + } + + console.log('šŸ” [DEBUG] Calling restoreProgress...'); + const success = Utils.restoreProgress(fileData); + console.log('šŸ” [DEBUG] RestoreProgress result:', success); + + if (success) { + console.log('āœ… [DEBUG] File loaded successfully'); + + // CRITICAL FIX: Set initial setup complete if file had complete data + if (hasCompleteData) { + console.log('šŸ” [DEBUG] Setting initialSetupComplete to true due to complete data'); + state.initialSetupComplete = true; + } + + updateUI('fileLoaded', 'success'); + Utils.showAlert(Utils.t('fileLoaded'), 'success'); + updateDataButtons(); + + console.log('šŸ” [DEBUG] Updating stats...'); + await updateStats(); + + // Restore overlay if image data was loaded from file + console.log('šŸ” [DEBUG] Attempting to restore overlay...'); + await Utils.restoreOverlayFromData().catch((error) => { + console.error('āŒ [DEBUG] Failed to restore overlay from file:', error); + }); + + console.log('šŸ” [DEBUG] Updating button states after load...'); + console.log(' - state.colorsChecked:', state.colorsChecked); + console.log(' - state.imageLoaded:', state.imageLoaded); + console.log(' - state.startPosition:', !!state.startPosition); + console.log(' - state.region:', !!state.region); + + if (state.colorsChecked) { + console.log('šŸ” [DEBUG] Enabling buttons due to colorsChecked'); + uploadBtn.disabled = false; + selectPosBtn.disabled = false; + resizeBtn.disabled = false; + } else { + console.log('šŸ” [DEBUG] Only enabling upload button'); + uploadBtn.disabled = false; + } + + if (state.imageLoaded && state.startPosition && state.region && state.colorsChecked) { + console.log('šŸ” [DEBUG] All conditions met, enabling start button'); + startBtn.disabled = false; + } else { + console.log('āš ļø [DEBUG] Start button requirements not met'); + } + } else { + console.error('āŒ [DEBUG] RestoreProgress failed'); + Utils.showAlert('File loading failed - check console for details', 'error'); + } + } catch (error) { + console.error('āŒ [DEBUG] Error in file load process:', error); + console.error('āŒ [DEBUG] Error stack:', error.stack); + + if (error.message === 'Invalid JSON file') { + console.error('āŒ [DEBUG] Invalid JSON file detected'); + Utils.showAlert(Utils.t('invalidFileFormat'), 'error'); + } else { + console.error('āŒ [DEBUG] General file error:', error.message); + Utils.showAlert(`File error: ${error.message}`, 'error'); + } + } + }); + } + + updateUI = (messageKey, type = 'default', params = {}, silent = false) => { + const message = Utils.t(messageKey, params); + statusText.textContent = message; + statusText.className = `wplace-status status-${type}`; + + if (!silent) { + // Trigger animation only when silent = false + statusText.style.animation = 'none'; + void statusText.offsetWidth; // trick to restart the animation + statusText.style.animation = 'slide-in 0.3s ease-out'; + } + }; + + function updateChargeStatsDisplay(intervalMs) { + const currentChargesEl = document.getElementById('wplace-stat-charges-value'); + const fullChargeEl = document.getElementById('wplace-stat-fullcharge-value'); + if (!fullChargeEl && !currentChargesEl) return; + if (!state.fullChargeData) { + fullChargeEl.textContent = '--:--:--'; + return; + } + + const { current, max, cooldownMs, startTime, spentSinceShot } = state.fullChargeData; + const elapsed = Date.now() - startTime; + + // total charges including elapsed time and spent during painting since snapshot + const chargesGained = elapsed / cooldownMs; + const rawCharges = current + chargesGained - spentSinceShot; + const cappedCharges = Math.min(rawCharges, max); + + // rounding with 0.95 threshold + let displayCharges; + const fraction = cappedCharges - Math.floor(cappedCharges); + if (fraction >= 0.95) { + displayCharges = Math.ceil(cappedCharges); + } else { + displayCharges = Math.floor(cappedCharges); + } + + state.displayCharges = Math.max(0, displayCharges); + state.preciseCurrentCharges = cappedCharges; + + const remainingMs = getMsToTargetCharges(cappedCharges, max, state.cooldown, intervalMs); + const timeText = Utils.msToTimeText(remainingMs); + + if (currentChargesEl) { + currentChargesEl.innerHTML = `${state.displayCharges} / ${state.maxCharges}`; + } + + if ( + state.displayCharges < state.cooldownChargeThreshold && + !state.stopFlag && + state.running + ) { + updateChargesThresholdUI(intervalMs); + } + + if (fullChargeEl) { + if (state.displayCharges >= max) { + fullChargeEl.innerHTML = `FULL`; + } else { + fullChargeEl.innerHTML = ` + ${timeText} + `; + } + } + } + + updateStats = async (isManualRefresh = false) => { + const isFirstCheck = !state.fullChargeData?.startTime; + + const minUpdateInterval = 60_000; + const maxUpdateInterval = 90_000; + const randomUpdateThreshold = + minUpdateInterval + Math.random() * (maxUpdateInterval - minUpdateInterval); + const timeSinceLastUpdate = Date.now() - (state.fullChargeData?.startTime || 0); + const isTimeToUpdate = timeSinceLastUpdate >= randomUpdateThreshold; + + const shouldCallApi = isFirstCheck || isTimeToUpdate; + + const { charges, max, cooldown } = await WPlaceService.getCharges(); + state.displayCharges = Math.floor(charges); + state.preciseCurrentCharges = charges; + state.cooldown = cooldown; + state.maxCharges = Math.floor(max) > 1 ? Math.floor(max) : state.maxCharges; + + state.fullChargeData = { + current: charges, + max: max, + cooldownMs: cooldown, + startTime: Date.now(), + spentSinceShot: 0, + }; + // Evaluate notifications every time we refresh server-side charges + NotificationManager.maybeNotifyChargesReached(); + + if (state.fullChargeInterval) { + clearInterval(state.fullChargeInterval); + state.fullChargeInterval = null; + } + const intervalMs = 1000; + state.fullChargeInterval = setInterval( + () => updateChargeStatsDisplay(intervalMs), + intervalMs + ); + + if (cooldownSlider && cooldownSlider.max !== state.maxCharges) { + cooldownSlider.max = state.maxCharges; + } + if (cooldownInput && cooldownInput.max !== state.maxCharges) { + cooldownInput.max = state.maxCharges; + } + + // // Update current account charges in the account list + // await updateCurrentAccountInList(); + + let imageStatsHTML = ''; + if (state.imageLoaded) { + const progress = + state.totalPixels > 0 ? Math.round((state.paintedPixels / state.totalPixels) * 100) : 0; + const remainingPixels = state.totalPixels - state.paintedPixels; + + // Updated estimation calculation for new cooldown logic + // Now we wait for cooldownChargeThreshold before processing pixels + + // Calculate batch size (with fallback if function not yet defined) + let batchSize; + try { + batchSize = typeof calculateBatchSize === 'function' ? calculateBatchSize() : state.paintingSpeed || 5; + } catch (e) { + batchSize = state.paintingSpeed || 5; + } + + // Ensure batchSize is valid and not zero + batchSize = Math.max(1, Math.min(batchSize, remainingPixels)); + const batchesNeeded = Math.ceil(remainingPixels / batchSize); + + // Calculate time accounting for cooldown threshold behavior with safety checks + let estimatedMs = 0; + if (remainingPixels > 0 && Number.isFinite(state.cooldown) && state.cooldown > 0) { + // Current charges available check + const currentCharges = Math.max(0, state.displayCharges); + const thresholdCharges = Math.max(1, state.cooldownChargeThreshold || 1); + + if (currentCharges < thresholdCharges) { + // Need to wait for charges to reach threshold first + const chargesNeeded = thresholdCharges - currentCharges; + estimatedMs += chargesNeeded * state.cooldown; + } + + // Calculate painting time more accurately + // Each batch uses thresholdCharges, so calculate how many cycles we need + const pixelsPerCycle = Math.min(batchSize, thresholdCharges); + const cyclesNeeded = Math.ceil(remainingPixels / pixelsPerCycle); + + // Time between cycles (waiting for charges to regenerate) + if (cyclesNeeded > 1) { + const regenerationTime = (cyclesNeeded - 1) * thresholdCharges * state.cooldown; + estimatedMs += regenerationTime; + } + + // Add painting speed delay if enabled + if (CONFIG.PAINTING_SPEED_ENABLED && state.paintingSpeed > 0) { + const paintingDelay = remainingPixels * (1000 / state.paintingSpeed); + estimatedMs += paintingDelay; + } + } + + // Safety check to prevent infinity or invalid values + if (!Number.isFinite(estimatedMs) || estimatedMs < 0) { + estimatedMs = 0; + } + + state.estimatedTime = estimatedMs; + progressBar.style.width = `${progress}%`; + + imageStatsHTML = ` +
+
${Utils.t('progress')}
+
${progress}%
+
+
+
${Utils.t( + 'pixels' + )}
+
${state.paintedPixels}/${state.totalPixels}
+
+ `; + } + + let colorSwatchesHTML = ''; + state.availableColors = state.availableColors.filter( + (c) => c.name !== 'Unknown CoIor NaN' && c.id !== null + ); + + const availableColors = Utils.extractAvailableColors(); + const newCount = Array.isArray(availableColors) ? availableColors.length : 0; + + if (newCount === 0 && isManualRefresh) { + Utils.showAlert(Utils.t('noColorsFound'), 'warning'); + } else if (newCount > 0 && state.availableColors.length < newCount) { + const oldCount = state.availableColors.length; + + Utils.showAlert( + Utils.t('colorsUpdated', { + oldCount, + newCount: newCount, + diffCount: newCount - oldCount, + }), + 'success' + ); + state.availableColors = availableColors; + } + if (state.colorsChecked) { + colorSwatchesHTML = state.availableColors + .map((color) => { + const rgbString = `rgb(${color.rgb.join(',')})`; + return `
`; + }) + .join(''); + } + + // Calculate multi-account statistics + let totalAllCharges = 0; + let totalMaxCharges = 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); + } + + statsArea.innerHTML = ` + ${imageStatsHTML} + ${state.allAccountsInfo.length > 0 ? ` +
+
+ Total All Accounts Charges +
+
+ ${totalAllCharges}/${totalMaxCharges} +
+
+ ` : ''} +
+
+ ${Utils.t('charges')} +
+
+ ${state.displayCharges} / ${state.maxCharges} +
+
+
+
+ ${Utils.t('fullChargeIn')} +
+
--:--:--
+
+ ${state.colorsChecked + ? ` +
+
${Utils.t( + 'availableColors', + { count: state.availableColors.length } + )}
+
+ ${colorSwatchesHTML} +
+
+ ` + : '' + } + `; + + // should be after statsArea.innerHTML = '...'. todo make full stats ui update partial + updateChargeStatsDisplay(intervalMs); + }; + + updateDataButtons = () => { + const hasImageData = state.imageLoaded && state.imageData; + saveBtn.disabled = !hasImageData; + saveToFileBtn.disabled = !hasImageData; + }; + + updateDataButtons(); + + function showMoveArtworkPanel() { + // Create move artwork control panel like status panels + const movePanel = document.createElement('div'); + movePanel.id = 'moveArtworkPanel'; + movePanel.className = 'wplace-move-panel'; + + movePanel.innerHTML = ` +
+
+ + Move Artwork +
+
+ +
+
+
+
+ +
+ +
1px
+ +
+ +
+
+ `; + + // No overlay - append directly to body like other panels + document.body.appendChild(movePanel); + + // Make the panel draggable + makeDraggable(movePanel); + + // Add event listeners for movement + let currentMessageTimeout = null; + + function moveArtwork(deltaX, deltaY) { + if (state.startPosition && state.region) { + const newX = state.startPosition.x + deltaX; + const newY = state.startPosition.y + deltaY; + + console.log(`šŸ”„ Moving artwork from (${state.startPosition.x}, ${state.startPosition.y}) to (${newX}, ${newY})`); + + state.startPosition.x = newX; + state.startPosition.y = newY; + + // Clear any existing message timeout and alerts immediately + if (currentMessageTimeout) { + clearTimeout(currentMessageTimeout); + currentMessageTimeout = null; + } + Utils.hideAlert(); // Hide current alert immediately + + // Update overlay position if available + if (overlayManager) { + overlayManager.setPosition(state.startPosition, state.region) + .then(() => { + console.log('āœ… Overlay position updated'); + Utils.showAlert(`Moved to position (${newX}, ${newY})`, 'success'); + // Set new timeout for this message + currentMessageTimeout = setTimeout(() => { + Utils.hideAlert(); + currentMessageTimeout = null; + }, 1500); // Message disappears after 1.5 seconds + }) + .catch(err => { + console.error('āŒ Failed to update overlay:', err); + Utils.showAlert('Failed to update overlay position', 'error'); + }); + } else { + Utils.showAlert(`Position updated to (${newX}, ${newY})`, 'success'); + // Set new timeout for this message + currentMessageTimeout = setTimeout(() => { + Utils.hideAlert(); + currentMessageTimeout = null; + }, 1500); + } + } else { + Utils.showAlert('Please select a position first before moving artwork', 'warning'); + } + } + + document.getElementById('moveUp').addEventListener('click', () => moveArtwork(0, -1)); + document.getElementById('moveDown').addEventListener('click', () => moveArtwork(0, 1)); + document.getElementById('moveLeft').addEventListener('click', () => moveArtwork(-1, 0)); + document.getElementById('moveRight').addEventListener('click', () => moveArtwork(1, 0)); + + // Close panel + function closeMovePanel() { + if (currentMessageTimeout) { + clearTimeout(currentMessageTimeout); + currentMessageTimeout = null; + } + Utils.hideAlert(); + document.body.removeChild(movePanel); + } + + document.getElementById('closeMovePanel').addEventListener('click', closeMovePanel); + } + + 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; + paintTransparentToggle.checked = state.paintTransparentPixels; + + 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 = + (state.paintTransparentPixels || 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 ( + (!state.paintTransparentPixels && 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(); + saveBotSettings(); + }; + + paintTransparentToggle.onchange = (e) => { + state.paintTransparentPixels = e.target.checked; + _updateResizePreview(); + saveBotSettings(); + }; + + 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 && + (state.paintTransparentPixels || 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 = (!state.paintTransparentPixels && 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 + + await 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 === null || 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 }); + await 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 = + !state.paintTransparentPixels && + 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; + // Keep existing lastPosition to continue from where we left off + // state.lastPosition = { x: 0, y: 0 }; // REMOVED: Don't reset position + + // 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; + } + + await updateStats(); + updateDataButtons(); + updateUI('imageLoaded', 'success', { count: totalValidPixels }); + } catch { + updateUI('imageError', 'error'); + } + }); + } + + // Load Extracted button event listener - for loading Art-Extractor JSON files + const loadExtractedBtn = document.getElementById('loadExtractedBtn'); + if (loadExtractedBtn) { + loadExtractedBtn.addEventListener('click', async () => { + try { + updateUI('loadingImage', 'default'); + const fileData = await Utils.loadExtractedFileData(); + + if (!fileData) { + updateUI('ready', 'default'); + return; + } + + console.log('šŸ“ Loading extracted artwork from Art-Extractor...'); + console.log('šŸ” [DEBUG] Loaded file data structure:', { + hasState: !!fileData.state, + hasImageData: !!fileData.imageData, + topLevelKeys: Object.keys(fileData), + stateKeys: fileData.state ? Object.keys(fileData.state) : 'N/A', + imageDataKeys: fileData.imageData ? Object.keys(fileData.imageData) : 'N/A', + fileDataType: typeof fileData, + stateType: typeof fileData.state, + imageDataType: typeof fileData.imageData + }); + + // Validate the data structure before restoring + if (!fileData || typeof fileData !== 'object') { + throw new Error('Invalid file format: File data is not a valid object'); + } + + if (!fileData.state || typeof fileData.state !== 'object') { + console.error('āŒ State validation failed. FileData:', fileData); + throw new Error('Invalid file format: Missing or invalid state object. Please ensure you exported from Art-Extractor correctly.'); + } + + if (!fileData.imageData || typeof fileData.imageData !== 'object') { + console.error('āŒ ImageData validation failed. FileData:', fileData); + throw new Error('Invalid file format: Missing or invalid imageData object. Please ensure you completed the area scan in Art-Extractor.'); + } + + if (!fileData.imageData.pixels || !Array.isArray(fileData.imageData.pixels) || fileData.imageData.pixels.length === 0) { + console.error('āŒ Pixel data validation failed. ImageData:', fileData.imageData); + throw new Error('Invalid file format: No pixel data found. Please scan an area in Art-Extractor before exporting.'); + } + + // Ensure critical fields are present for Art-Extractor compatibility + if (!fileData.state.availableColors) { + console.warn('āš ļø No availableColors in file, using defaults'); + fileData.state.availableColors = []; + } + + // Use the existing restoreProgress function but with special handling + const restoreSuccess = await Utils.restoreProgress(fileData); + + if (!restoreSuccess) { + throw new Error('Failed to restore progress data'); + } + + // After loading, we need to enter position selection mode like when uploading images + if (state.imageLoaded) { + console.log('šŸŽÆ Extracted artwork loaded, entering position selection mode...'); + + // Clear position data since Art-Extractor exports may have null positions for manual placement + state.startPosition = null; + state.region = null; + state.selectingPosition = true; + + // For extracted files, force enable all necessary flags for full functionality + state.colorsChecked = true; + state.imageLoaded = true; + + // Ensure image processor is available for extracted files + if (state.imageData && !state.imageData.processor) { + try { + // Create a temporary canvas to generate a data URL for the processor + const canvas = document.createElement('canvas'); + canvas.width = state.imageData.width; + canvas.height = state.imageData.height; + const ctx = canvas.getContext('2d'); + + // Create image data from pixels + const imageData = new ImageData( + new Uint8ClampedArray(state.imageData.pixels), + state.imageData.width, + state.imageData.height + ); + ctx.putImageData(imageData, 0, 0); + + // Create processor with canvas data URL using the correct class + const dataUrl = canvas.toDataURL(); + state.imageData.processor = new window.WPlaceImageProcessor(dataUrl); + + // CRITICAL: Load the processor before using it + await state.imageData.processor.load(); + console.log('šŸ”§ Created and loaded WPlaceImageProcessor for extracted artwork'); + } catch (error) { + console.error('āŒ Failed to create image processor for extracted artwork:', error); + // Fallback: use global processor if available + if (window.globalImageProcessor) { + console.log('šŸ”„ Using global image processor as fallback'); + state.imageData.processor = window.globalImageProcessor; + } + } + } + + console.log('šŸŽØ Force-enabled all flags for extracted JSON to access resize panel'); + + // For extracted images, ensure overlay is enabled but don't set position yet + try { + if (overlayManager && state.imageData) { + // Create image bitmap from the restored image data + const canvas = new OffscreenCanvas(state.imageData.width, state.imageData.height); + const ctx = canvas.getContext('2d'); + const imageData = new ImageData( + new Uint8ClampedArray(state.imageData.pixels), + state.imageData.width, + state.imageData.height + ); + ctx.putImageData(imageData, 0, 0); + const imageBitmap = await canvas.transferToImageBitmap(); + + await overlayManager.setImage(imageBitmap); + overlayManager.enable(); + console.log('āœ… Overlay enabled for extracted artwork'); + } + } catch (overlayError) { + console.warn('āš ļø Could not set overlay for extracted artwork:', overlayError); + } + + // For extracted images, don't try to set overlay position until user selects one + // The overlay should already be enabled from the restoreProgress function + + // Update UI with custom message for extracted artwork + Utils.showAlert('šŸŽØ Extracted artwork loaded! Please click on the canvas to select where to place it.', 'info'); + updateUI('ready', 'default'); // Use 'ready' instead of 'waitingPosition' to avoid translation issues + + // Enable relevant buttons + selectPosBtn.disabled = false; + // Always enable resize button for extracted files since they have image processor + resizeBtn.disabled = false; + console.log('šŸ”§ Resize button enabled for extracted artwork'); + + // Enable move artwork button (will be fully enabled after position is set) + const moveArtworkBtn = document.getElementById('moveArtworkBtn'); + if (moveArtworkBtn) { + moveArtworkBtn.disabled = false; + } + + // Set up position selection like the regular selectPosBtn + const tempFetch = async (url, options) => { + if ( + typeof url === 'string' && + url.includes('/s0/pixel/') && + options && + options.method === 'POST' + ) { + let coords; + try { + const body = JSON.parse(options.body); + coords = body.coords; + } catch (e) { + return window.originalFetch(url, options); + } + + const tileMatches = url.match(/\/s0\/pixel\/(\-?\d+)\/(\-?\d+)/); + if (tileMatches && coords && coords.length >= 2) { + const tileX = parseInt(tileMatches[1]); + const tileY = parseInt(tileMatches[2]); + const pixelX = coords[0]; + const pixelY = coords[1]; + + state.startPosition = { x: pixelX, y: pixelY }; + state.region = { x: tileX, y: tileY }; + state.selectingPosition = false; + + console.log('šŸŽÆ Position selected for extracted artwork:', { + startPosition: state.startPosition, + region: state.region + }); + + // Restore original fetch + window.fetch = window.originalFetch; + + // Update overlay position with validation + try { + if (state.startPosition && state.region && overlayManager) { + await overlayManager.setPosition(state.startPosition, state.region); + console.log('āœ… Overlay position updated successfully'); + } else { + console.warn('āš ļø Cannot set overlay position: missing startPosition or region'); + } + } catch (positionError) { + console.error('āŒ Failed to set overlay position:', positionError); + } + + startBtn.disabled = false; + selectPosBtn.textContent = Utils.t('selectPosition'); + + // Enable Move Artwork button when position is set + const moveArtworkBtn = document.getElementById('moveArtworkBtn'); + if (moveArtworkBtn) { + moveArtworkBtn.disabled = false; + } + + updateUI('ready', 'success'); + Utils.showAlert('Position selected! Ready to start painting.', 'success'); + } + } + return window.originalFetch(url, options); + }; + + // Store original fetch and replace with position capture + if (!window.originalFetch) { + window.originalFetch = window.fetch; + } + window.fetch = tempFetch; + } + + } catch (error) { + console.error('āŒ Failed to load extracted artwork:', error); + Utils.showAlert(`Failed to load extracted artwork: ${error.message}`, 'error'); + updateUI('ready', 'default'); + } + }); + } + + if (resizeBtn) { + resizeBtn.addEventListener('click', () => { + console.log('šŸ” [DEBUG] Resize button clicked. State check:', { + imageLoaded: state.imageLoaded, + hasProcessor: !!(state.imageData && state.imageData.processor), + colorsChecked: state.colorsChecked, + hasAvailableColors: !!(state.availableColors && state.availableColors.length > 0) + }); + + if (state.imageLoaded && state.imageData && state.imageData.processor) { + showResizeDialog(state.imageData.processor); + } else { + let message = 'Please upload an image and check colors first.'; + if (!state.imageLoaded) message = 'Please upload an image first.'; + else if (!state.imageData || !state.imageData.processor) message = 'Image processor not available. Please reload the image.'; + Utils.showAlert(message, 'warning'); + } + }); + } + + // Move Artwork button event listener + const moveArtworkBtn = document.getElementById('moveArtworkBtn'); + if (moveArtworkBtn) { + moveArtworkBtn.addEventListener('click', () => { + if (state.imageLoaded && (state.startPosition || state.selectingPosition)) { + showMoveArtworkPanel(); + } else if (!state.imageLoaded) { + Utils.showAlert('Please upload or load an image first', 'warning'); + } else { + Utils.showAlert('Please select a position for the artwork first', 'warning'); + } + }); + } + + if (selectPosBtn) { + selectPosBtn.addEventListener('click', async () => { + if (state.selectingPosition) return; + + state.selectingPosition = true; + state.startPosition = null; + state.region = null; + startBtn.disabled = true; + + // Disable Move Artwork button when selecting new position + const moveArtworkBtn = document.getElementById('moveArtworkBtn'); + if (moveArtworkBtn) { + moveArtworkBtn.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], + }; + // Keep existing lastPosition to continue from where we left off + // state.lastPosition = { x: 0, y: 0 }; // REMOVED: Don't reset position + + // Update overlay position with validation + try { + if (state.startPosition && state.region && overlayManager) { + await overlayManager.setPosition(state.startPosition, state.region); + console.log('āœ… Regular overlay position updated successfully'); + } else { + console.warn('āš ļø Cannot set regular overlay position: missing startPosition or region'); + } + } catch (positionError) { + console.error('āŒ Failed to set regular overlay position:', positionError); + } + + if (state.imageLoaded) { + startBtn.disabled = false; + + // Enable Move Artwork button when position is set + const moveArtworkBtn = document.getElementById('moveArtworkBtn'); + if (moveArtworkBtn) { + moveArtworkBtn.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; + } + await ensureToken(); + if (!getTurnstileToken()) return; + + // Only reset painted pixels on first start of session (when pre-filtering hasn't been done) + if (!state.preFilteringDone) { + const savedPaintedPixels = state.paintedPixels; // Store original value + state.paintedPixels = 0; + console.log(`šŸ”„ First start this session - reset progress counter for accurate tracking (was: ${savedPaintedPixels})`); + await updateStats(); // Update UI to show 0 progress + } else { + console.log('šŸ”„ Continuing session - pre-filtering already done, keeping current progress'); + } + + 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(); + } catch (e) { + console.error('Unexpected error:', e); + updateUI('paintingError', 'error'); + } finally { + state.running = false; + stopBtn.disabled = true; + saveBtn.disabled = false; + + if (state.stopFlag) { + startBtn.disabled = false; + } else { + startBtn.disabled = true; + uploadBtn.disabled = false; + const loadExtractedBtn = document.getElementById('loadExtractedBtn'); + if (loadExtractedBtn) loadExtractedBtn.disabled = false; + selectPosBtn.disabled = false; + resizeBtn.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('paintingStoppedByUser', '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 && cooldownInput && cooldownValue && cooldownDecrease && cooldownIncrease) { + const updateCooldown = (newValue) => { + const threshold = Math.max(1, Math.min(state.maxCharges || 999, parseInt(newValue))); + state.cooldownChargeThreshold = threshold; + + // Update both controls (value shows in input, label shows unit only) + cooldownSlider.value = threshold; + cooldownInput.value = threshold; + cooldownValue.textContent = `${Utils.t('charges')}`; + + saveBotSettings(); + NotificationManager.resetEdgeTracking(); // prevent spurious notify after threshold change + }; + + // Slider event listener + cooldownSlider.addEventListener('input', (e) => { + updateCooldown(e.target.value); + }); + + // Number input event listener + cooldownInput.addEventListener('input', (e) => { + updateCooldown(e.target.value); + }); + + // Decrease button + cooldownDecrease.addEventListener('click', () => { + updateCooldown(parseInt(cooldownInput.value) - 1); + }); + + // Increase button + cooldownIncrease.addEventListener('click', () => { + updateCooldown(parseInt(cooldownInput.value) + 1); + }); + + // Add scroll-to-adjust for cooldown slider + Utils.createScrollToAdjust(cooldownSlider, updateCooldown, 1, state.maxCharges, 1); + } + + loadBotSettings(); + // Ensure notification poller reflects current settings + NotificationManager.syncFromState(); + } + + function getMsToTargetCharges(current, target, cooldown, intervalMs = 0) { + const remainingCharges = target - current; + return Math.max(0, remainingCharges * cooldown - intervalMs); + } + + function updateChargesThresholdUI(intervalMs) { + if (state.stopFlag) return; + + const threshold = state.cooldownChargeThreshold; + const remainingMs = getMsToTargetCharges( + state.preciseCurrentCharges, + threshold, + state.cooldown, + intervalMs + ); + const timeText = Utils.msToTimeText(remainingMs); + + updateUI( + 'noChargesThreshold', + 'warning', + { + threshold, + current: state.displayCharges, + time: timeText, + }, + true + ); + } + + function generateCoordinates(width, height, mode, direction, snake, blockWidth, blockHeight, startFromX = 0, startFromY = 0) { + const coords = []; + console.log( + 'Generating coordinates with \n mode:', + mode, + '\n direction:', + direction, + '\n snake:', + snake, + '\n blockWidth:', + blockWidth, + '\n blockHeight:', + blockHeight, + '\n startFromX:', + startFromX, + '\n startFromY:', + startFromY + ); + // --------- Standard 4 corners traversal ---------- + let xStart, xEnd, xStep; + let yStart, yEnd, yStep; + switch (direction) { + case 'top-left': + xStart = 0; + xEnd = width; + xStep = 1; + yStart = 0; + yEnd = height; + yStep = 1; + break; + case 'top-right': + xStart = width - 1; + xEnd = -1; + xStep = -1; + yStart = 0; + yEnd = height; + yStep = 1; + break; + case 'bottom-left': + xStart = 0; + xEnd = width; + xStep = 1; + yStart = height - 1; + yEnd = -1; + yStep = -1; + break; + case 'bottom-right': + xStart = width - 1; + xEnd = -1; + xStep = -1; + yStart = height - 1; + yEnd = -1; + yStep = -1; + break; + default: + throw new Error(`Unknown direction: ${direction}`); + } + + // --------- Traversal modes ---------- + if (mode === 'rows') { + for (let y = yStart; y !== yEnd; y += yStep) { + if (snake && (y - yStart) % 2 !== 0) { + for (let x = xEnd - xStep; x !== xStart - xStep; x -= xStep) { + coords.push([x, y]); + } + } else { + for (let x = xStart; x !== xEnd; x += xStep) { + coords.push([x, y]); + } + } + } + } else if (mode === 'columns') { + for (let x = xStart; x !== xEnd; x += xStep) { + if (snake && (x - xStart) % 2 !== 0) { + for (let y = yEnd - yStep; y !== yStart - yStep; y -= yStep) { + coords.push([x, y]); + } + } else { + for (let y = yStart; y !== yEnd; y += yStep) { + coords.push([x, y]); + } + } + } + } else if (mode === 'circle-out') { + const cx = Math.floor(width / 2); + const cy = Math.floor(height / 2); + const maxRadius = Math.ceil(Math.sqrt(cx * cx + cy * cy)); + + for (let r = 0; r <= maxRadius; r++) { + for (let y = cy - r; y <= cy + r; y++) { + for (let x = cx - r; x <= cx + r; x++) { + if (x >= 0 && x < width && y >= 0 && y < height) { + const dist = Math.max(Math.abs(x - cx), Math.abs(y - cy)); + if (dist === r) coords.push([x, y]); + } + } + } + } + } else if (mode === 'circle-in') { + const cx = Math.floor(width / 2); + const cy = Math.floor(height / 2); + const maxRadius = Math.ceil(Math.sqrt(cx * cx + cy * cy)); + + for (let r = maxRadius; r >= 0; r--) { + for (let y = cy - r; y <= cy + r; y++) { + for (let x = cx - r; x <= cx + r; x++) { + if (x >= 0 && x < width && y >= 0 && y < height) { + const dist = Math.max(Math.abs(x - cx), Math.abs(y - cy)); + if (dist === r) coords.push([x, y]); + } + } + } + } + } else if (mode === 'blocks' || mode === 'shuffle-blocks') { + const blocks = []; + for (let by = 0; by < height; by += blockHeight) { + for (let bx = 0; bx < width; bx += blockWidth) { + const block = []; + for (let y = by; y < Math.min(by + blockHeight, height); y++) { + for (let x = bx; x < Math.min(bx + blockWidth, width); x++) { + block.push([x, y]); + } + } + blocks.push(block); + } + } + + if (mode === 'shuffle-blocks') { + // Simple Fisher-Yates shuffle + for (let i = blocks.length - 1; i > 0; i--) { + const j = Math.floor(Math.random() * (i + 1)); + [blocks[i], blocks[j]] = [blocks[j], blocks[i]]; + } + } + + // Concatenate all blocks + for (const block of blocks) { + coords.push(...block); + } + } else { + throw new Error(`Unknown mode: ${mode}`); + } + + // Filter coordinates to start from the specified position + if (startFromX > 0 || startFromY > 0) { + console.log(`šŸ”„ Filtering coordinates to resume from position (${startFromX}, ${startFromY})`); + let startIndex = -1; + + // Find the starting position in the coordinate list + for (let i = 0; i < coords.length; i++) { + const [x, y] = coords[i]; + if (x === startFromX && y === startFromY) { + startIndex = i; + break; + } + } + + if (startIndex >= 0) { + // Resume from the found position (skip all previous coordinates) + const filteredCoords = coords.slice(startIndex); + console.log(`āœ‚ļø Resuming: skipped ${startIndex} coordinates, continuing with ${filteredCoords.length} remaining`); + return filteredCoords; + } else { + console.warn(`āš ļø Resume position (${startFromX}, ${startFromY}) not found in coordinate list, starting from beginning`); + } + } + + return coords; + } + + async function flushPixelBatch(pixelBatch) { + if (!pixelBatch || pixelBatch.pixels.length === 0) return true; + + const batchSize = pixelBatch.pixels.length; + console.log( + `šŸ“¦ Sending batch with ${batchSize} pixels (region: ${pixelBatch.regionX},${pixelBatch.regionY})` + ); + const success = await sendBatchWithRetry( + pixelBatch.pixels, + pixelBatch.regionX, + pixelBatch.regionY + ); + if (success) { + pixelBatch.pixels.forEach((p) => { + state.paintedPixels++; + Utils.markPixelPainted(p.x, p.y, pixelBatch.regionX, pixelBatch.regionY); + }); + + // IMPORTANT: Decrement charges locally to match Acc-Switch.js behavior + state.displayCharges = Math.max(0, state.displayCharges - batchSize); + state.preciseCurrentCharges = Math.max(0, state.preciseCurrentCharges - batchSize); + + state.fullChargeData = { + ...state.fullChargeData, + spentSinceShot: state.fullChargeData.spentSinceShot + batchSize, + }; + await updateStats(); + // Update account list with new charges + await updateCurrentAccountInList(); + // Progress tracking removed from UI to reduce visual clutter + Utils.performSmartSave(); + + if (CONFIG.PAINTING_SPEED_ENABLED && state.paintingSpeed > 0 && batchSize > 0) { + const delayPerPixel = 1000 / state.paintingSpeed; + const totalDelay = Math.max(100, delayPerPixel * batchSize); + await Utils.sleep(totalDelay); + } + } else { + console.error(`āŒ Batch failed permanently after retries. Stopping painting.`); + state.stopFlag = true; + updateUI('paintingBatchFailed', 'error'); + } + + pixelBatch.pixels = []; + return success; + } + + async function processImage() { + console.log('šŸš€ Starting auto-swap enabled painting workflow'); + + try { + // Main painting cycle - repeats until image complete or stopped + while (!state.stopFlag) { + console.log('šŸ“‹ Phase 1: Starting painting session'); + const paintingResult = await executePaintingSession(); + + if (paintingResult === 'completed') { + console.log('šŸŽ‰ Image painting completed!'); + break; + } + + if (paintingResult === 'stopped') { + console.log('ā¹ļø Painting stopped by user'); + break; + } + + if (paintingResult === 'charges_depleted') { + if (!CONFIG.autoSwap) { + // Original workflow: cooldown period + console.log('ā±ļø Phase 2: Entering cooldown period (auto-swap disabled)'); + const cooldownResult = await executeCooldownPeriod(); + + if (cooldownResult === 'stopped') { + console.log('ā¹ļø Cooldown stopped by user'); + break; + } + + // Phase 3: Regenerate token for next painting session + console.log('šŸ”‘ Phase 3: Regenerating token for next session'); + const tokenResult = await regenerateTokenForNewSession(); + + if (!tokenResult) { + console.log('āŒ Failed to regenerate token, stopping'); + state.stopFlag = true; + break; + } + } else { + // Auto-swap workflow: simplified logic + console.log('šŸ”„ Auto-swap enabled: checking account switching options'); + + // Retrieve fresh accounts list each time to avoid stale data + const accounts = JSON.parse(localStorage.getItem("accounts")) || []; + console.log(`šŸ“Š Retrieved ${accounts.length} accounts from localStorage`); + + if (accounts.length <= 1) { + console.log('šŸ“‹ Only one account available, using standard cooldown'); + const cooldownResult = await executeCooldownPeriod(); + if (cooldownResult === 'stopped') break; + + const tokenResult = await regenerateTokenForNewSession(); + if (!tokenResult) { + state.stopFlag = true; + break; + } + } else { + // Debug current state + console.log(`šŸ“Š Account Status - Current ID: ${state.originalAccountOrder[state.currentActiveIndex]?.orderId}, Total IDs: ${state.originalAccountOrder.length}`); + + // Check if we're at the last ID in the sequence + const isLastAccount = state.currentActiveIndex >= state.originalAccountOrder.length - 1; + console.log(`šŸ” Is last ID? ${isLastAccount} (ID ${state.originalAccountOrder[state.currentActiveIndex]?.orderId} of ${state.originalAccountOrder.length})`); + + if (!isLastAccount) { + // Switch to next ID immediately (no cooldown) + const nextId = state.originalAccountOrder[state.currentActiveIndex + 1]?.orderId; + console.log(`šŸ”„ Switching to next ID ${nextId} (${state.currentActiveIndex + 2}/${state.originalAccountOrder.length})`); + const switchResult = await switchToNextAccount(accounts); + if (!switchResult) { + console.log('āŒ Account switch failed, stopping'); + state.stopFlag = true; + break; + } + // Continue painting with new account immediately + continue; + } else { + // Last account reached - use cooldown then switch to ID 1 (first account) + console.log('ā±ļø Last account reached, entering cooldown period'); + console.log(`šŸ“Š Current ID: ${state.originalAccountOrder[state.currentActiveIndex]?.orderId}, Last ID: ${state.originalAccountOrder[state.originalAccountOrder.length - 1]?.orderId}`); + + const cooldownResult = await executeCooldownPeriod(); + if (cooldownResult === 'stopped') break; + + // After cooldown, switch to ID 1 (first account in order) + console.log('šŸ” Cooldown complete, switching to ID 1 (first account)'); + console.log(`šŸ”„ Before switch - Current ID: ${state.originalAccountOrder[state.currentActiveIndex]?.orderId}, Target: ID 1`); + + const firstAccountInfo = state.originalAccountOrder[0]; // ID 1 is always at index 0 + const firstAccountToken = firstAccountInfo?.token; + + if (!firstAccountToken) { + console.log('āŒ First account (ID 1) token not found, stopping'); + state.stopFlag = true; + break; + } + + // Reset to first position (ID 1) + state.currentActiveIndex = 0; + + // Update accountIndex to match the position in accounts array + const accountsIndex = accounts.findIndex(token => token === firstAccountToken); + if (accountsIndex !== -1) { + state.accountIndex = accountsIndex; + } + + const switchResult = await switchToSpecificAccount(firstAccountToken, 0); + if (!switchResult) { + console.log('āŒ Switch to ID 1 failed, stopping'); + state.stopFlag = true; + break; + } + + console.log(`āœ… Successfully switched to ID 1 (${firstAccountInfo.displayName}). Cycle restarted.`); + // Continue painting with first account + continue; + } + } + } + } + + console.log('šŸ”„ Cycle complete, starting next painting session'); + } + } finally { + await finalizePaintingProcess(); + } + } + + // Phase 1: Execute a complete painting session using all available charges + async function executePaintingSession() { + console.log('šŸŽØ Starting painting session - using all charges until 0'); + const { width, height, pixels } = state.imageData; + const { x: startX, y: startY } = state.startPosition; + const { x: regionX, y: regionY } = state.region; + + // Check if we're working with restored data by looking for existing availableColors + const isRestoredData = state.availableColors && state.availableColors.length > 0 && state.colorsChecked; + + if (!isRestoredData) { + // Wait for original tiles to load if needed + const tilesReady = await overlayManager.waitForTiles( + regionX, + regionY, + width, + height, + startX, + startY, + 10000 // timeout 10s + ); + + if (!tilesReady) { + updateUI('overlayTilesNotLoaded', 'error'); + state.stopFlag = true; + return 'stopped'; + } + } + + let pixelBatch = null; + let skippedPixels = { + transparent: 0, + white: 0, + alreadyPainted: 0, + colorUnavailable: 0, + }; + + // IMPORTANT: Check charges once at start, then paint until depleted + console.log('šŸ”‹ Checking initial charges for painting session'); + const initialChargeCheck = await WPlaceService.getCharges(); + state.displayCharges = Math.floor(initialChargeCheck.charges); + state.preciseCurrentCharges = initialChargeCheck.charges; + state.cooldown = initialChargeCheck.cooldown; + await updateStats(); + if (state.displayCharges <= 0) { + console.log('⚔ No charges available, skipping painting session'); + return 'charges_depleted'; + } + + console.log(`šŸ”‹ Starting with ${state.displayCharges} charges - painting until depleted`); + + // Paint pixels until we run out of charges or complete the image + try { + const coords = generateCoordinates( + width, + height, + state.coordinateMode, + state.coordinateDirection, + state.coordinateSnake, + state.blockWidth, + state.blockHeight, + state.lastPosition.x, + state.lastPosition.y + ); + + // OPTIMIZATION: Pre-filter already painted pixels (happens only once per session) + let eligibleCoords = []; + let alreadyPaintedCount = 0; + + // Log resume information if applicable + if (state.lastPosition.x > 0 || state.lastPosition.y > 0) { + console.log(`šŸ”„ Resuming painting from position (${state.lastPosition.x}, ${state.lastPosition.y})`); + console.log(`šŸ“Š Current progress: ${state.paintedPixels} pixels painted`); + } + + if (!state.preFilteringDone) { + console.log('šŸ” Pre-filtering already painted pixels (one-time detection for this session)...'); + + for (const [x, y] of coords) { + const targetPixelInfo = checkPixelEligibility(x, y); + + if (!targetPixelInfo.eligible) { + if (targetPixelInfo.reason !== 'alreadyPainted') { + skippedPixels[targetPixelInfo.reason]++; + + } + continue; + } + + // Check if already painted (only once per session) + 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; + + try { + const tilePixelRGBA = await overlayManager.getTilePixelColor( + regionX + adderX, + regionY + adderY, + pixelX, + pixelY + ); + + if (tilePixelRGBA && Array.isArray(tilePixelRGBA)) { + const mappedCanvasColor = Utils.resolveColor( + tilePixelRGBA.slice(0, 3), + state.availableColors, + !state.paintUnavailablePixels // Use same parameter as target pixel + ); + const isMatch = mappedCanvasColor.id === targetPixelInfo.mappedColorId; + if (isMatch) { + alreadyPaintedCount++; + // Add detected already-painted pixels to progress (after reset to 0) + state.paintedPixels++; + Utils.markPixelPainted(x, y, regionX + adderX, regionY + adderY); + continue; // Skip already painted pixels + } + } + } catch (e) { + // If we can't check, include the pixel (better to attempt than skip) + } + + // Add eligible unpainted pixel to list + eligibleCoords.push([x, y, targetPixelInfo]); + } + + // Mark pre-filtering as done for this session + state.preFilteringDone = true; + + // Log pre-filtering results + if (alreadyPaintedCount > 0) { + console.log(`āœ“ Pre-filter complete: ${alreadyPaintedCount} already painted pixels detected and added to progress`); + console.log('ā„¹ļø This detection will not happen again until a new image/save is loaded'); + // Update UI to reflect the new progress immediately + await updateStats(); + } + skippedPixels.alreadyPainted = alreadyPaintedCount; + } else { + // Pre-filtering already done this session, just filter for basic eligibility + console.log('šŸ” Using existing pre-filter results (already done this session)'); + for (const [x, y] of coords) { + const targetPixelInfo = checkPixelEligibility(x, y); + + if (!targetPixelInfo.eligible) { + if (targetPixelInfo.reason !== 'alreadyPainted') { + skippedPixels[targetPixelInfo.reason]++; + } + continue; + } + + // Only include pixels that haven't been marked as painted yet + let absX = startX + x; + let absY = startY + y; + let adderX = Math.floor(absX / 1000); + let adderY = Math.floor(absY / 1000); + + if (!Utils.isPixelPainted(x, y, regionX + adderX, regionY + adderY)) { + eligibleCoords.push([x, y, targetPixelInfo]); + } + } + } + + // Paint eligible pixels (already pre-filtered, no duplicate checks) + outerLoop: for (const [x, y, targetPixelInfo] of eligibleCoords) { + if (state.stopFlag) { + if (pixelBatch && pixelBatch.pixels.length > 0) { + console.log(`šŸŽÆ Sending last batch before stop with ${pixelBatch.pixels.length} pixels`); + await flushPixelBatch(pixelBatch); + } + state.lastPosition = { x, y }; + // Removed 'Paused at' message from main panel to reduce UI clutter + return 'stopped'; + } + + // Check if we have charges left (local count, no API call) + if (state.displayCharges <= 0) { + // console.log("Try to buy paint charges"); + // await purchase("paint_charges"); + await updateStats(); + if (state.displayCharges <= 0) { + console.log('⚔ No charges left (local count), ending painting session'); + if (pixelBatch && pixelBatch.pixels.length > 0) { + console.log(`šŸŽÆ Sending final batch with ${pixelBatch.pixels.length} pixels`); + await flushPixelBatch(pixelBatch); + } + state.lastPosition = { x, y }; + return 'charges_depleted'; + } + // else { + // console.log(`šŸ”‹ Charges after purchase: ${state.displayCharges}, continuing painting`); + // } + } + + 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; + + const targetMappedColorId = targetPixelInfo.mappedColorId; + + // Set up pixel batch for new region if needed + 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`); + const success = await flushPixelBatch(pixelBatch); + if (!success) { + console.error(`āŒ Batch failed permanently after retries. Stopping painting.`); + state.stopFlag = true; + return 'stopped'; + } + await updateStats(); + } + + pixelBatch = { + regionX: regionX + adderX, + regionY: regionY + adderY, + pixels: [], + }; + } + + // Add pixel to batch (no need to check again - already pre-filtered) + pixelBatch.pixels.push({ + x: pixelX, + y: pixelY, + color: targetMappedColorId, + localX: x, + localY: y, + }); + + // Send batch if it's full + const maxBatchSize = calculateBatchSize(); + if (pixelBatch.pixels.length >= maxBatchSize) { + console.log(`šŸ“¦ Sending batch with ${pixelBatch.pixels.length} pixels`); + const success = await flushPixelBatch(pixelBatch); + if (!success) { + console.error(`āŒ Batch failed permanently after retries. Stopping painting.`); + state.stopFlag = true; + return 'stopped'; + } + pixelBatch.pixels = []; + await updateStats(); + } + } + + // Send final batch if any pixels remain + if (pixelBatch && pixelBatch.pixels.length > 0 && !state.stopFlag) { + console.log(`šŸ Sending final batch with ${pixelBatch.pixels.length} pixels`); + const success = await flushPixelBatch(pixelBatch); + if (!success) { + console.warn(`āš ļø Final batch failed with ${pixelBatch.pixels.length} pixels`); + } + } + + // If we completed the entire coordinate loop, image is complete + return state.stopFlag ? 'stopped' : 'completed'; + + } finally { + // Log skip statistics for this session + console.log(`šŸ“Š Session Statistics:`); + console.log(` New pixels painted: ${state.paintedPixels - (skippedPixels.alreadyPainted || 0)}`); + console.log(` Already painted detected: ${skippedPixels.alreadyPainted}`); + console.log(` Total progress: ${state.paintedPixels}`); + console.log(` Pre-filtered - Transparent: ${skippedPixels.transparent}`); + console.log(` Pre-filtered - White: ${skippedPixels.white}`); + console.log(` Pre-filtered - Color Unavailable: ${skippedPixels.colorUnavailable}`); + } + } + + // Phase 2: Execute cooldown period - wait for target charges (NO token regeneration) + async function executeCooldownPeriod() { + console.log('ā±ļø Entering cooldown period - waiting for target charges'); + console.log('🚫 NO token regeneration during cooldown (even if expired/invalid)'); + + // Check initial charges to calculate wait time + let chargeCheckCount = 0; + // const maxChargeChecks = 10; // REMOVED: No limit on API calls during cooldown + + while (!state.stopFlag) { + chargeCheckCount++; + + const { charges, cooldown } = await WPlaceService.getCharges(); + state.displayCharges = Math.floor(charges); + state.preciseCurrentCharges = charges; + state.cooldown = cooldown; + + if (state.displayCharges >= state.cooldownChargeThreshold) { + console.log(`āœ… Cooldown target reached: ${state.displayCharges}/${state.cooldownChargeThreshold}`); + NotificationManager.maybeNotifyChargesReached(true); + await updateStats(); + return 'target_reached'; + } + + updateUI('noChargesThreshold', 'warning', { + time: Utils.msToTimeText(state.cooldown), + threshold: state.cooldownChargeThreshold, + current: state.displayCharges, + }); + await updateStats(); + + // Smart delay calculation to reduce API calls + const chargesNeeded = state.cooldownChargeThreshold - state.displayCharges; + const estimatedWaitTime = chargesNeeded * state.cooldown; + + // Use longer delays during cooldown to prevent rate limiting + let delayTime; + if (chargeCheckCount < 3) { + // First few checks - shorter delay + delayTime = Math.max(3000, state.cooldown); // 3 seconds minimum + } else if (estimatedWaitTime > 60000) { + // Long wait expected - check every 15 seconds + delayTime = 10000; + } else if (estimatedWaitTime > 30000) { + // Medium wait - check every 15 seconds + delayTime = 15000; + } else { + // Close to target - check every 5 seconds + delayTime = 5000; + } + + console.log(`ā±ļø Cooldown check ${chargeCheckCount}: ${state.displayCharges}/${state.cooldownChargeThreshold} charges, waiting 10s before next check`); + await Utils.sleep(10000); + + // REMOVED: No limit on charge checks - bot will wait infinitely until charges are available + // if (chargeCheckCount >= maxChargeChecks) { + // console.warn('āš ļø Max charge checks reached during cooldown, continuing anyway'); + // break; + // } + } + + return 'stopped'; + } + + // Phase 3: Regenerate token for new painting session + async function regenerateTokenForNewSession() { + console.log('šŸ”‘ Regenerating token for new painting session'); + + try { + // Force regenerate token for new session + await ensureToken(true); // forceRefresh = true + + if (!getTurnstileToken()) { + console.error('āŒ Failed to generate token for new session'); + return false; + } + + console.log('āœ… Token regenerated successfully for new session'); + return true; + } catch (error) { + console.error('āŒ Token regeneration failed:', error); + return false; + } + } + + // Finalize painting process cleanup + async function finalizePaintingProcess() { + console.log('🧹 Finalizing painting process'); + + if (window._chargesInterval) { + clearInterval(window._chargesInterval); + window._chargesInterval = null; + } + + if (state.stopFlag) { + // Save progress when stopped to preserve painted map + Utils.saveProgress(); + } else { + updateUI('paintingComplete', 'success', { count: state.paintedPixels }); + state.lastPosition = { x: 0, y: 0 }; // Only reset when truly complete + Utils.saveProgress(); // Save final complete state + overlayManager.clear(); + const toggleOverlayBtn = document.getElementById('toggleOverlayBtn'); + if (toggleOverlayBtn) { + toggleOverlayBtn.classList.remove('active'); + toggleOverlayBtn.disabled = true; + } + } + } + + // Helper function to check pixel eligibility (shared by painting functions) + function checkPixelEligibility(x, y) { + const { width, height, pixels } = state.imageData; + const transparencyThreshold = state.customTransparencyThreshold || CONFIG.TRANSPARENCY_THRESHOLD; + + // CRITICAL FIX: Check module availability before processing + if (!Utils || typeof Utils.isWhitePixel !== 'function') { + console.error('āŒ Utils module not available for pixel eligibility check'); + return { + eligible: false, + reason: 'moduleUnavailable', + }; + } + + const idx = (y * width + x) * 4; + const r = pixels[idx], + g = pixels[idx + 1], + b = pixels[idx + 2], + a = pixels[idx + 3]; + + if (!state.paintTransparentPixels && a < transparencyThreshold) + return { + eligible: false, + reason: 'transparent', + }; + if (!state.paintWhitePixels && Utils.isWhitePixel(r, g, b)) + return { + eligible: false, + reason: 'white', + }; + + let targetRgb = Utils.isWhitePixel(r, g, b) + ? [255, 255, 255] + : Utils.findClosestPaletteColor(r, g, b, state.activeColorPalette); + + const mappedTargetColorId = Utils.resolveColor( + targetRgb, + state.availableColors, + !state.paintUnavailablePixels + ); + + if (!state.paintUnavailablePixels && !mappedTargetColorId.id) { + return { + eligible: false, + reason: 'colorUnavailable', + r, + g, + b, + a, + mappedColorId: mappedTargetColorId.id, + }; + } + return { eligible: true, r, g, b, a, mappedColorId: mappedTargetColorId.id }; + } + + // Helper function to skip pixel and log the reason (minimized logging) + function skipPixel(reason, id, rgb, x, y, skippedPixels) { + // Minimize logging to prevent console flooding - only log non-routine skips + if (reason !== 'transparent' && reason !== 'alreadyPainted') { + console.log(`Skipped pixel for ${reason} (id: ${id}, (${rgb.join(', ')})) at (${x}, ${y})`); + } + skippedPixels[reason]++; + } + 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 = state.displayCharges; + 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} - no token available during processing`); + console.log(`āŒ Stopping batch processing - tokens must be generated at startup/start button only`); + updateUI('captchaFailed', 'error'); + return false; // Stop processing entirely - don't regenerate during processing + } else if (result === 'token_regenerated') { + console.log(`šŸ”„ Token regenerated on attempt ${attempt} after 403 error - retrying batch`); + updateUI('paintingPaused', 'warning', { message: 'Token refreshed, resuming...' }); + // Don't count token regeneration as a failed attempt, retry immediately + attempt--; + await Utils.sleep(500); // Brief pause before retry + continue; + } else if (result === 'token_regeneration_failed') { + console.log(`āŒ Token regeneration failed on attempt ${attempt} after 403 error`); + updateUI('captchaFailed', 'error'); + return false; // Stop processing if we can't get a valid token + } else if (result === 'invalid_token_error') { + console.log(`šŸ”‘ Invalid token detected on attempt ${attempt}, regenerating...`); + updateUI('captchaSolving', 'warning'); + try { + await handleCaptcha(true); // Allow generation for invalid token cases + // Don't count token regeneration as a failed attempt + attempt--; + continue; + } catch (e) { + console.error(`āŒ Token regeneration failed after invalid token 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 = getTurnstileToken(); + + // Don't auto-generate tokens during processing - return error if no token available + if (!token) { + console.warn('āš ļø No token available and auto-generation disabled during processing'); + 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: fpStr32 }; + 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. Token invalid during painting - regeneration allowed.'); + + // 403 errors during painting allow token regeneration per workflow requirements + console.log('ļæ½ Token invalid (403) during painting - regenerating token as allowed by workflow'); + setTurnstileToken(null); + createTokenPromise(); + + // Attempt to regenerate token immediately + const newToken = await ensureToken(true); + if (newToken) { + console.log('āœ… Token regenerated after 403 error, returning regenerate signal'); + return 'token_regenerated'; + } else { + console.error('āŒ Failed to regenerate token after 403 error'); + return 'token_regeneration_failed'; + } + } + + 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, + 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, + paintTransparentPixels: state.paintTransparentPixels, + resizeSettings: state.resizeSettings, + paintUnavailablePixels: state.paintUnavailablePixels, + coordinateMode: state.coordinateMode, + coordinateDirection: state.coordinateDirection, + coordinateSnake: state.coordinateSnake, + blockWidth: state.blockWidth, + blockHeight: state.blockHeight, // 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, + originalImage: state.originalImage, + }; + 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; + 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.paintTransparentPixels = settings.paintTransparentPixels ?? false; + state.resizeSettings = settings.resizeSettings ?? null; + state.originalImage = settings.originalImage ?? null; + state.paintUnavailablePixels = settings.paintUnavailablePixels ?? CONFIG.PAINT_UNAVAILABLE; + state.coordinateMode = settings.coordinateMode ?? CONFIG.COORDINATE_MODE; + state.coordinateDirection = settings.coordinateDirection ?? CONFIG.COORDINATE_DIRECTION; + state.coordinateSnake = settings.coordinateSnake ?? CONFIG.COORDINATE_SNAKE; + state.blockWidth = settings.blockWidth ?? CONFIG.COORDINATE_BLOCK_WIDTH; + state.blockHeight = settings.blockHeight ?? CONFIG.COORDINATE_BLOCK_HEIGHT; + // 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; + } + // Initialize coordinate generation UI + const coordinateModeSelect = document.getElementById('coordinateModeSelect'); + if (coordinateModeSelect) coordinateModeSelect.value = state.coordinateMode; + + const coordinateDirectionSelect = document.getElementById('coordinateDirectionSelect'); + if (coordinateDirectionSelect) coordinateDirectionSelect.value = state.coordinateDirection; + + const coordinateSnakeToggle = document.getElementById('coordinateSnakeToggle'); + if (coordinateSnakeToggle) coordinateSnakeToggle.checked = state.coordinateSnake; + + const settingsContainer = document.getElementById('wplace-settings-container'); + const directionControls = settingsContainer.querySelector('#directionControls'); + const snakeControls = settingsContainer.querySelector('#snakeControls'); + const blockControls = settingsContainer.querySelector('#blockControls'); + Utils.updateCoordinateUI({ + mode: state.coordinateMode, + directionControls, + snakeControls, + blockControls, + }); + + const paintUnavailablePixelsToggle = document.getElementById('paintUnavailablePixelsToggle'); + if (paintUnavailablePixelsToggle) { + paintUnavailablePixelsToggle.checked = state.paintUnavailablePixels; + } + + const settingsPaintWhiteToggle = settingsContainer.querySelector('#settingsPaintWhiteToggle'); + if (settingsPaintWhiteToggle) { + settingsPaintWhiteToggle.checked = state.paintWhitePixels; + } + + const settingsPaintTransparentToggle = settingsContainer.querySelector( + '#settingsPaintTransparentToggle' + ); + if (settingsPaintTransparentToggle) { + settingsPaintTransparentToggle.checked = state.paintTransparentPixels; + } + + const speedSlider = document.getElementById('speedSlider'); + const speedInput = document.getElementById('speedInput'); + const speedValue = document.getElementById('speedValue'); + if (speedSlider) speedSlider.value = state.paintingSpeed; + if (speedInput) speedInput.value = state.paintingSpeed; + if (speedValue) speedValue.textContent = `pixels`; + + 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'); + const cooldownInput = document.getElementById('cooldownInput'); + const cooldownValue = document.getElementById('cooldownValue'); + if (cooldownSlider) cooldownSlider.value = state.cooldownChargeThreshold; + if (cooldownInput) cooldownInput.value = state.cooldownChargeThreshold; + if (cooldownValue) cooldownValue.textContent = `${Utils.t('charges')}`; + + 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!'); + + // Generate fingerprint string for requests + if (!window.WPlaceTokenManager) { + console.error('āŒ WPlaceTokenManager not available - dependency loading issue'); + return; + } + const fpStr32 = tokenManager._randStr(32); + console.log('šŸ”‘ Generated fingerprint string for API requests'); + + // 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'); + } + + // Enable Load Extracted button at the same time as Upload button + const loadExtractedBtn = document.getElementById('loadExtractedBtn'); + if (loadExtractedBtn) { + loadExtractedBtn.disabled = false; + loadExtractedBtn.title = ''; + console.log('āœ… Load Extracted button enabled after initial setup'); + } + + // Show a notification that file operations are now available + Utils.showAlert(Utils.t('fileOperationsAvailable'), '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'); + + console.log('Attempting to load Turnstile script...'); + await Utils.loadTurnstile(); + console.log('Turnstile script loaded. Attempting to generate token...'); + + // Use TokenManager's handleCaptchaWithRetry method instead + const token = await tokenManager.handleCaptchaWithRetry(); + if (token) { + setTurnstileToken(token); + console.log('āœ… Startup token generated successfully'); + updateUI('tokenReady', 'success'); + Utils.showAlert(Utils.t('tokenGeneratorReady'), 'success'); + enableFileOperations(); // Enable file operations since initial setup is complete + } else { + console.warn( + 'āš ļø Startup token generation failed (no token received), 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.error('āŒ Critical error during Turnstile initialization:', error); // More specific 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(); + applyTheme(); + + var pawtect_chunk = null; + + //find module if pawtect_chunk is null + pawtect_chunk ??= await findTokenModule("pawtect_wasm_bg.wasm"); + + 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); + let wasm; + try { + wasm = await mod._(); + console.log('āœ… WASM initialized successfully'); + } catch (wasmError) { + console.error('āŒ WASM initialization failed:', wasmError); + return null; + } + try { + try { + const me = await fetch(`https://backend.wplace.live/me`, { credentials: 'include' }).then(r => 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('šŸ”‘ Full token:'); + console.log(token); + return token; + } catch (error) { + console.error('āŒ Failed to generate fp parameter:', error); + return null; + } + } + + async function findTokenModule(str) { + console.log('šŸ”Ž Searching for wasm Module...'); + const links = Array.from(document.querySelectorAll('link[rel="modulepreload"][href$=".js"]')); + + for (const link of links) { + try { + const url = new URL(link.getAttribute("href"), location.origin).href; + const code = await fetch(url).then(r => r.text()); + if (code.includes(str)) { + console.log('āœ… Found wasm Module...'); + return url.split('/').pop(); + } + } catch { } + } + 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" + }); + // POST request completed + 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); + } + } + async function swapAccountTrigger(token) { + // STRICT GUARD: Only allow account switching during active painting sessions OR controlled refresh + if (!state.running && !state.isFetchingAllAccounts) { + console.warn('šŸ”’ Account switching blocked - only allowed during active painting or controlled refresh'); + console.warn('šŸ”’ Current state.running:', state.running, 'state.isFetchingAllAccounts:', state.isFetchingAllAccounts); + return false; + } + + localStorage.removeItem("lp"); + if (!token) { + console.error('āŒ Cannot swap account: token is null or undefined'); + return false; + } + + console.log(`šŸ”„ Triggering account swap with token: ${token.substring(0, 20)}...`); + console.log('šŸ“¤ Sending setCookie message to extension...'); + + try { + window.postMessage({ + source: 'my-userscript', + type: 'setCookie', + value: token + }, '*'); + + console.log('āœ… setCookie message sent successfully'); + return true; + } catch (error) { + console.error('āŒ Failed to send setCookie message:', error); + return false; + } + } + 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); + } + + 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 = `
Fetching account details...
`; + } + + let originalToken = null; + let originalAccountData = null; + + try { + await getAccounts(); + const accountsTokens = JSON.parse(localStorage.getItem("accounts")) || []; + if (accountsTokens.length === 0) { + if (accountsListArea) accountsListArea.innerHTML = `
No accounts found.
`; + return; + } + + // Store original account info to switch back later + originalAccountData = await WPlaceService.getCharges(); + originalToken = accountsTokens.find(token => { + try { + const payload = JSON.parse(atob(token.split('.')[1])); + return payload.userId === originalAccountData.id; + } catch (e) { + return false; + } + }); + + state.allAccountsInfo = []; + renderAccountsList(); + + // Temporarily disable the strict guard for controlled refresh operation + const originalRunningState = state.running; + state.running = true; // Temporarily enable to allow controlled switching + + console.log('šŸ”“ Temporarily enabling controlled account switching for refresh'); + + for (let i = 0; i < accountsTokens.length; i++) { + const token = accountsTokens[i]; + + console.log(`šŸ“‹ Fetching details for account ${i + 1}/${accountsTokens.length}`); + + // Controlled account switch for data fetching + const swapResult = await swapAccountTrigger(token); + if (!swapResult) { + console.warn(`āš ļø Failed to trigger swap for account ${i + 1}`); + continue; + } + + // Wait for account switch to complete + let retries = 0; + let accountData = null; + while (retries < 8 && !accountData) { + // await Utils.sleep(1000); + try { + const fetchedInfo = await WPlaceService.fetchCheck(); + const chargesInfo = await WPlaceService.getCharges(); + if (fetchedInfo.ID) { + accountData = { + ...fetchedInfo, + Charges: Math.floor(chargesInfo.charges), + Max: Math.floor(chargesInfo.max), + Droplets: Math.floor(chargesInfo.droplets) + }; + } + } catch (e) { + retries++; + console.warn(`āš ļø Retry ${retries}/8 for account ${i + 1}`); + } + } + + if (accountData) { + const actualName = accountData.Username || accountData.name || `User${accountData.ID}`; + const isCurrentAccount = accountData.ID === originalAccountData.id; + + state.allAccountsInfo.push({ + ...accountData, + token, + displayName: actualName, + isCurrent: isCurrentAccount + }); + + console.log(`āœ… Fetched details for: ${actualName} (${accountData.Charges}/${accountData.Max} charges)`); + } else { + // Fallback for failed accounts + const fallbackName = `Account ${i + 1}`; + state.allAccountsInfo.push({ + token, + ID: `...${token.slice(-4)}`, + displayName: fallbackName, + isCurrent: false, + error: 'Failed to fetch details' + }); + console.warn(`āŒ Failed to fetch details for account ${i + 1}`); + } + + renderAccountsList(); + } + + // Restore original state and switch back to original account + state.running = originalRunningState; + console.log('šŸ”’ Restored original running state and switching back to original account'); + + } catch (error) { + console.error("Error fetching all account details:", error); + if (accountsListArea) accountsListArea.innerHTML = `
Error loading accounts.
`; + } finally { + // Switch back to original account + if (originalToken) { + console.log('šŸ”„ Switching back to original account...'); + const originalRunningState = state.running; + state.running = true; // Temporarily enable for switch back + + await swapAccountTrigger(originalToken); + await Utils.sleep(2000); + + // Verify we're back on the original account + try { + const verifyData = await WPlaceService.getCharges(); + if (verifyData.id === originalAccountData.id) { + console.log('āœ… Successfully switched back to original account'); + } + } catch (e) { + console.warn('āš ļø Could not verify switch back to original account'); + } + + state.running = originalRunningState; // Restore state + } + + // Update current account info in the list + if (originalAccountData) { + const currentAccountInList = state.allAccountsInfo.find(acc => acc.ID === originalAccountData.id); + if (currentAccountInList) { + currentAccountInList.isCurrent = true; + currentAccountInList.Charges = Math.floor(originalAccountData.charges); + currentAccountInList.Max = Math.floor(originalAccountData.max); + currentAccountInList.Droplets = Math.floor(originalAccountData.droplets); + } + } + + // Keep original account order - do NOT rearrange the array + // Instead, assign IDs based on current account being ID 1 + const currentAccountIndex = state.allAccountsInfo.findIndex(acc => acc.isCurrent); + + if (currentAccountIndex !== -1) { + console.log(`šŸŽÆ Current account found at array position ${currentAccountIndex}`); + + // Assign IDs: current account gets ID 1, others get sequential IDs + state.allAccountsInfo.forEach((acc, index) => { + if (acc.isCurrent) { + acc.orderId = 1; // Current account always gets ID 1 + } else { + // Other accounts get IDs based on their position relative to current + const relativePosition = index > currentAccountIndex ? + (index - currentAccountIndex + 1) : + (state.allAccountsInfo.length - currentAccountIndex + index + 1); + acc.orderId = relativePosition; + } + }); + + // Sort the display order by orderId for UI, but keep original array intact + const sortedForDisplay = [...state.allAccountsInfo].sort((a, b) => a.orderId - b.orderId); + + // Create the switching order array (ID 1 first, then sequential) + state.originalAccountOrder = sortedForDisplay; + + // Find the current account's position in the switching order (should be 0 since it's ID 1) + state.currentActiveIndex = 0; // Current account (ID 1) is always first in switching order + state.accountIndex = currentAccountIndex; // Keep track of original array position + + console.log(`šŸ“‹ Account IDs assigned:`); + state.allAccountsInfo.forEach((acc) => { + console.log(` Array pos ${state.allAccountsInfo.indexOf(acc)}: ID ${acc.orderId} - ${acc.displayName}${acc.isCurrent ? ' (CURRENT)' : ''}`); + }); + + console.log(`šŸ“‹ Switching order (by ID):`); + state.originalAccountOrder.forEach((acc, idx) => { + console.log(` Switch pos ${idx}: ID ${acc.orderId} - ${acc.displayName}${acc.isCurrent ? ' (CURRENT)' : ''}`); + }); + } else { + console.warn('āš ļø No current account found, using original order'); + state.allAccountsInfo.forEach((acc, index) => { + acc.orderId = index + 1; + }); + state.originalAccountOrder = [...state.allAccountsInfo]; + state.currentActiveIndex = 0; + state.accountIndex = 0; + } + + await updateStats(); + renderAccountsList(); + + state.isFetchingAllAccounts = false; + if (refreshBtn) { + refreshBtn.innerHTML = ''; + refreshBtn.disabled = false; + } + + console.log('āœ… Account list refresh completed with full details'); + } + } + + // Function to update current account charges in the account list + async function updateCurrentAccountInList() { + if (state.allAccountsInfo.length === 0) return; + + // Find current account in the list and update its charges + const currentAccountInList = state.allAccountsInfo.find(acc => acc.isCurrent); + if (currentAccountInList) { + const { charges, cooldown } = await WPlaceService.getCharges(); + state.displayCharges = Math.floor(charges); + state.preciseCurrentCharges = charges; + await updateStats(); + currentAccountInList.Charges = Math.floor(state.displayCharges || state.preciseCurrentCharges || 0); + currentAccountInList.Max = state.maxCharges; + // Re-render the account list to show updated charges + renderAccountsList(); + } + } + + // Function to update current account spotlight when switching during painting + async function updateCurrentAccountSpotlight() { + if (state.allAccountsInfo.length === 0) return; + await Utils.sleep(500); // Wait a bit for the switch to take effect + try { + const currentAccountData = await WPlaceService.getCharges(); + console.log("Current account after switch:", currentAccountData); + console.log(`šŸ” Switched to account with ID: ${currentAccountData.id}`); + + // Update all accounts to not be current + state.allAccountsInfo.forEach(acc => acc.isCurrent = false); + + // Find and mark the new current account in the original array + const newCurrentAccount = state.allAccountsInfo.find(acc => acc.ID === currentAccountData.id); + if (newCurrentAccount) { + const currentAccountInfo = await WPlaceService.fetchCheck(); + newCurrentAccount.isCurrent = true; + newCurrentAccount.Charges = Math.floor(currentAccountData.charges); + newCurrentAccount.Max = Math.floor(currentAccountData.max); + newCurrentAccount.Droplets = Math.floor(currentAccountData.droplets); + newCurrentAccount.displayName = currentAccountInfo.Username || currentAccountInfo.name || newCurrentAccount.displayName; + + console.log(`šŸŽÆ Updated current account spotlight: ${newCurrentAccount.displayName} (ID ${newCurrentAccount.orderId})`); + + // Update the switching order array to reflect the current account change + const switchingOrderIndex = state.originalAccountOrder.findIndex(acc => acc.orderId === newCurrentAccount.orderId); + if (switchingOrderIndex !== -1) { + state.originalAccountOrder[switchingOrderIndex] = newCurrentAccount; + // DON'T update currentActiveIndex here - it's managed by switchToNextAccount() + console.log(`šŸŽÆ Updated account data at switching order index ${switchingOrderIndex} (ID ${newCurrentAccount.orderId})`); + console.log(`šŸ“Š Current state: activeIndex=${state.currentActiveIndex}, expectedIndex=${switchingOrderIndex}`); + } + } + + // Re-render the account list to show new current account + renderAccountsList(); + + } catch (error) { + console.warn('āš ļø Failed to update account spotlight:', error); + } + } + + 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; + } + + // Render accounts in switching order (sorted by orderId) + state.originalAccountOrder.forEach((info, switchIndex) => { + const item = document.createElement('div'); + + // Determine if this is the current account or next in sequence + const isCurrentAccount = info.isCurrent; + const isNextInSequence = state.currentActiveIndex !== -1 && + ((state.currentActiveIndex + 1) % state.originalAccountOrder.length) === switchIndex; + + let itemClasses = 'wplace-account-item'; + if (isCurrentAccount) { + itemClasses += ' current'; + } else if (isNextInSequence) { + itemClasses += ' next-in-sequence'; + } + + item.className = itemClasses; + + // Create ordering number element with order ID + const orderNumber = document.createElement('div'); + orderNumber.className = 'wplace-account-number'; + orderNumber.textContent = info.orderId; // Always show the assigned order ID + + // Add visual indicator for current position in sequence + if (isCurrentAccount) { + orderNumber.style.background = '#2ecc71'; // Green color + orderNumber.style.color = 'white'; + orderNumber.style.boxShadow = '0 0 10px rgba(46, 204, 113, 0.8)'; + orderNumber.title = `Order ID ${info.orderId} - Currently active account`; + + // Add a subtle pulsing effect to the account number for extra visibility + orderNumber.style.animation = 'currentAccountNumberPulse 1.5s ease-in-out infinite alternate'; + } else if (isNextInSequence) { + orderNumber.style.background = '#f39c12'; // Orange color + orderNumber.style.color = 'white'; + orderNumber.style.boxShadow = '0 0 8px rgba(243, 156, 18, 0.6)'; + orderNumber.title = `Order ID ${info.orderId} - Next account in switching sequence`; + } else { + orderNumber.style.background = ''; // Reset to default + orderNumber.style.boxShadow = ''; // Remove any glow + orderNumber.title = `Order ID ${info.orderId}`; + orderNumber.style.animation = ''; // Remove any animation + } + + const displayName = info.displayName || `Account ${index + 1}`; + + const details = document.createElement('div'); + details.className = 'wplace-account-details'; + const nameDiv = document.createElement('div'); + nameDiv.className = 'wplace-account-name'; + nameDiv.textContent = displayName; + nameDiv.title = displayName; + details.appendChild(nameDiv); + + let stats; + if (info.error) { + stats = document.createElement('div'); + stats.className = 'wplace-account-stats'; + stats.style.color = 'red'; + stats.textContent = 'Error'; + } else { + stats = document.createElement('div'); + stats.className = 'wplace-account-stats'; + stats.innerHTML = ` + ${Math.floor(info.Charges || 0)}/${Math.floor(info.Max || 0)} + ${Math.floor(info.Droplets || 0)} + `; + } + + item.appendChild(orderNumber); + item.appendChild(details); + item.appendChild(stats); + accountsListArea.appendChild(item); + }); + } + + // Account switching helper functions + async function switchToNextAccount(accounts) { + const previousActiveIndex = state.currentActiveIndex; + + // Move to next account in strict ID order (next position in switching order) + const nextIndex = (state.currentActiveIndex + 1) % state.originalAccountOrder.length; + + // Get the account info from switching order + const nextAccountInfo = state.originalAccountOrder[nextIndex]; + const prevAccountInfo = state.originalAccountOrder[previousActiveIndex]; + + // Strict validation: ensure ID 1 always goes to ID 2 + if (prevAccountInfo?.orderId === 1 && nextAccountInfo?.orderId !== 2) { + console.error(`āŒ STRICT ORDER VIOLATION: ID 1 must switch to ID 2, but found ID ${nextAccountInfo?.orderId}`); + // Find ID 2 account + const id2Account = state.originalAccountOrder.find(acc => acc.orderId === 2); + if (id2Account) { + const id2Index = state.originalAccountOrder.indexOf(id2Account); + console.log(`šŸ”„ Correcting: Forcing switch from ID 1 to ID 2 (index ${id2Index})`); + state.currentActiveIndex = id2Index; + } else { + console.error(`āŒ FATAL: No ID 2 account found in switching order!`); + return false; + } + } else { + state.currentActiveIndex = nextIndex; + } + + // Re-get account info after potential correction + const finalNextAccountInfo = state.originalAccountOrder[state.currentActiveIndex]; + + console.log(`šŸ”„ Switching from ID ${prevAccountInfo?.orderId} (${prevAccountInfo?.displayName || 'Unknown'}) to ID ${finalNextAccountInfo?.orderId} (${finalNextAccountInfo?.displayName || 'Unknown'})`); + console.log(`šŸ“ Index progression: ${previousActiveIndex} → ${state.currentActiveIndex} (order: ${prevAccountInfo?.orderId} → ${finalNextAccountInfo?.orderId})`); + + // Find the token for this account by matching the account in the original allAccountsInfo array + const originalAccountIndex = state.allAccountsInfo.findIndex(acc => acc.orderId === finalNextAccountInfo?.orderId); + if (originalAccountIndex === -1) { + console.warn(`āš ļø Could not find account with ID ${finalNextAccountInfo?.orderId} in original array`); + return false; + } + + const nextToken = finalNextAccountInfo?.token; + console.log(`šŸ”‘ Next token for ID ${finalNextAccountInfo?.orderId}: ${nextToken ? nextToken.substring(0, 20) + '...' : 'INVALID'}`); + console.log(`šŸ” Expected account: ${finalNextAccountInfo?.displayName} (ID ${finalNextAccountInfo?.orderId})`); + + if (!nextToken) { + console.warn(`āš ļø Invalid token for ID ${finalNextAccountInfo?.orderId}, skipping...`); + return false; + } + + // Update accountIndex to match the position in the accounts token array + const accountsIndex = accounts.findIndex(token => token === nextToken); + if (accountsIndex !== -1) { + state.accountIndex = accountsIndex; + console.log(`šŸ”— Found token at accounts array index: ${accountsIndex}`); + console.log(`šŸ” Token from accounts[${accountsIndex}]: ${accounts[accountsIndex].substring(0, 20)}...`); + } else { + console.warn(`āš ļø Token not found in accounts array for ID ${finalNextAccountInfo?.orderId}`); + console.warn(`šŸ” Available accounts tokens: ${accounts.map((t, i) => `[${i}] ${t.substring(0, 20)}...`).join(', ')}`); + return false; + } + + return await switchToSpecificAccount(nextToken, state.currentActiveIndex); + } + + async function switchToSpecificAccount(token, originalOrderIndex) { + // Get account name and ID for better logging + const accountInfo = state.originalAccountOrder && state.originalAccountOrder[originalOrderIndex] + ? state.originalAccountOrder[originalOrderIndex] + : null; + const accountName = accountInfo?.displayName || `Account ${originalOrderIndex + 1}`; + const accountId = accountInfo?.orderId || (originalOrderIndex + 1); + + console.log(`šŸ”„ Attempting to switch to ID ${accountId} (${accountName})`); + console.log(`šŸ”‘ Using token: ${token.substring(0, 20)}...`); + + await swapAccountTrigger(token); + + // Give more time for the initial swap to process (like Acc-Switch.js) + // console.log('ā³ Waiting 5 seconds for initial swap processing...'); + // await new Promise(resolve => setTimeout(resolve, 5000)); + + let maxRetries = 20; // Match Acc-Switch.js retry count + 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 (like Acc-Switch.js) + await new Promise(resolve => setTimeout(resolve, 1000)); + + try { + // Use the same verification method as Acc-Switch.js + await fetchAccount(); + console.log('āœ… Account swap confirmed.'); + swapSuccess = true; + } catch (error) { + console.warn('āŒ Account swap not yet successful. Retrying...', error); + retryCount++; + + // Re-trigger swap every 5 attempts + if (retryCount % 5 === 0) { + console.log('šŸ”„ Re-triggering account swap...'); + await swapAccountTrigger(token); + await new Promise(resolve => setTimeout(resolve, 2000)); + } + } + } + + if (swapSuccess) { + const { charges, cooldown } = await WPlaceService.getCharges(); + state.displayCharges = Math.floor(charges); + state.preciseCurrentCharges = charges; + state.cooldown = cooldown; + // Don't update state.accountIndex here as it should be managed by the calling function + Utils.performSmartSave(); + await updateStats(); + + // Update account list spotlight to show new current account + await updateCurrentAccountSpotlight(); + + console.log(`āœ… Successfully switched to ID ${accountId} (${accountName}) with ${Math.floor(charges)} charges`); + return true; + } else { + console.error(`āŒ Failed to swap to ID ${accountId} (${accountName}) after multiple retries.`); + return false; + } + } + + // Wait for dependencies before initializing UI + async function waitForDependenciesAndInitialize() { + // Wait for all global managers to be available + let attempts = 0; + const maxAttempts = 50; // 5 seconds max wait + + while (attempts < maxAttempts) { + const managers = { + utilsManager: window.globalUtilsManager, + imageProcessor: window.globalImageProcessor, + overlayManager: window.globalOverlayManager, + tokenManager: window.globalTokenManager + }; + + const missingManagers = Object.entries(managers) + .filter(([name, manager]) => !manager) + .map(([name]) => name); + + if (missingManagers.length === 0) { + console.log('āœ… All global managers are available, initializing UI...'); + break; + } + + console.log(`ā³ Waiting for managers: ${missingManagers.join(', ')} (attempt ${attempts + 1}/${maxAttempts})`); + await new Promise(resolve => setTimeout(resolve, 100)); + attempts++; + } + + if (attempts >= maxAttempts) { + console.warn('āš ļø Some global managers not available after waiting, proceeding anyway...'); + console.log('Available managers:', { + utilsManager: !!window.globalUtilsManager, + imageProcessor: !!window.globalImageProcessor, + overlayManager: !!window.globalOverlayManager, + tokenManager: !!window.globalTokenManager + }); + } + + return createUI(); + } + + waitForDependenciesAndInitialize().then(() => { + // Generate token automatically after UI is ready + setTimeout(initializeTokenGenerator, 1000); + + // Auto-refresh account list on startup + setTimeout(() => { + console.log('šŸ”„ Auto-refreshing account list on startup...'); + fetchAllAccountDetails(); + }, 2000); + + // 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(Utils.t('advancedColorSettingsReset'), 'success'); + }); + }; + // Delay to ensure resize UI built + setTimeout(advancedInit, 500); + + // Add cleanup on page unload + window.addEventListener('beforeunload', () => { + Utils.cleanupTurnstile(); + }); + }); +})(); + + diff --git a/Extensions/scripts/Auto-Repair.js b/Extensions/scripts/Auto-Repair.js new file mode 100644 index 0000000..1b4e0f2 --- /dev/null +++ b/Extensions/scripts/Auto-Repair.js @@ -0,0 +1,3331 @@ +// eslint-disable-next-line prettier/prettier +; (async () => { + // CONFIGURATION CONSTANTS + const CONFIG = { + COOLDOWN_DEFAULT: 31000, + TRANSPARENCY_THRESHOLD: 100, + WHITE_THRESHOLD: 250, + LOG_INTERVAL: 10, + PAINTING_SPEED: { + MIN: 1, + MAX: 1000, + DEFAULT: 5, + }, + TOKEN_SOURCE: 'generator', // "generator", "manual", or "hybrid" + AUTONOMOUS_MODE: true, // Enable autonomous operation + AUTO_TOKEN_REFRESH: true, // Automatically refresh tokens + 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 } }, + 2: { id: 3, name: 'Gray', rgb: { r: 120, g: 120, b: 120 } }, + 3: { id: 4, name: 'Light Gray', rgb: { r: 210, g: 210, b: 210 } }, + 4: { id: 5, name: 'White', rgb: { r: 255, g: 255, b: 255 } }, + 5: { id: 6, name: 'Deep Red', rgb: { r: 96, g: 0, b: 24 } }, + 6: { id: 7, name: 'Red', rgb: { r: 237, g: 28, b: 36 } }, + 7: { id: 8, name: 'Orange', rgb: { r: 255, g: 127, b: 39 } }, + 8: { id: 9, name: 'Gold', rgb: { r: 246, g: 170, b: 9 } }, + 9: { id: 10, name: 'Yellow', rgb: { r: 249, g: 221, b: 59 } }, + 10: { id: 11, name: 'Light Yellow', rgb: { r: 255, g: 250, b: 188 } }, + 11: { id: 12, name: 'Dark Green', rgb: { r: 14, g: 185, b: 104 } }, + 12: { id: 13, name: 'Green', rgb: { r: 19, g: 230, b: 123 } }, + 13: { id: 14, name: 'Light Green', rgb: { r: 135, g: 255, b: 94 } }, + 14: { id: 15, name: 'Dark Teal', rgb: { r: 12, g: 129, b: 110 } }, + 15: { id: 16, name: 'Teal', rgb: { r: 16, g: 174, b: 166 } }, + 16: { id: 17, name: 'Light Teal', rgb: { r: 19, g: 225, b: 190 } }, + 17: { id: 20, name: 'Cyan', rgb: { r: 96, g: 247, b: 242 } }, + 18: { id: 44, name: 'Light Cyan', rgb: { r: 187, g: 250, b: 242 } }, + 19: { id: 18, name: 'Dark Blue', rgb: { r: 40, g: 80, b: 158 } }, + 20: { id: 19, name: 'Blue', rgb: { r: 64, g: 147, b: 228 } }, + 21: { id: 21, name: 'Indigo', rgb: { r: 107, g: 80, b: 246 } }, + 22: { id: 22, name: 'Light Indigo', rgb: { r: 153, g: 177, b: 251 } }, + 23: { id: 23, name: 'Dark Purple', rgb: { r: 120, g: 12, b: 153 } }, + 24: { id: 24, name: 'Purple', rgb: { r: 170, g: 56, b: 185 } }, + 25: { id: 25, name: 'Light Purple', rgb: { r: 224, g: 159, b: 249 } }, + 26: { id: 26, name: 'Dark Pink', rgb: { r: 203, g: 0, b: 122 } }, + 27: { id: 27, name: 'Pink', rgb: { r: 236, g: 31, b: 128 } }, + 28: { id: 28, name: 'Light Pink', rgb: { r: 243, g: 141, b: 169 } }, + 29: { id: 29, name: 'Dark Brown', rgb: { r: 104, g: 70, b: 52 } }, + 30: { id: 30, name: 'Brown', rgb: { r: 149, g: 104, b: 42 } }, + 31: { id: 31, name: 'Beige', rgb: { r: 248, g: 178, b: 119 } }, + 32: { id: 52, name: 'Light Beige', rgb: { r: 255, g: 197, b: 165 } }, + 33: { id: 32, name: 'Medium Gray', rgb: { r: 170, g: 170, b: 170 } }, + 34: { id: 33, name: 'Dark Red', rgb: { r: 165, g: 14, b: 30 } }, + 35: { id: 34, name: 'Light Red', rgb: { r: 250, g: 128, b: 114 } }, + 36: { id: 35, name: 'Dark Orange', rgb: { r: 228, g: 92, b: 26 } }, + 37: { id: 37, name: 'Dark Goldenrod', rgb: { r: 156, g: 132, b: 49 } }, + 38: { id: 38, name: 'Goldenrod', rgb: { r: 197, g: 173, b: 49 } }, + 39: { id: 39, name: 'Light Goldenrod', rgb: { r: 232, g: 212, b: 95 } }, + 40: { id: 40, name: 'Dark Olive', rgb: { r: 74, g: 107, b: 58 } }, + 41: { id: 41, name: 'Olive', rgb: { r: 90, g: 148, b: 74 } }, + 42: { id: 42, name: 'Light Olive', rgb: { r: 132, g: 197, b: 115 } }, + 43: { id: 43, name: 'Dark Cyan', rgb: { r: 15, g: 121, b: 159 } }, + 44: { id: 45, name: 'Light Blue', rgb: { r: 125, g: 199, b: 255 } }, + 45: { id: 46, name: 'Dark Indigo', rgb: { r: 77, g: 49, b: 184 } }, + 46: { id: 47, name: 'Dark Slate Blue', rgb: { r: 74, g: 66, b: 132 } }, + 47: { id: 48, name: 'Slate Blue', rgb: { r: 122, g: 113, b: 196 } }, + 48: { id: 49, name: 'Light Slate Blue', rgb: { r: 181, g: 174, b: 241 } }, + 49: { id: 53, name: 'Dark Peach', rgb: { r: 155, g: 82, b: 73 } }, + 50: { id: 54, name: 'Peach', rgb: { r: 209, g: 128, b: 120 } }, + 51: { id: 55, name: 'Light Peach', rgb: { r: 250, g: 182, b: 164 } }, + 52: { id: 50, name: 'Light Brown', rgb: { r: 219, g: 164, b: 99 } }, + 53: { id: 56, name: 'Dark Tan', rgb: { r: 123, g: 99, b: 82 } }, + 54: { id: 57, name: 'Tan', rgb: { r: 156, g: 132, b: 107 } }, + 55: { id: 36, name: 'Light Tan', rgb: { r: 214, g: 181, b: 148 } }, + 56: { id: 51, name: 'Dark Beige', rgb: { r: 209, g: 128, b: 81 } }, + 57: { id: 61, name: 'Dark Stone', rgb: { r: 109, g: 100, b: 63 } }, + 58: { id: 62, name: 'Stone', rgb: { r: 148, g: 140, b: 107 } }, + 59: { id: 63, name: 'Light Stone', rgb: { r: 205, g: 197, b: 158 } }, + 60: { id: 58, name: 'Dark Slate', rgb: { r: 51, g: 57, b: 65 } }, + 61: { id: 59, name: 'Slate', rgb: { r: 109, g: 117, b: 141 } }, + 62: { id: 60, name: 'Light Slate', rgb: { r: 179, g: 185, b: 209 } }, + 63: { id: 0, name: 'Transparent', rgb: null }, + }, + }; + + // GLOBAL STATE + const state = { + running: false, + imageLoaded: false, + totalPixels: 0, + paintedPixels: 0, + availableColors: [], + displayCharges: 0, + maxCharges: 1, + cooldown: CONFIG.COOLDOWN_DEFAULT, + imageData: null, + stopFlag: false, + startPosition: null, + region: null, + paintWhitePixels: true, + paintTransparentPixels: true, // Changed to true to fix transparent pixel detection + autoRepairEnabled: false, + autoRepairInterval: 30, + autoRepairTimer: null, + debugLogs: [], + customTransparencyThreshold: CONFIG.TRANSPARENCY_THRESHOLD, + customWhiteThreshold: CONFIG.WHITE_THRESHOLD, + tokenSource: CONFIG.TOKEN_SOURCE, + autonomousMode: CONFIG.AUTONOMOUS_MODE, + autoTokenRefresh: CONFIG.AUTO_TOKEN_REFRESH, + tokenPreloadBuffer: CONFIG.TOKEN_PRELOAD_BUFFER, + retryCount: 0, + tokenRetryTimer: null, + tokenPreloadTimer: null, + 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 + const randStr = (len, chars = 'abcdefghijklmnopqrstuvwxyz0123456789') => + [...Array(len)].map(() => chars[(crypto?.getRandomValues?.(new Uint32Array(1))[0] % chars.length) || Math.floor(Math.random() * chars.length)]).join(''); + + const fpStr32 = randStr(32); + + // Enhanced Turnstile token handling - Actualizado con la lógica de new.txt lol + let turnstileToken = null; + let tokenExpiryTime = 0; + let tokenGenerationInProgress = false; + let _resolveToken = null; + let tokenPromise = new Promise((resolve) => { + _resolveToken = resolve; + }); + let retryCount = 0; + const MAX_RETRIES = 10; + const MAX_BATCH_RETRIES = 10; + const TOKEN_LIFETIME = 240000; // 4 minutes (tokens typically last 5 min, use 4 for safety) + + function setTurnstileToken(token) { + if (_resolveToken) { + _resolveToken(token); + _resolveToken = null; + } + turnstileToken = token; + tokenExpiryTime = Date.now() + TOKEN_LIFETIME; + console.log('āœ… Turnstile token set successfully'); + Utils.addDebugLog('Token cached successfully', 'success'); + } + + function isTokenValid() { + return turnstileToken && Date.now() < tokenExpiryTime; + } + + function invalidateToken() { + turnstileToken = null; + tokenExpiryTime = 0; + console.log('šŸ—‘ļø Token invalidated, will force fresh generation'); + Utils.addDebugLog('Token invalidated, will force fresh generation', 'warning'); + } + + async function ensureToken(forceRefresh = false) { + // Return cached token if still valid and not forcing refresh + if (isTokenValid() && !forceRefresh) { + return turnstileToken; + } + + // Invalidate token if forcing refresh + if (forceRefresh) invalidateToken(); + + // Avoid multiple simultaneous token generations + if (tokenGenerationInProgress) { + console.log('šŸ”„ Token generation already in progress, waiting...'); + await Utils.sleep(2000); + return isTokenValid() ? turnstileToken : null; + } + + tokenGenerationInProgress = true; + + try { + console.log('šŸ”„ Token expired or missing, generating new one...'); + const token = await handleCaptchaWithRetry(); + if (token && token.length > 20) { + setTurnstileToken(token); + console.log('āœ… Token captured and cached successfully'); + return token; + } + + console.log('āš ļø Invisible Turnstile failed, forcing browser automation...'); + const fallbackToken = await handleCaptchaFallback(); + if (fallbackToken && fallbackToken.length > 20) { + setTurnstileToken(fallbackToken); + console.log('āœ… Fallback token captured successfully'); + return fallbackToken; + } + + console.log('āŒ All token generation methods failed'); + return null; + } finally { + tokenGenerationInProgress = false; + } + } + + async function handleCaptchaWithRetry() { + const startTime = performance.now(); + + try { + const { sitekey, token: preGeneratedToken } = await Utils.obtainSitekeyAndToken(); + + if (!sitekey) { + throw new Error('No valid sitekey found'); + } + + console.log('šŸ”‘ Using sitekey:', sitekey); + + if (typeof window !== 'undefined' && window.navigator) { + console.log( + '🧭 UA:', + window.navigator.userAgent.substring(0, 50) + '...', + 'Platform:', + window.navigator.platform + ); + } + + let token = null; + + if ( + preGeneratedToken && + typeof preGeneratedToken === 'string' && + preGeneratedToken.length > 20 + ) { + console.log('ā™»ļø Reusing pre-generated Turnstile token'); + token = preGeneratedToken; + } else { + if (isTokenValid()) { + console.log('ā™»ļø Using existing cached token (from previous session)'); + token = turnstileToken; + } else { + console.log('šŸ” Generating new token with executeTurnstile...'); + token = await Utils.executeTurnstile(sitekey, 'paint'); + if (token) setTurnstileToken(token); + } + } + + if (token && typeof token === 'string' && token.length > 20) { + const elapsed = Math.round(performance.now() - startTime); + console.log(`āœ… Turnstile token generated successfully in ${elapsed}ms`); + return token; + } else { + throw new Error(`Invalid or empty token received - Length: ${token?.length || 0}`); + } + } catch (error) { + const elapsed = Math.round(performance.now() - startTime); + console.error(`āŒ Turnstile token generation failed after ${elapsed}ms:`, error); + throw error; + } + } + + async function handleCaptchaFallback() { + return new Promise(async (resolve, reject) => { + try { + // Ensure we have a fresh promise to await for a new token capture + if (!_resolveToken) { + tokenPromise = new Promise((res) => { + _resolveToken = res; + }); + } + const timeoutPromise = Utils.sleep(20000).then(() => + reject(new Error('Auto-CAPTCHA timed out.')) + ); + + const solvePromise = (async () => { + const mainPaintBtn = await Utils.waitForSelector( + 'button.btn.btn-primary.btn-lg, button.btn-primary.sm\\:btn-xl', + 200, + 10000 + ); + if (!mainPaintBtn) throw new Error('Could not find the main paint button.'); + mainPaintBtn.click(); + await Utils.sleep(500); + + const transBtn = await Utils.waitForSelector('button#color-0', 200, 5000); + if (!transBtn) throw new Error('Could not find the transparent color button.'); + transBtn.click(); + await Utils.sleep(500); + + const canvas = await Utils.waitForSelector('canvas', 200, 5000); + if (!canvas) throw new Error('Could not find the canvas element.'); + + canvas.setAttribute('tabindex', '0'); + canvas.focus(); + const rect = canvas.getBoundingClientRect(); + const centerX = Math.round(rect.left + rect.width / 2); + const centerY = Math.round(rect.top + rect.height / 2); + + canvas.dispatchEvent( + new MouseEvent('mousemove', { + clientX: centerX, + clientY: centerY, + bubbles: true, + }) + ); + canvas.dispatchEvent( + new KeyboardEvent('keydown', { + key: ' ', + code: 'Space', + bubbles: true, + }) + ); + await Utils.sleep(50); + canvas.dispatchEvent( + new KeyboardEvent('keyup', { + key: ' ', + code: 'Space', + bubbles: true, + }) + ); + await Utils.sleep(500); + + // 800ms delay before sending confirmation + await Utils.sleep(800); + + // Keep confirming until token is captured + const confirmLoop = async () => { + while (!turnstileToken) { + let confirmBtn = await Utils.waitForSelector( + 'button.btn.btn-primary.btn-lg, button.btn.btn-primary.sm\\:btn-xl' + ); + if (!confirmBtn) { + const allPrimary = Array.from(document.querySelectorAll('button.btn-primary')); + confirmBtn = allPrimary.length ? allPrimary[allPrimary.length - 1] : null; + } + if (confirmBtn) { + confirmBtn.click(); + } + await Utils.sleep(500); // 500ms delay between confirmation attempts + } + }; + + // Start confirmation loop and wait for token + confirmLoop(); + const token = await tokenPromise; + await Utils.sleep(300); // small delay after token is captured + resolve(token); + })(); + + await Promise.race([solvePromise, timeoutPromise]); + } catch (error) { + console.error('Auto-CAPTCHA process failed:', error); + reject(error); + } + }); + } + + // NUEVA LƓGICA DE INYECCIƓN - ACTUALIZADA DESDE new.txt + function inject(callback) { + try { + const script = document.createElement('script'); + script.textContent = `(${callback})();`; + document.documentElement?.appendChild(script); + script.remove(); + } catch (error) { + console.error('āŒ Injection error:', error); + } + } + + inject(() => { + const fetchedBlobQueue = new Map(); + + window.addEventListener('message', (event) => { + const { source, blobID, blobData } = event.data; + if (source === 'auto-image-overlay' && blobID && blobData) { + const callback = fetchedBlobQueue.get(blobID); + if (typeof callback === 'function') { + callback(blobData); + } + fetchedBlobQueue.delete(blobID); + } + }); + + const originalFetch = window.fetch; + window.fetch = async function (...args) { + const response = await originalFetch.apply(this, args); + const url = args[0] instanceof Request ? args[0].url : args[0]; + + if (typeof url === 'string') { + if (url.includes('https://backend.wplace.live/s0/pixel/')) { + try { + const payload = JSON.parse(args[1].body); + if (payload.t) { + // šŸ“Š Debug log + console.log( + `šŸ”āœ… Turnstile Token Captured - Type: ${typeof payload.t}, Value: ${payload.t + ? typeof payload.t === 'string' + ? payload.t.length > 50 + ? payload.t.substring(0, 50) + '...' + : payload.t + : JSON.stringify(payload.t) + : 'null/undefined' + }, Length: ${payload.t?.length || 0}` + ); + window.postMessage({ source: 'turnstile-capture', token: payload.t }, '*'); + } + } catch (_) { + /* ignore */ + } + } + + const contentType = response.headers.get('content-type') || ''; + if (contentType.includes('image/png') && url.includes('.png')) { + const cloned = response.clone(); + return new Promise(async (resolve) => { + const blobUUID = crypto.randomUUID(); + const originalBlob = await cloned.blob(); + + fetchedBlobQueue.set(blobUUID, (processedBlob) => { + resolve( + new Response(processedBlob, { + headers: cloned.headers, + status: cloned.status, + statusText: cloned.statusText, + }) + ); + }); + + window.postMessage( + { + source: 'auto-image-tile', + endpoint: url, + blobID: blobUUID, + blobData: originalBlob, + }, + '*' + ); + }); + } + } + + return response; + }; + }); + + window.addEventListener('message', (event) => { + const { source, endpoint, blobID, blobData, token } = event.data; + + if (source === 'auto-image-tile' && endpoint && blobID && blobData) { + overlayManager.processAndRespondToTileRequest(event.data); + } + + if (source === 'turnstile-capture' && token) { + setTurnstileToken(token); + Utils.addDebugLog('Token captured from injection system', 'success'); + updateTokenStatus(); + } + }); + + // NUEVO SISTEMA WASM - ACTUALIZADO DESDE new.txt + var pawtect_chunk = null; + + // Find module if pawtect_chunk is null + pawtect_chunk ??= await findTokenModule("pawtect_wasm_bg.wasm"); + + 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); + let wasm; + try { + wasm = await mod._(); + console.log('āœ… WASM initialized successfully'); + } catch (wasmError) { + console.error('āŒ WASM initialization failed:', wasmError); + return null; + } + try { + try { + const me = await fetch(`https://backend.wplace.live/me`, { credentials: 'include' }).then(r => 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); + } + + 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); + } + + console.log('šŸŽ‰ SUCCESS!'); + console.log('šŸ”‘ Full token:', token); + return token; + } catch (error) { + console.error('āŒ Failed to generate fp parameter:', error); + return null; + } + } + + async function findTokenModule(str) { + console.log('šŸ”Ž Searching for wasm Module...'); + const links = Array.from(document.querySelectorAll('link[rel="modulepreload"][href$=".js"]')); + + for (const link of links) { + try { + const url = new URL(link.getAttribute("href"), location.origin).href; + const code = await fetch(url).then(r => r.text()); + if (code.includes(str)) { + console.log('āœ… Found wasm Module...'); + return url.split('/').pop(); + } + } catch { } + } + console.error(`āŒ Could not find Pawtect chunk with string: ${str}`); + } + + // Audio notification system + function playNotificationSound() { + try { + const audio = new Audio('https://cdn.pixabay.com/download/audio/2025/03/21/audio_9bec51b17f.mp3?filename=glass-break-316720.mp3'); + audio.volume = 0.5; + audio.play().catch(() => { + console.warn('Could not play notification sound'); + }); + } catch (error) { + console.warn('Audio notification failed:', error); + } + } + + // Notification system for attacks and repairs + function showAttackNotification(type, count = 0) { + const existing = document.getElementById('attack-notification'); + if (existing) existing.remove(); + + const notification = document.createElement('div'); + notification.id = 'attack-notification'; + notification.style.cssText = ` + position: fixed; top: 50%; left: 50%; transform: translate(-50%, -50%); + z-index: 15000; background: rgba(0,0,0,0.9); color: white; + border-radius: 15px; padding: 20px; text-align: center; + box-shadow: 0 10px 30px rgba(0,0,0,0.8); + border: 2px solid ${type === 'attack' ? '#ff4444' : '#44ff44'}; + min-width: 350px; max-width: 450px; + `; + + if (type === 'attack') { + notification.innerHTML = ` +
+ Attack +
+

+ PIXEL ART ATTACKED! +

+

+ Detected ${count} damaged pixels! +

+ `; + } else if (type === 'repaired') { + notification.innerHTML = ` +
+ Repaired +
+

+ COMPLETELY REPAIRED! +

+

+ All pixels have been restored! +

+ `; + } + + document.body.appendChild(notification); + + setTimeout(() => { + if (notification.parentNode) { + notification.style.transition = 'opacity 0.5s ease'; + notification.style.opacity = '0'; + setTimeout(() => { + if (notification.parentNode) notification.remove(); + }, 500); + } + }, 3000); + } + + // Peaceful state display in main window + function showPeacefulState() { + const statusDiv = document.getElementById('status'); + if (statusDiv && !state.running) { + statusDiv.innerHTML = ` +
+ Peaceful + No attacks detected - All secure +
+ `; + } + } + + // Typewriter effect for title + function createTypewriterTitle() { + const titleText = "WPlace Autonomous Repair Tool"; + let currentIndex = 0; + const titleElement = document.getElementById('main-title'); + + if (!titleElement) return; + + function typeNext() { + if (currentIndex < titleText.length) { + titleElement.textContent = titleText.substring(0, currentIndex + 1); + currentIndex++; + setTimeout(typeNext, 100); + } else { + setTimeout(() => { + currentIndex = 0; + titleElement.textContent = ''; + setTimeout(typeNext, 1000); + }, 3000); + } + } + + typeNext(); + } + + // Fallback translations + const TEXTS = { + title: 'WPlace Autonomous Repair Tool', + loadFromFile: 'Load Progress File', + repairPixels: 'Repair Pixels', + enableAutoRepair: 'Enable Auto Repair', + repairInterval: 'Check Interval (seconds)', + debug: 'Debug Console', + clearDebug: 'Clear Debug', + scanningForDamage: 'Scanning for damaged pixels...', + damageDetected: 'Damage detected: {count} pixels', + noDamageDetected: 'No damage found', + repairingPixels: 'Repairing {count} damaged pixels...', + repairComplete: 'Repair completed: {repaired} pixels fixed', + autoRepairStarted: 'Auto repair started (every {interval}s)', + autoRepairStopped: 'Auto repair stopped', + fileLoaded: 'Progress file loaded successfully', + invalidFile: 'Invalid file format', + noImageData: 'No image data found in file', + turnstileInstructions: 'Complete the verification', + hideTurnstileBtn: 'Hide', + tokenCapturedSuccess: 'Token captured successfully', + autonomousModeActive: 'Autonomous mode active', + tokenSystemReady: 'Advanced token system ready', + fileOperationsAvailable: 'File operations available', + initializingToken: 'Initializing token system...', + tokenReady: 'Token system ready', + }; + + // UTILIDADES ACTUALIZADAS CON NUEVA IMPLEMENTACIƓN TURNSTILE + const Utils = { + sleep: (ms) => new Promise((r) => setTimeout(r, ms)), + + t: (key, params = {}) => { + let text = TEXTS[key] || key; + Object.keys(params).forEach((param) => { + text = text.replace(`{${param}}`, params[param]); + }); + return text; + }, + + showAlert: (message, type = 'info') => { + const alertDiv = document.createElement('div'); + alertDiv.style.cssText = ` + position: fixed; top: 20px; right: 20px; z-index: 10000; + padding: 12px 16px; border-radius: 8px; color: white; font-weight: 500; + background: ${type === 'success' ? '#28a745' : type === 'error' ? '#dc3545' : type === 'warning' ? '#ffc107' : '#17a2b8'}; + box-shadow: 0 4px 12px rgba(0,0,0,0.3); + max-width: 300px; word-wrap: break-word; + `; + alertDiv.textContent = message; + document.body.appendChild(alertDiv); + + setTimeout(() => { + alertDiv.style.transition = 'opacity 0.3s ease'; + alertDiv.style.opacity = '0'; + setTimeout(() => { + if (alertDiv.parentNode) alertDiv.remove(); + }, 300); + }, 4000); + }, + + addDebugLog: (message, type = 'info') => { + const timestamp = new Date().toLocaleTimeString(); + state.debugLogs.push({ timestamp, message, type }); + + if (state.debugLogs.length > 150) { + state.debugLogs = state.debugLogs.slice(-150); + } + + updateDebugConsole(); + console.log(`[${timestamp}] ${message}`); + }, + + waitForSelector: async (selector, interval = 200, timeout = 5000) => { + const start = Date.now(); + while (Date.now() - start < timeout) { + const el = document.querySelector(selector); + if (el) return el; + await Utils.sleep(interval); + } + return null; + }, + + createFileUploader: () => + new Promise((resolve, reject) => { + const input = document.createElement('input'); + input.type = 'file'; + input.accept = '.json'; + input.onchange = (e) => { + const file = e.target.files[0]; + if (file) { + const reader = new FileReader(); + reader.onload = () => { + try { + const data = JSON.parse(reader.result); + resolve(data); + } catch (error) { + reject(new Error('Invalid JSON file')); + } + }; + reader.onerror = () => reject(new Error('File reading error')); + reader.readAsText(file); + } else { + reject(new Error('No file selected')); + } + }; + input.click(); + }), + + isWhitePixel: (r, g, b) => { + const wt = state.customWhiteThreshold || CONFIG.WHITE_THRESHOLD; + return r >= wt && g >= wt && b >= wt; + }, + + resolveColor(targetRgb, availableColors) { + if (!availableColors || availableColors.length === 0) { + return { id: null, rgb: targetRgb }; + } + + let bestId = availableColors[0].id; + let bestRgb = [...availableColors[0].rgb]; + let bestScore = Infinity; + + for (let i = 0; i < availableColors.length; i++) { + const c = availableColors[i]; + const [r, g, b] = c.rgb; + const rmean = (r + targetRgb[0]) / 2; + const rdiff = r - targetRgb[0]; + const gdiff = g - targetRgb[1]; + const bdiff = b - targetRgb[2]; + const dist = Math.sqrt( + (((512 + rmean) * rdiff * rdiff) >> 8) + + 4 * gdiff * gdiff + + (((767 - rmean) * bdiff * bdiff) >> 8) + ); + if (dist < bestScore) { + bestScore = dist; + bestId = c.id; + bestRgb = [...c.rgb]; + if (dist === 0) break; + } + } + + return { id: bestId, rgb: bestRgb }; + }, + + calculateTileRange( + startRegionX, + startRegionY, + startPixelX, + startPixelY, + width, + height, + tileSize = 1000 + ) { + const endPixelX = startPixelX + width; + const endPixelY = startPixelY + height; + + return { + startTileX: startRegionX + Math.floor(startPixelX / tileSize), + startTileY: startRegionY + Math.floor(startPixelY / tileSize), + endTileX: startRegionX + Math.floor((endPixelX - 1) / tileSize), + endTileY: startRegionY + Math.floor((endPixelY - 1) / tileSize), + }; + }, + + dynamicSleep: async function (tickAndGetRemainingMs) { + let remaining = Math.max(0, await tickAndGetRemainingMs()); + while (remaining > 0) { + const interval = remaining > 5000 ? 2000 : remaining > 1000 ? 500 : 100; + await this.sleep(Math.min(interval, remaining)); + remaining = Math.max(0, await tickAndGetRemainingMs()); + } + }, + + // NUEVA IMPLEMENTACIƓN TURNSTILE COMPLETA - ACTUALIZADA DESDE new.txt + turnstileLoaded: false, + _turnstileContainer: null, + _turnstileOverlay: null, + _turnstileWidgetId: null, + _lastSitekey: null, + _cachedSitekey: null, + + async loadTurnstile() { + if (window.turnstile) { + this.turnstileLoaded = true; + return Promise.resolve(); + } + + return new Promise((resolve, reject) => { + if (document.querySelector('script[src^="https://challenges.cloudflare.com/turnstile/v0/api.js"]')) { + const checkReady = () => { + if (window.turnstile) { + this.turnstileLoaded = true; + resolve(); + } else { + setTimeout(checkReady, 100); + } + }; + return checkReady(); + } + + const script = document.createElement('script'); + script.src = 'https://challenges.cloudflare.com/turnstile/v0/api.js?render=explicit'; + script.async = true; + script.defer = true; + script.onload = () => { + this.turnstileLoaded = true; + Utils.addDebugLog('Turnstile script loaded successfully', 'success'); + resolve(); + }; + script.onerror = () => { + Utils.addDebugLog('Failed to load Turnstile script', 'error'); + reject(new Error('Failed to load Turnstile')); + }; + document.head.appendChild(script); + }); + }, + + ensureTurnstileContainer() { + if (!this._turnstileContainer || !document.body.contains(this._turnstileContainer)) { + if (this._turnstileContainer) { + this._turnstileContainer.remove(); + } + + this._turnstileContainer = document.createElement('div'); + this._turnstileContainer.className = 'wplace-turnstile-hidden'; + this._turnstileContainer.setAttribute('aria-hidden', 'true'); + this._turnstileContainer.id = 'turnstile-widget-container'; + document.body.appendChild(this._turnstileContainer); + } + return this._turnstileContainer; + }, + + ensureTurnstileOverlayContainer() { + if (this._turnstileOverlay && document.body.contains(this._turnstileOverlay)) { + return this._turnstileOverlay; + } + + const overlay = document.createElement('div'); + overlay.id = 'turnstile-overlay-container'; + overlay.className = 'wplace-turnstile-overlay wplace-overlay-hidden'; + + const title = document.createElement('div'); + title.textContent = Utils.t('turnstileInstructions'); + title.className = 'wplace-turnstile-title'; + + const host = document.createElement('div'); + host.id = 'turnstile-overlay-host'; + host.className = 'wplace-turnstile-host'; + + const hideBtn = document.createElement('button'); + hideBtn.textContent = Utils.t('hideTurnstileBtn'); + hideBtn.className = 'wplace-turnstile-hide-btn'; + hideBtn.addEventListener('click', () => overlay.remove()); + + overlay.appendChild(title); + overlay.appendChild(host); + overlay.appendChild(hideBtn); + document.body.appendChild(overlay); + + this._turnstileOverlay = overlay; + return overlay; + }, + + async executeTurnstile(sitekey, action = 'paint') { + await this.loadTurnstile(); + + // Try reusing existing widget first if sitekey matches + if (this._turnstileWidgetId && this._lastSitekey === sitekey && window.turnstile?.execute) { + try { + console.log('šŸ”„ Reusing existing Turnstile widget...'); + const token = await Promise.race([ + window.turnstile.execute(this._turnstileWidgetId, { action }), + new Promise((_, reject) => + setTimeout(() => reject(new Error('Execute timeout')), 15000) + ), + ]); + if (token && token.length > 20) { + console.log('āœ… Token generated via widget reuse'); + return token; + } + } catch (error) { + console.log(' Widget reuse failed, will create a fresh widget:', error.message); + } + } + + // Try invisible widget first + const invisibleToken = await this.createTurnstileWidget(sitekey, action); + if (invisibleToken && invisibleToken.length > 20) { + return invisibleToken; + } + + console.log(' Falling back to interactive Turnstile (visible).'); + return await this.createTurnstileWidgetInteractive(sitekey, action); + }, + + async createTurnstileWidget(sitekey, action) { + return new Promise((resolve) => { + try { + // Force cleanup of any existing widget + if (this._turnstileWidgetId && window.turnstile?.remove) { + try { + window.turnstile.remove(this._turnstileWidgetId); + console.log('🧹 Cleaned up existing Turnstile widget'); + } catch (e) { + console.warn('āš ļø Widget cleanup warning:', e.message); + } + } + + const container = this.ensureTurnstileContainer(); + container.innerHTML = ''; + + // Verify Turnstile is available + if (!window.turnstile?.render) { + console.error('āŒ Turnstile not available for rendering'); + resolve(null); + return; + } + + console.log('šŸ”§ Creating invisible Turnstile widget...'); + const widgetId = window.turnstile.render(container, { + sitekey, + action, + size: 'invisible', + retry: 'auto', + 'retry-interval': 8000, + callback: (token) => { + console.log('āœ… Invisible Turnstile callback'); + resolve(token); + }, + 'error-callback': () => resolve(null), + 'timeout-callback': () => resolve(null), + }); + + this._turnstileWidgetId = widgetId; + this._lastSitekey = sitekey; + + if (!widgetId) { + return resolve(null); + } + + // Execute the widget and race with timeout + Promise.race([ + window.turnstile.execute(widgetId, { action }), + new Promise((_, reject) => + setTimeout(() => reject(new Error('Invisible execute timeout')), 12000) + ), + ]) + .then(resolve) + .catch(() => resolve(null)); + } catch (e) { + console.error('āŒ Invisible Turnstile creation failed:', e); + resolve(null); + } + }); + }, + + async createTurnstileWidgetInteractive(sitekey, action) { + // Create a visible widget that users can interact with if needed + console.log('šŸ”„ Creating interactive Turnstile widget (visible)'); + + return new Promise((resolve) => { + try { + // Force cleanup of any existing widget + if (this._turnstileWidgetId && window.turnstile?.remove) { + try { + window.turnstile.remove(this._turnstileWidgetId); + } catch (e) { + console.warn('āš ļø Widget cleanup warning:', e.message); + } + } + + const overlay = this.ensureTurnstileOverlayContainer(); + overlay.classList.remove('wplace-overlay-hidden'); + overlay.style.display = 'block'; + + const host = overlay.querySelector('#turnstile-overlay-host'); + host.innerHTML = ''; + + // Set a timeout for interactive mode + const timeout = setTimeout(() => { + console.warn('ā° Interactive Turnstile widget timeout'); + overlay.classList.add('wplace-overlay-hidden'); + overlay.style.display = 'none'; + resolve(null); + }, 60000); // 60 seconds for user interaction + + const widgetId = window.turnstile.render(host, { + sitekey, + action, + size: 'normal', + theme: 'light', + callback: (token) => { + clearTimeout(timeout); + overlay.classList.add('wplace-overlay-hidden'); + overlay.style.display = 'none'; + console.log('āœ… Interactive Turnstile completed successfully'); + + if (typeof token === 'string' && token.length > 20) { + resolve(token); + } else { + console.warn('āŒ Invalid token from interactive widget'); + resolve(null); + } + }, + 'error-callback': (error) => { + clearTimeout(timeout); + overlay.classList.add('wplace-overlay-hidden'); + overlay.style.display = 'none'; + console.warn('āŒ Interactive Turnstile error:', error); + resolve(null); + }, + }); + + this._turnstileWidgetId = widgetId; + this._lastSitekey = sitekey; + + if (!widgetId) { + clearTimeout(timeout); + overlay.classList.add('wplace-overlay-hidden'); + overlay.style.display = 'none'; + console.warn('āŒ Failed to create interactive Turnstile widget'); + resolve(null); + } else { + console.log('āœ… Interactive Turnstile widget created, waiting for user interaction...'); + } + } catch (e) { + console.error('āŒ Interactive Turnstile creation failed:', e); + resolve(null); + } + }); + }, + + cleanupTurnstile() { + if (this._turnstileWidgetId && window.turnstile?.remove) { + try { + window.turnstile.remove(this._turnstileWidgetId); + } catch (e) { + console.warn('Failed to cleanup Turnstile widget:', e); + } + } + + if (this._turnstileContainer && document.body.contains(this._turnstileContainer)) { + this._turnstileContainer.remove(); + } + + if (this._turnstileOverlay && document.body.contains(this._turnstileOverlay)) { + this._turnstileOverlay.remove(); + } + + this._turnstileWidgetId = null; + this._turnstileContainer = null; + this._turnstileOverlay = null; + this._lastSitekey = null; + }, + + // DETECCIƓN DE SITEKEY MEJORADA - ACTUALIZADA DESDE new.txt + async obtainSitekeyAndToken(fallback = '0x4AAAAAABpqJe8FO0N84q0F') { + // Cache sitekey to avoid repeated DOM queries + if (this._cachedSitekey) { + console.log('šŸ” Using cached sitekey:', this._cachedSitekey); + + return isTokenValid() + ? { + sitekey: this._cachedSitekey, + token: turnstileToken, + } + : { sitekey: this._cachedSitekey, token: null }; + } + + // List of potential sitekeys to try + const potentialSitekeys = [ + '0x4AAAAAABpqJe8FO0N84q0F', // WPlace common sitekey + '0x4AAAAAAAJ7xjKAp6Mt_7zw', // Alternative WPlace sitekey + '0x4AAAAAADm5QWx6Ov2LNF2g', // Another common sitekey + ]; + const trySitekey = async (sitekey, source) => { + if (!sitekey || sitekey.length < 10) return null; + + console.log(`šŸ” Testing sitekey from ${source}:`, sitekey); + const token = await this.executeTurnstile(sitekey); + + if (token && token.length >= 20) { + console.log(`āœ… Valid token generated from ${source} sitekey`); + setTurnstileToken(token); + this._cachedSitekey = sitekey; + return { sitekey, token }; + } else { + console.log(`āŒ Failed to get token from ${source} sitekey`); + return null; + } + }; + + try { + // 1ļøāƒ£ data-sitekey attribute + const sitekeySel = document.querySelector('[data-sitekey]'); + if (sitekeySel) { + const sitekey = sitekeySel.getAttribute('data-sitekey'); + const result = await trySitekey(sitekey, 'data attribute'); + if (result) { + return result; + } + } + + // 2ļøāƒ£ Turnstile element + const turnstileEl = document.querySelector('.cf-turnstile'); + if (turnstileEl?.dataset?.sitekey) { + const sitekey = turnstileEl.dataset.sitekey; + const result = await trySitekey(sitekey, 'turnstile element'); + if (result) { + return result; + } + } + + // 3ļøāƒ£ Meta tags + const metaTags = document.querySelectorAll( + 'meta[name*="turnstile"], meta[property*="turnstile"]' + ); + for (const meta of metaTags) { + const content = meta.getAttribute('content'); + const result = await trySitekey(content, 'meta tag'); + if (result) { + return result; + } + } + + // 4ļøāƒ£ Global variable + if (window.__TURNSTILE_SITEKEY) { + const result = await trySitekey(window.__TURNSTILE_SITEKEY, 'global variable'); + if (result) { + return result; + } + } + + // 5ļøāƒ£ Script tags + const scripts = document.querySelectorAll('script'); + for (const script of scripts) { + const content = script.textContent || script.innerHTML; + const match = content.match( + /(?:sitekey|data-sitekey)['"\s\[\]:\=\(]*['"]?([0-9a-zA-Z_-]{20,})['"]?/i + ); + if (match && match[1]) { + const extracted = match[1].replace(/['"]/g, ''); + const result = await trySitekey(extracted, 'script content'); + if (result) { + return result; + } + } + } + + // 6ļøāƒ£ Known potential sitekeys + console.log('šŸ” Testing known potential sitekeys...'); + for (const testSitekey of potentialSitekeys) { + const result = await trySitekey(testSitekey, 'known list'); + if (result) { + return result; + } + } + } catch (error) { + console.warn('āš ļø Error during sitekey detection:', error); + } + + // 7ļøāƒ£ Fallback + console.log('šŸ”§ Trying fallback sitekey:', fallback); + const fallbackResult = await trySitekey(fallback, 'fallback'); + if (fallbackResult) { + return fallbackResult; + } + + console.error('āŒ No working sitekey or token found.'); + return { sitekey: null, token: null }; + }, + }; + + // Enhanced Overlay Manager for pixel detection with autonomous capabilities + class OverlayManager { + constructor() { + this.originalTiles = new Map(); + this.originalTilesData = new Map(); + this.tileSize = 1000; + this.loadingPromises = new Map(); + this.autonomousMode = state.autonomousMode; + } + + async processAndRespondToTileRequest(eventData) { + const { endpoint, blobID, blobData } = eventData; + + const tileMatch = endpoint.match(/(\d+)\/(\d+)\.png/); + if (tileMatch) { + const tileX = parseInt(tileMatch[1], 10); + const tileY = parseInt(tileMatch[2], 10); + const tileKey = `${tileX},${tileY}`; + + try { + const originalBitmap = await createImageBitmap(blobData); + this.originalTiles.set(tileKey, originalBitmap); + + try { + let canvas, ctx; + if (typeof OffscreenCanvas !== 'undefined') { + canvas = new OffscreenCanvas(originalBitmap.width, originalBitmap.height); + ctx = canvas.getContext('2d'); + } else { + canvas = document.createElement('canvas'); + canvas.width = originalBitmap.width; + canvas.height = originalBitmap.height; + ctx = canvas.getContext('2d'); + } + ctx.imageSmoothingEnabled = false; + ctx.drawImage(originalBitmap, 0, 0); + const imgData = ctx.getImageData(0, 0, originalBitmap.width, originalBitmap.height); + + this.originalTilesData.set(tileKey, { + w: originalBitmap.width, + h: originalBitmap.height, + data: new Uint8ClampedArray(imgData.data), + }); + + if (this.autonomousMode) { + Utils.addDebugLog(`Auto-cached tile: ${tileKey} (${originalBitmap.width}x${originalBitmap.height})`, 'info'); + } + } catch (e) { + Utils.addDebugLog(`Failed to cache tile ImageData: ${tileKey} - ${e.message}`, 'warning'); + } + } catch (e) { + Utils.addDebugLog(`Failed to create tile bitmap: ${tileKey} - ${e.message}`, 'error'); + } + } + + window.postMessage( + { + source: 'auto-image-overlay', + blobID: blobID, + blobData: blobData, + }, + '*' + ); + } + + async getTilePixelColor(tileX, tileY, pixelX, pixelY) { + const tileKey = `${tileX},${tileY}`; + const alphaThresh = state.customTransparencyThreshold || CONFIG.TRANSPARENCY_THRESHOLD; + + const cached = this.originalTilesData.get(tileKey); + if (cached && cached.data && cached.w > 0 && cached.h > 0) { + const x = Math.max(0, Math.min(cached.w - 1, pixelX)); + const y = Math.max(0, Math.min(cached.h - 1, pixelY)); + const idx = (y * cached.w + x) * 4; + const d = cached.data; + const a = d[idx + 3]; + + // Always return pixel data, let caller decide about transparency + return [d[idx], d[idx + 1], d[idx + 2], a]; + } + + const bitmap = this.originalTiles.get(tileKey); + if (!bitmap) { + if (this.autonomousMode) { + Utils.addDebugLog(`Tile ${tileKey} not available, requesting load...`, 'warning'); + } + return null; + } + + try { + let canvas, ctx; + if (typeof OffscreenCanvas !== 'undefined') { + canvas = new OffscreenCanvas(bitmap.width, bitmap.height); + ctx = canvas.getContext('2d'); + } else { + canvas = document.createElement('canvas'); + canvas.width = bitmap.width; + canvas.height = bitmap.height; + ctx = canvas.getContext('2d'); + } + ctx.imageSmoothingEnabled = false; + ctx.drawImage(bitmap, 0, 0); + + const x = Math.max(0, Math.min(bitmap.width - 1, pixelX)); + const y = Math.max(0, Math.min(bitmap.height - 1, pixelY)); + const data = ctx.getImageData(x, y, 1, 1).data; + const a = data[3]; + + // 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'); + return null; + } + } + + async waitForTiles(startRegionX, startRegionY, pixelWidth, pixelHeight, startPixelX = 0, startPixelY = 0, timeoutMs = 15000) { + const { startTileX, startTileY, endTileX, endTileY } = Utils.calculateTileRange( + startRegionX, + startRegionY, + startPixelX, + startPixelY, + pixelWidth, + pixelHeight, + this.tileSize + ); + + const requiredTiles = []; + for (let ty = startTileY; ty <= endTileY; ty++) { + for (let tx = startTileX; tx <= endTileX; tx++) { + requiredTiles.push(`${tx},${ty}`); + } + } + + if (requiredTiles.length === 0) return true; + + Utils.addDebugLog(`Waiting for ${requiredTiles.length} tiles (autonomous: ${this.autonomousMode})...`, 'info'); + + const startTime = Date.now(); + let lastProgress = 0; + + while (Date.now() - startTime < timeoutMs) { + if (state.stopFlag) { + Utils.addDebugLog('waitForTiles: stopped by user', 'warning'); + return false; + } + + const loaded = requiredTiles.filter((k) => this.originalTiles.has(k)).length; + const progress = Math.round((loaded / requiredTiles.length) * 100); + + if (progress !== lastProgress && progress % 20 === 0) { + Utils.addDebugLog(`Tile loading progress: ${loaded}/${requiredTiles.length} (${progress}%)`, 'info'); + lastProgress = progress; + } + + if (loaded === requiredTiles.length) { + Utils.addDebugLog(`All ${requiredTiles.length} required tiles are loaded`, 'success'); + return true; + } + + await Utils.sleep(this.autonomousMode ? 500 : 1000); + } + + const loaded = requiredTiles.filter((k) => this.originalTiles.has(k)).length; + Utils.addDebugLog(`Timeout waiting for tiles: ${loaded}/${requiredTiles.length} loaded`, 'warning'); + + if (this.autonomousMode && loaded > requiredTiles.length * 0.8) { + Utils.addDebugLog(`Autonomous mode: proceeding with ${loaded}/${requiredTiles.length} tiles (80%+ loaded)`, 'warning'); + return true; + } + + return loaded > 0; + } + } + + const overlayManager = new OverlayManager(); + + // Enhanced WPlace API Service with auto-batch functionality + const WPlaceService = { + async paintPixelInRegion(regionX, regionY, pixelX, pixelY, color, retryCount = 0) { + try { + await ensureToken(); + if (!turnstileToken) { + Utils.addDebugLog('No valid token available for paint request', 'error'); + return 'token_error'; + } + + const payload = { + coords: [pixelX, pixelY], + colors: [color], + t: turnstileToken, + fp: fpStr32, + }; + + const wasmToken = await createWasmToken(regionX, regionY, payload); + if (!wasmToken) { + Utils.addDebugLog('Failed to generate WASM token', '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 pixel (${pixelX},${pixelY}). Token invalid/expired.`, 'error'); + invalidateToken(); + + if (retryCount < 2 && state.autonomousMode) { + Utils.addDebugLog(`Autonomous retry ${retryCount + 1}/2 for pixel (${pixelX},${pixelY})`, 'warning'); + await Utils.sleep(1000); + return await this.paintPixelInRegion(regionX, regionY, pixelX, pixelY, color, retryCount + 1); + } + + return 'token_error'; + } + + if (!res.ok) { + Utils.addDebugLog(`Paint request failed with status ${res.status}`, 'error'); + return false; + } + + const data = await res.json(); + const success = data?.painted === 1; + + if (success) { + Utils.addDebugLog(`Paint SUCCESS for (${pixelX},${pixelY}) with color ${color}`, 'success'); + } else { + Utils.addDebugLog(`Paint FAILED for (${pixelX},${pixelY}) - server response: ${JSON.stringify(data)}`, 'error'); + } + + return success; + } catch (e) { + Utils.addDebugLog(`Paint request error for (${pixelX},${pixelY}): ${e.message}`, 'error'); + return false; + } + }, + + // 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', { + credentials: 'include', + }); + if (!res.ok) return { charges: 0, max: 1, cooldown: CONFIG.COOLDOWN_DEFAULT }; + const data = await res.json(); + return { + charges: data.charges?.count ?? 0, + max: data.charges?.max ?? 1, + cooldown: data.charges?.cooldownMs ?? CONFIG.COOLDOWN_DEFAULT, + }; + } catch (e) { + Utils.addDebugLog(`Error fetching charges: ${e.message}`, 'warning'); + return { charges: 0, max: 1, cooldown: CONFIG.COOLDOWN_DEFAULT }; + } + }, + }; + + // 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 enhanced damage scan with transparent pixel detection...', 'info'); + updateStatus(Utils.t('scanningForDamage')); + + const damagedPixels = []; + const { width, height, pixels } = state.imageData; + + // Check if we're working with restored data - skip tile waiting for restored saves + const isRestoredData = state.availableColors && state.availableColors.length > 0 && state.colorsChecked; + + if (!isRestoredData) { + const ready = await overlayManager.waitForTiles( + state.region.x, + state.region.y, + width, + height, + state.startPosition.x, + state.startPosition.y, + state.autonomousMode ? 20000 : 15000 + ); + + if (!ready) { + Utils.addDebugLog('Failed to load required tiles for scanning', 'error'); + if (state.autonomousMode) { + Utils.addDebugLog('Autonomous mode: will retry scan in 30 seconds', 'warning'); + setTimeout(() => { + if (state.autoRepairEnabled && !state.stopFlag) { + scanForDamage(); + } + }, 30000); + } + return []; + } + } else { + Utils.addDebugLog('Using restored data - skipping tile wait for damage scan', '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++) { + for (let x = 0; x < width; x++) { + if (state.stopFlag) break; + + const idx = (y * width + x) * 4; + const originalR = pixels[idx]; + const originalG = pixels[idx + 1]; + const originalB = pixels[idx + 2]; + const originalA = pixels[idx + 3]; + + // Enhanced transparent pixel detection + const isOriginalTransparent = originalA < state.customTransparencyThreshold; + + // Skip white pixels if configured + if (!state.paintWhitePixels && Utils.isWhitePixel(originalR, originalG, originalB)) { + continue; + } + + scannedPixels++; + + const absX = state.startPosition.x + x; + const absY = state.startPosition.y + y; + const tileX = state.region.x + Math.floor(absX / 1000); + const tileY = state.region.y + Math.floor(absY / 1000); + const pixelX = absX % 1000; + const pixelY = absY % 1000; + + try { + const currentPixel = await overlayManager.getTilePixelColor(tileX, tileY, pixelX, pixelY); + + // Handle missing current pixel data (could indicate transparent area) + if (!currentPixel) { + 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 currentIsTransparent = currentPixel[3] < state.customTransparencyThreshold; + + // 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: { id: 63, name: 'Transparent', rgb: [0, 0, 0] }, + originalRgb: [originalR, originalG, originalB], + 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; + } + + // 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) { + // Pixel is already correctly painted - skip it + continue; + } + + // Pixel has wrong color - mark as damaged + 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'); + } + } + } + + if (state.autonomousMode && Date.now() - lastProgressUpdate > 5000) { + 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'); + } + } + + const logLevel = damagedPixels.length > 0 ? 'warning' : 'success'; + 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; + } + + async function repairDamagedPixels(damagedPixels) { + if (damagedPixels.length === 0) { + updateStatus(Utils.t('noDamageDetected')); + return 0; + } + + 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; + + // 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; + } + + 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); + + // Enhanced charge management for batch processing + await updateCharges(); + let chargeWaitAttempts = 0; + const maxChargeWaitAttempts = state.autonomousMode ? 20 : 10; + + 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'); + } + + await Utils.dynamicSleep(() => { + if (state.displayCharges >= actualBatchSize) return 0; + if (state.stopFlag) return 0; + return waitTime; + }); + 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); + } + + if (consecutiveFailures >= maxConsecutiveFailures) break; + } + + const message = Utils.t('repairComplete', { repaired: repairedCount }); + updateStatus(message); + Utils.addDebugLog(`${message} (${damagedPixels.length - repairedCount} remaining) - Used auto-batch with size ${state.currentBatchSize}`, 'success'); + + return repairedCount; + } + + 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); + + 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( + regionX, + regionY, + pixelX, + pixelY, + color || originalColor.id + ); + + if (result === 'token_error') { + Utils.addDebugLog('Token error during repair, refreshing token...', 'warning'); + await ensureToken(true); + await Utils.sleep(state.autonomousMode ? 500 : 1000); + + // Retry once with new token + 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'); + return false; + } + } + + async function performRepairCheck() { + if (state.running) { + Utils.addDebugLog('Repair check skipped - manual repair in progress', 'info'); + return; + } + + try { + Utils.addDebugLog('Autonomous repair check triggered', 'info'); + + // Ensure we have a valid token before starting + if (!isTokenValid()) { + Utils.addDebugLog('No valid token for autonomous repair, generating...', 'warning'); + await ensureToken(true); + if (!isTokenValid()) { + Utils.addDebugLog('Failed to generate token for autonomous repair, will retry next cycle', 'error'); + return; + } + } + + const damagedPixels = await scanForDamage(); + + if (damagedPixels.length > 0) { + Utils.addDebugLog(`Autonomous repair: Found ${damagedPixels.length} damaged pixels, starting repair`, 'warning'); + updateStatus(Utils.t('damageDetected', { count: damagedPixels.length })); + + // Show attack notification + showAttackNotification('attack', damagedPixels.length); + state.lastAttackState = 'attacked'; + + const repairedCount = await repairDamagedPixels(damagedPixels); + + // Show repair complete notification if all pixels were repaired + if (repairedCount === damagedPixels.length) { + showAttackNotification('repaired'); + state.lastAttackState = 'repaired'; + } + } else { + updateStatus(Utils.t('noDamageDetected')); + + // Show peaceful state if we're not under attack + if (state.lastAttackState !== 'peaceful') { + showPeacefulState(); + state.lastAttackState = 'peaceful'; + } + + if (!state.autonomousMode) { + Utils.addDebugLog('Autonomous repair: No damage detected', 'success'); + } + } + } catch (error) { + Utils.addDebugLog(`Autonomous repair error: ${error.message}`, 'error'); + + if (state.autonomousMode) { + Utils.addDebugLog('Autonomous mode: will retry repair check in 120 seconds due to error', 'warning'); + setTimeout(() => { + if (state.autoRepairEnabled && !state.stopFlag) { + performRepairCheck(); + } + }, 120000); + } + } + } + + function startAutoRepair() { + if (state.autoRepairTimer) { + clearInterval(state.autoRepairTimer); + } + + const intervalMs = state.autoRepairInterval * 1000; + state.autoRepairTimer = setInterval(performRepairCheck, intervalMs); + + const message = Utils.t('autoRepairStarted', { interval: state.autoRepairInterval }); + Utils.addDebugLog(`${message} (autonomous: ${state.autonomousMode})`, 'info'); + Utils.showAlert(message, 'success'); + + // Perform first check immediately + setTimeout(performRepairCheck, 2000); + } + + function stopAutoRepair() { + if (state.autoRepairTimer) { + clearInterval(state.autoRepairTimer); + state.autoRepairTimer = null; + } + + const message = Utils.t('autoRepairStopped'); + Utils.addDebugLog(`${message}`, 'info'); + Utils.showAlert(message, 'info'); + } + + // FUNCIONES DE UI MEJORADAS CON GESTIƓN DE CONFIGURACIƓN INICIAL + function enableFileOperations() { + state.initialSetupComplete = true; + + const loadBtn = document.querySelector('#loadFileBtn'); + if (loadBtn) { + loadBtn.disabled = false; + loadBtn.title = ''; + loadBtn.style.animation = 'pulse 0.6s ease-in-out'; + setTimeout(() => { + if (loadBtn) loadBtn.style.animation = ''; + }, 600); + Utils.addDebugLog('Load Progress button enabled after initial setup', 'success'); + } + + Utils.showAlert(Utils.t('fileOperationsAvailable'), 'success'); + } + + async function initializeTokenGenerator() { + if (isTokenValid()) { + Utils.addDebugLog('Valid token already available, skipping initialization', 'success'); + updateTokenStatus(); + enableFileOperations(); + return; + } + + try { + Utils.addDebugLog('Initializing Turnstile token generator...', 'info'); + updateStatus(Utils.t('initializingToken')); + + await Utils.loadTurnstile(); + Utils.addDebugLog('Turnstile script loaded successfully', 'success'); + + if (state.autoTokenRefresh) { + Utils.addDebugLog('Pre-generating token for autonomous operations...', 'info'); + await ensureToken(); + } + + state.initialSetupComplete = true; + Utils.addDebugLog('Enhanced token generator initialization complete', 'success'); + updateStatus(Utils.t('tokenReady')); + enableFileOperations(); + + } catch (error) { + Utils.addDebugLog('Token system initialization failed: ' + error.message, 'warning'); + Utils.addDebugLog('Manual token generation will be required', 'info'); + enableFileOperations(); + } + } + + function updateStatus(message) { + const statusEl = document.getElementById('status'); + if (statusEl) { + if (typeof message === 'string') { + statusEl.innerHTML = message; + } else { + statusEl.textContent = message; + } + } + } + + function updateDebugConsole() { + const debugConsole = document.getElementById('debugConsole'); + if (!debugConsole) return; + + const logsHtml = state.debugLogs.map(log => { + const colorClass = log.type === 'error' ? 'neon-text danger' : + log.type === 'warning' ? 'neon-text warning' : + log.type === 'success' ? 'neon-text success' : + log.type === 'info' ? 'neon-text info' : 'neon-text'; + + return `
+ [${log.timestamp}] ${log.message} +
`; + }).join(''); + + debugConsole.innerHTML = logsHtml; + debugConsole.scrollTop = debugConsole.scrollHeight; + } + + async function updateCharges() { + try { + const { charges, max, cooldown } = await WPlaceService.getCharges(); + state.displayCharges = Math.floor(charges); + state.maxCharges = Math.max(1, Math.floor(max)); + state.cooldown = cooldown; + + updateChargesDisplay(); + } catch (error) { + if (!state.autonomousMode) { + Utils.addDebugLog(`Error updating charges: ${error.message}`, 'error'); + } + } + } + + function updateChargesDisplay() { + const chargesEl = document.getElementById('chargesInfo'); + if (chargesEl) { + chargesEl.textContent = `Charges: ${state.displayCharges}/${state.maxCharges} (cooldown: ${Math.round(state.cooldown / 1000)}s)`; + } + } + + function updateTokenStatus() { + const tokenEl = document.getElementById('tokenInfo'); + if (tokenEl) { + // Remove existing classes first + tokenEl.className = ''; + + if (isTokenValid()) { + const remaining = Math.round((tokenExpiryTime - Date.now()) / 1000); + tokenEl.textContent = `Token: Valid (expires in ${remaining}s)`; + tokenEl.className = 'neon-text success'; + } else { + tokenEl.textContent = 'Token: Not generated or expired'; + tokenEl.className = 'neon-text warning'; + } + } + } + + // 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) { + const autonomousStatus = state.autonomousMode ? 'AUTONOMOUS' : 'MANUAL'; + const tokenMode = state.tokenSource.toUpperCase(); + systemEl.textContent = `System: ${autonomousStatus} | Token: ${tokenMode} | Auto-refresh: ${state.autoTokenRefresh ? 'ON' : 'OFF'}`; + } + } + + // Window dragging functionality + function makeWindowDraggable() { + const container = document.getElementById('wplace-repair-tool'); + const header = document.getElementById('window-header'); + + let isDragging = false; + let currentX; + let currentY; + let initialX; + let initialY; + let xOffset = 0; + let yOffset = 0; + + function dragStart(e) { + if (e.target.tagName === 'BUTTON' || e.target.tagName === 'INPUT' || e.target.tagName === 'SELECT') { + return; + } + + initialX = e.clientX - xOffset; + initialY = e.clientY - yOffset; + + if (e.target === header || header.contains(e.target)) { + isDragging = true; + container.style.cursor = 'grabbing'; + } + } + + function dragEnd() { + initialX = currentX; + initialY = currentY; + isDragging = false; + container.style.cursor = 'default'; + } + + function drag(e) { + if (isDragging) { + e.preventDefault(); + currentX = e.clientX - initialX; + currentY = e.clientY - initialY; + xOffset = currentX; + yOffset = currentY; + + container.style.transform = `translate(${currentX}px, ${currentY}px)`; + } + } + + header.addEventListener('mousedown', dragStart); + document.addEventListener('mouseup', dragEnd); + document.addEventListener('mousemove', drag); + } + + // Enhanced UI Creation with NEW IMPLEMENTATION + function createUI() { + const existing = document.getElementById('wplace-repair-tool'); + if (existing) existing.remove(); + + const container = document.createElement('div'); + container.id = 'wplace-repair-tool'; + container.style.cssText = ` + position: fixed; top: 20px; left: 20px; z-index: 10000; + background: #1a1a2e; + border: 3px solid #00ff41; + border-radius: 0; + min-width: 600px; max-width: 700px; + box-shadow: + 0 0 30px rgb(0 255 65 / 50%), + inset 0 0 30px rgb(0 255 65 / 10%), + 0 0 0 1px #00ff41; + color: #00ff41; + font-family: 'Press Start 2P', monospace, 'Courier New'; + resize: both; overflow: hidden; + animation: neon-pulse 2s ease-in-out infinite alternate; + `; + + // Add Neon theme styles + const neonStyles = document.createElement('style'); + neonStyles.id = 'auto-repair-neon-styles'; + neonStyles.textContent = ` + @import url('https://fonts.googleapis.com/css2?family=Press+Start+2P&display=swap'); + + #wplace-repair-tool::before { + content: ''; + position: absolute; + top: 0; + left: 0; + right: 0; + height: 2px; + background: linear-gradient(90deg, transparent, #00ff41, transparent); + z-index: 1; + pointer-events: none; + animation: scanline 3s linear infinite; + opacity: 0.7; + } + + .neon-btn { + background: #16213e !important; + border: 2px solid #00ff41 !important; + border-radius: 0 !important; + color: #00ff41 !important; + padding: 8px 15px !important; + font-family: 'Press Start 2P', monospace !important; + font-size: 8px !important; + text-transform: uppercase !important; + cursor: pointer !important; + transition: all 0.3s ease !important; + text-shadow: 0 0 8px #00ff41 !important; + letter-spacing: 1px !important; + margin: 4px !important; + } + + .neon-btn:hover { + background: rgba(0,255,65,0.1) !important; + box-shadow: 0 0 20px rgb(0 255 65 / 60%) !important; + animation: pixel-blink 0.5s infinite !important; + } + + .neon-btn:disabled { + background: #0d1117 !important; + border-color: #555 !important; + color: #555 !important; + text-shadow: none !important; + cursor: not-allowed !important; + } + + .neon-btn.danger { + border-color: #ff073a !important; + color: #ff073a !important; + text-shadow: 0 0 8px #ff073a !important; + } + + .neon-btn.danger:hover { + background: rgba(255, 7, 58, 0.1) !important; + box-shadow: 0 0 20px rgb(255 7 58 / 60%) !important; + } + + .neon-btn.warning { + border-color: #ff6b35 !important; + color: #ff6b35 !important; + text-shadow: 0 0 8px #ff6b35 !important; + } + + .neon-btn.warning:hover { + background: rgba(255, 107, 53, 0.1) !important; + box-shadow: 0 0 20px rgb(255 107 53 / 60%) !important; + } + + .neon-input { + background: #16213e !important; + border: 2px solid #00ff41 !important; + border-radius: 0 !important; + color: #00ff41 !important; + padding: 6px 10px !important; + font-family: 'Press Start 2P', monospace !important; + font-size: 8px !important; + text-shadow: 0 0 5px #00ff41 !important; + } + + .neon-input:focus { + outline: none !important; + box-shadow: 0 0 15px rgb(0 255 65 / 60%) !important; + animation: pixel-blink 0.5s infinite !important; + } + + .neon-select { + background: #16213e !important; + border: 2px solid #00ff41 !important; + border-radius: 0 !important; + color: #00ff41 !important; + padding: 4px 8px !important; + font-family: 'Press Start 2P', monospace !important; + font-size: 7px !important; + text-shadow: 0 0 5px #00ff41 !important; + } + + .neon-checkbox { + accent-color: #00ff41 !important; + transform: scale(1.2) !important; + 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; + border-radius: 0 !important; + padding: 15px !important; + margin: 10px 0 !important; + box-shadow: inset 0 0 15px rgba(0, 255, 65, 0.1) !important; + } + + .neon-text { + color: #00ff41 !important; + text-shadow: 0 0 5px #00ff41 !important; + font-family: 'Press Start 2P', monospace !important; + } + + .neon-text.success { + color: #39ff14 !important; + text-shadow: 0 0 5px #39ff14 !important; + } + + .neon-text.warning { + color: #ff6b35 !important; + text-shadow: 0 0 5px #ff6b35 !important; + } + + .neon-text.danger { + color: #ff073a !important; + text-shadow: 0 0 5px #ff073a !important; + } + + .neon-text.info { + color: #00ccff !important; + text-shadow: 0 0 5px #00ccff !important; + } + + /* Custom scrollbar for debug console */ + #debugConsole::-webkit-scrollbar { + width: 12px; + } + + #debugConsole::-webkit-scrollbar-track { + background: #16213e; + border: 1px solid #00ff41; + } + + #debugConsole::-webkit-scrollbar-thumb { + background: #00ff41; + border-radius: 0; + box-shadow: 0 0 10px #00ff41; + } + + #debugConsole::-webkit-scrollbar-thumb:hover { + background: #39ff14; + box-shadow: 0 0 15px #39ff14; + } + + /* Animations */ + @keyframes neon-pulse { + 0% { box-shadow: 0 0 30px rgb(0 255 65 / 50%), inset 0 0 30px rgb(0 255 65 / 10%), 0 0 0 1px #00ff41; } + 100% { box-shadow: 0 0 40px rgb(0 255 65 / 70%), inset 0 0 40px rgb(0 255 65 / 15%), 0 0 0 1px #00ff41; } + } + + @keyframes pixel-blink { + 0%, 50% { opacity: 1; } + 51%, 100% { opacity: 0.7; } + } + + @keyframes scanline { + 0% { transform: translateY(-100%); } + 100% { transform: translateY(400px); } + } + + @keyframes text-glow { + 0% { text-shadow: 0 0 15px #00ff41; } + 100% { text-shadow: 0 0 25px #00ff41, 0 0 35px #00ff41; } + } + `; + + if (!document.getElementById('auto-repair-neon-styles')) { + document.head.appendChild(neonStyles); + } + + container.innerHTML = ` +
+
+
šŸ”§
+

+ WPlace Autonomous Repair Tool +

+
+
+ + +
+
+ +
+
+
+ ${Utils.t('autonomousModeActive')} +
+
+ Charges: 0/1 (cooldown: 31s) +
+
+ Token: Initializing... +
+
+ System: AUTONOMOUS | Token: GENERATOR | Auto-refresh: ON +
+
+ Auto-Batch: ON | Size: 5 | Opt: ON +
+
+ +
+ + + + + +
+ +
+
+ +
+ +
+ + + seconds +
+ +
+ + +
+ + +
+
+ šŸŽÆ Pixel Batch Mode +
+ + +
+
+ +
+ +
+ +
+
+ + +
+ Batch Size: +
+ + + +
+ pixels +
+
+ +
+
+ +
+
Image: Not loaded
+
Position: Not set
+
Colors: 0 available
+
Batch Mode: Auto (${state.currentBatchSize} pixels)
+
+
+ +
+
+

+ šŸ“‹ ${Utils.t('debug')} (Enhanced System) +

+ +
+ +
+
[Ready] WPlace Autonomous Repair Tool v3.0
+
[Info] NEW Turnstile implementation active
+
[Info] Enhanced token management with preloading
+
[Info] ${Utils.t('tokenSystemReady')}
+
+
+
+ `; + + document.body.appendChild(container); + makeWindowDraggable(); + setupEventListeners(); + + // Start typewriter effect + setTimeout(() => { + createTypewriterTitle(); + }, 1000); + + // Start autonomous systems + initializeAutonomousSystems(); + } + + function initializeAutonomousSystems() { + // Update charges periodically + updateCharges(); + setInterval(updateCharges, 10000); + + // Update token status + updateTokenStatus(); + setInterval(updateTokenStatus, 5000); + + // Update system status + updateSystemStatus(); + setInterval(updateSystemStatus, 15000); + + // Update batch information + updateBatchInfo(); + + // Initialize token generator + initializeTokenGenerator(); + } + + function setupEventListeners() { + // Window controls + document.getElementById('minimizeBtn').addEventListener('click', () => { + const content = document.getElementById('window-content'); + state.windowMinimized = !state.windowMinimized; + + if (state.windowMinimized) { + content.style.display = 'none'; + document.getElementById('minimizeBtn').textContent = '+'; + } else { + content.style.display = 'block'; + document.getElementById('minimizeBtn').textContent = 'āˆ’'; + } + }); + + document.getElementById('closeBtn').addEventListener('click', () => { + const container = document.getElementById('wplace-repair-tool'); + if (container) { + container.style.transition = 'opacity 0.3s ease, transform 0.3s ease'; + container.style.opacity = '0'; + container.style.transform = 'scale(0.8)'; + setTimeout(() => { + container.remove(); + cleanup(); + }, 300); + } + }); + + // Load file + document.getElementById('loadFileBtn').addEventListener('click', async () => { + try { + Utils.addDebugLog('Loading progress file...', 'info'); + const data = await Utils.createFileUploader(); + + if (!data || !data.state || !data.imageData) { + throw new Error(Utils.t('invalidFile')); + } + + const migratedData = migrateProgressData(data); + + Object.assign(state, migratedData.state); + state.imageData = { + ...migratedData.imageData, + pixels: new Uint8ClampedArray(migratedData.imageData.pixels), + }; + + state.imageLoaded = true; + document.getElementById('repairBtn').disabled = false; + + // Update info displays + document.getElementById('imageInfo').textContent = `${state.imageData.width}x${state.imageData.height}`; + document.getElementById('positionInfo').textContent = state.startPosition ? + `(${state.startPosition.x}, ${state.startPosition.y}) in region (${state.region.x}, ${state.region.y})` : 'Not set'; + document.getElementById('colorsInfo').textContent = `${state.availableColors?.length || 0} available`; + + updateStatus(Utils.t('fileLoaded')); + Utils.addDebugLog(`Loaded image: ${state.imageData.width}x${state.imageData.height}`, 'success'); + Utils.addDebugLog(`Position: (${state.startPosition?.x}, ${state.startPosition?.y})`, 'info'); + Utils.addDebugLog(`Region: (${state.region?.x}, ${state.region?.y})`, 'info'); + Utils.addDebugLog(`Available colors: ${state.availableColors?.length || 0}`, 'info'); + + if (migratedData.state.paintWhitePixels !== undefined) { + state.paintWhitePixels = migratedData.state.paintWhitePixels; + Utils.addDebugLog(`Paint white pixels: ${state.paintWhitePixels}`, 'info'); + } + if (migratedData.state.paintTransparentPixels !== undefined) { + state.paintTransparentPixels = migratedData.state.paintTransparentPixels; + Utils.addDebugLog(`Paint transparent pixels: ${state.paintTransparentPixels}`, 'info'); + } + + Utils.showAlert(Utils.t('fileLoaded'), 'success'); + + // If autonomous mode and auto repair is enabled, start it + if (state.autonomousMode && document.getElementById('autoRepairEnabled').checked) { + Utils.addDebugLog('Autonomous mode: starting auto repair after file load', 'info'); + setTimeout(() => { + state.autoRepairEnabled = true; + startAutoRepair(); + }, 5000); + } + + } catch (error) { + Utils.addDebugLog(`Load error: ${error.message}`, 'error'); + Utils.showAlert(error.message, 'error'); + } + }); + + // Generate Token button + document.getElementById('generateTokenBtn').addEventListener('click', async () => { + const btn = document.getElementById('generateTokenBtn'); + const originalText = btn.textContent; + + try { + btn.disabled = true; + btn.textContent = 'šŸ”„ Generating...'; + btn.style.background = 'linear-gradient(135deg, #ffa726 0%, #ff9800 100%)'; + + Utils.addDebugLog('Manual token generation requested', 'info'); + + const token = await ensureToken(true); + + if (token) { + Utils.addDebugLog('Token generation succeeded!', 'success'); + Utils.showAlert('Token generated successfully!', 'success'); + updateTokenStatus(); + } else { + throw new Error('Token generation failed'); + } + } catch (error) { + Utils.addDebugLog(`Token generation failed: ${error.message}`, 'error'); + Utils.showAlert('Token generation failed', 'error'); + playNotificationSound(); + } finally { + btn.disabled = false; + btn.textContent = originalText; + btn.style.background = 'linear-gradient(135deg, #43e97b 0%, #38f9d7 100%)'; + } + }); + + // Manual repair + let repairRunning = false; + document.getElementById('repairBtn').addEventListener('click', async () => { + if (!state.imageLoaded) { + Utils.showAlert('Please load a progress file first', 'warning'); + return; + } + + if (repairRunning) { + state.stopFlag = true; + Utils.addDebugLog('Manual repair stop requested', 'warning'); + return; + } + + repairRunning = true; + state.running = true; + state.stopFlag = false; + const repairBtn = document.getElementById('repairBtn'); + repairBtn.textContent = 'āøļø Stop Repair'; + repairBtn.style.background = 'linear-gradient(135deg, #ff6b35 0%, #f7931e 100%)'; + + try { + Utils.addDebugLog('Starting manual repair with enhanced autonomous system...', 'info'); + + if (!isTokenValid()) { + Utils.addDebugLog('No valid token, generating...', 'info'); + await ensureToken(true); + } + + const damagedPixels = await scanForDamage(); + + if (damagedPixels.length > 0) { + updateStatus(Utils.t('damageDetected', { count: damagedPixels.length })); + showAttackNotification('attack', damagedPixels.length); + + const repairedCount = await repairDamagedPixels(damagedPixels); + + if (repairedCount === damagedPixels.length) { + showAttackNotification('repaired'); + } + } else { + updateStatus(Utils.t('noDamageDetected')); + showPeacefulState(); + } + } catch (error) { + Utils.addDebugLog(`Repair error: ${error.message}`, 'error'); + Utils.showAlert('Repair failed', 'error'); + } finally { + repairRunning = false; + state.running = false; + state.stopFlag = false; + repairBtn.textContent = 'šŸ”§ ' + Utils.t('repairPixels'); + repairBtn.style.background = 'linear-gradient(135deg, #f093fb 0%, #f5576c 100%)'; + } + }); + + // Auto repair toggle + document.getElementById('autoRepairEnabled').addEventListener('change', (e) => { + state.autoRepairEnabled = e.target.checked; + + if (state.autoRepairEnabled) { + if (!state.imageLoaded) { + Utils.showAlert('Please load a progress file first', 'warning'); + e.target.checked = false; + state.autoRepairEnabled = false; + return; + } + startAutoRepair(); + } else { + stopAutoRepair(); + } + }); + + // Repair interval change + document.getElementById('repairInterval').addEventListener('change', (e) => { + const value = parseInt(e.target.value) || 30; + state.autoRepairInterval = Math.max(10, Math.min(3600, value)); + e.target.value = state.autoRepairInterval; + + Utils.addDebugLog(`Repair interval changed to ${state.autoRepairInterval} seconds`, 'info'); + + if (state.autoRepairEnabled && state.imageLoaded) { + Utils.addDebugLog('Restarting auto repair with new interval', 'info'); + startAutoRepair(); + } + }); + + // Auto token refresh toggle + document.getElementById('autoTokenRefresh').addEventListener('change', (e) => { + state.autoTokenRefresh = e.target.checked; + Utils.addDebugLog(`Auto token refresh ${state.autoTokenRefresh ? 'enabled' : 'disabled'}`, 'info'); + updateSystemStatus(); + + if (state.autoTokenRefresh && state.autonomousMode && !isTokenValid()) { + ensureToken(); + } + }); + + // Token source selection + document.getElementById('tokenSourceSelect').addEventListener('change', (e) => { + state.tokenSource = e.target.value; + Utils.addDebugLog(`Token source changed to: ${state.tokenSource}`, 'info'); + updateSystemStatus(); + + // Invalidate current token to force new generation with new method + if (isTokenValid()) { + Utils.addDebugLog('Invalidating current token due to source change', 'info'); + invalidateToken(); + } + }); + + // 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 = []; + updateDebugConsole(); + Utils.addDebugLog('Debug console cleared', 'info'); + Utils.addDebugLog('Enhanced WPlace Autonomous Repair Tool ready', 'info'); + }); + } + + // Enhanced data migration helper + function migrateProgressData(data) { + if (!data.version || data.version < '2.0') { + Utils.addDebugLog('Migrating old progress data format...', 'info'); + + if (!data.state) data.state = {}; + if (!data.imageData) data.imageData = {}; + + data.state.paintWhitePixels = data.state.paintWhitePixels ?? true; + data.state.paintTransparentPixels = data.state.paintTransparentPixels ?? false; + + data.version = '2.0'; + } + + // Additional migrations for autonomous features + if (data.version === '2.0') { + data.state.autonomousMode = data.state.autonomousMode ?? CONFIG.AUTONOMOUS_MODE; + data.state.autoTokenRefresh = data.state.autoTokenRefresh ?? CONFIG.AUTO_TOKEN_REFRESH; + data.state.tokenSource = data.state.tokenSource ?? CONFIG.TOKEN_SOURCE; + data.version = '3.0'; + Utils.addDebugLog('Migrated to autonomous features v3.0', 'info'); + } + + return data; + } + + // Main initialization function + async function initialize() { + Utils.addDebugLog('Starting WPlace Autonomous Repair Tool v3.0...', 'info'); + + // Create UI first + createUI(); + + Utils.addDebugLog('NEW Enhanced Features Initialized:', 'info'); + Utils.addDebugLog('• NEW Turnstile implementation with widget reuse', 'info'); + Utils.addDebugLog('• Enhanced sitekey detection from multiple sources', 'info'); + Utils.addDebugLog('• Sophisticated token management with preloading', 'info'); + Utils.addDebugLog('• Autonomous operation with smart retry logic', 'info'); + Utils.addDebugLog('• Enhanced tile interception and caching', 'info'); + Utils.addDebugLog('• Adaptive repair algorithms with failure handling', 'info'); + Utils.addDebugLog('• Visual notifications and audio alerts', 'info'); + Utils.addDebugLog('• Draggable window with minimize/close controls', 'info'); + + if (state.autonomousMode) { + Utils.addDebugLog('AUTONOMOUS MODE ACTIVE - Tool will operate independently', 'success'); + Utils.addDebugLog('Instructions for autonomous operation:', 'info'); + Utils.addDebugLog('1. Wait for initial setup to complete', 'info'); + Utils.addDebugLog('2. Load a progress file using "Load Progress File"', 'info'); + Utils.addDebugLog('3. Enable "Auto Repair" to start autonomous monitoring', 'info'); + Utils.addDebugLog('4. Tool will automatically scan and repair damage', 'info'); + Utils.addDebugLog('5. Tokens will be generated and refreshed automatically', 'info'); + Utils.addDebugLog('6. Monitor debug console for autonomous operation logs', 'info'); + } else { + Utils.addDebugLog('Manual mode active - use controls for manual operation', 'info'); + } + + // Show autonomous mode status + updateStatus(state.autonomousMode ? Utils.t('autonomousModeActive') : 'Manual mode ready - Load file and enable auto repair to start'); + + Utils.addDebugLog('WPlace Autonomous Repair Tool ready for operation', 'success'); + } + + // Enhanced cleanup function + function cleanup() { + Utils.addDebugLog('Cleaning up autonomous systems...', 'info'); + + // Clear all timers + if (state.autoRepairTimer) { + clearInterval(state.autoRepairTimer); + state.autoRepairTimer = null; + } + + if (state.tokenRetryTimer) { + clearTimeout(state.tokenRetryTimer); + state.tokenRetryTimer = null; + } + + if (state.tokenPreloadTimer) { + clearTimeout(state.tokenPreloadTimer); + state.tokenPreloadTimer = null; + } + + // Cleanup Turnstile + Utils.cleanupTurnstile(); + + // Set stop flag + state.stopFlag = true; + + Utils.addDebugLog('Cleanup completed', 'info'); + } + + // Add cleanup on page unload + window.addEventListener('beforeunload', cleanup); + + // Handle page visibility changes for autonomous mode + document.addEventListener('visibilitychange', () => { + if (state.autonomousMode) { + if (document.hidden) { + Utils.addDebugLog('Page hidden - autonomous systems continue running', 'info'); + } else { + Utils.addDebugLog('Page visible - autonomous systems active', 'info'); + // Refresh status when page becomes visible + updateCharges(); + updateTokenStatus(); + } + } + }); + + // Enhanced error handling for autonomous mode + window.addEventListener('error', (event) => { + if (state.autonomousMode) { + Utils.addDebugLog(`Global error in autonomous mode: ${event.error?.message || 'Unknown error'}`, 'error'); + } + }); + + window.addEventListener('unhandledrejection', (event) => { + if (state.autonomousMode) { + Utils.addDebugLog(`Unhandled promise rejection in autonomous mode: ${event.reason}`, 'error'); + } + }); + + // Start the enhanced autonomous application + initialize().catch(error => { + console.error('Failed to initialize autonomous repair tool:', error); + Utils.addDebugLog(`Initialization failed: ${error.message}`, 'error'); + }); + +})(); diff --git a/Extensions/scripts/image-processor.js b/Extensions/scripts/image-processor.js new file mode 100644 index 0000000..37beb2e --- /dev/null +++ b/Extensions/scripts/image-processor.js @@ -0,0 +1,1315 @@ +// ==UserScript== +// @name WPlace Image Processor +// @namespace http://tampermonkey.net/ +// @version 2025-09-16.1 +// @description Image processing and color management for WPlace AutoBot +// @author Wbot +// @match https://wplace.live/* +// @grant none +// ==/UserScript== + +/** + * ImageProcessor - Handles image loading, processing, color conversion, and dithering for WPlace AutoBot + * Extracted from Auto-Image.js for better modularity and reusabilit + */ +class ImageProcessor { + constructor(imageSrc = null) { + this.imageSrc = imageSrc; + this.img = null; + this.canvas = null; + this.ctx = null; + + // Dithering buffers + this._ditherWorkBuf = null; + this._ditherEligibleBuf = null; + + // Color cache for performance optimization + this._colorCache = new Map(); + this._labCache = new Map(); + this._hsvCache = new Map(); + this._hslCache = new Map(); + this._xyzCache = new Map(); + this._luvCache = new Map(); + this._yuvCache = new Map(); + this._oklabCache = new Map(); + this._lchCache = new Map(); + + // Configuration constants + this.TRANSPARENCY_THRESHOLD = 128; + this.WHITE_THRESHOLD = 230; + this.COLOR_CACHE_LIMIT = 15000; + } + + /** + * Load image from source + * @returns {Promise} + */ + async load() { + if (!this.imageSrc) { + throw new Error('No image source provided'); + } + + return new Promise((resolve, reject) => { + this.img = new Image(); + this.img.crossOrigin = 'anonymous'; + this.img.onload = () => { + this.canvas = document.createElement('canvas'); + this.ctx = this.canvas.getContext('2d'); + this.canvas.width = this.img.width; + this.canvas.height = this.img.height; + this.ctx.drawImage(this.img, 0, 0); + resolve(); + }; + this.img.onerror = reject; + this.img.src = this.imageSrc; + }); + } + + /** + * Get image dimensions + * @returns {{width: number, height: number}} + */ + getDimensions() { + if (!this.canvas) { + throw new Error('Image not loaded. Call load() first.'); + } + return { + width: this.canvas.width, + height: this.canvas.height, + }; + } + + /** + * Get pixel data from the image + * @returns {Uint8ClampedArray} RGBA pixel data + */ + getPixelData() { + if (!this.ctx) { + throw new Error('Image not loaded. Call load() first.'); + } + return this.ctx.getImageData(0, 0, this.canvas.width, this.canvas.height).data; + } + + /** + * Resize image to new dimensions + * @param {number} newWidth - Target width + * @param {number} newHeight - Target height + * @returns {Uint8ClampedArray} Resized image data + */ + resize(newWidth, newHeight) { + if (!this.canvas || !this.ctx) { + throw new Error('Image not loaded. Call load() first.'); + } + + const tempCanvas = document.createElement('canvas'); + const tempCtx = tempCanvas.getContext('2d'); + + tempCanvas.width = newWidth; + tempCanvas.height = newHeight; + + tempCtx.imageSmoothingEnabled = false; + tempCtx.drawImage(this.canvas, 0, 0, newWidth, newHeight); + + this.canvas.width = newWidth; + this.canvas.height = newHeight; + this.ctx.imageSmoothingEnabled = false; + this.ctx.drawImage(tempCanvas, 0, 0); + + return this.ctx.getImageData(0, 0, newWidth, newHeight).data; + } + + /** + * Generate preview with resizing + * @param {number} width - Preview width + * @param {number} height - Preview height + * @returns {string} Base64 data URL of the preview + */ + generatePreview(width, height) { + if (!this.canvas) { + throw new Error('Image not loaded. Call load() first.'); + } + + const previewCanvas = document.createElement('canvas'); + const previewCtx = previewCanvas.getContext('2d'); + previewCanvas.width = width; + previewCanvas.height = height; + + previewCtx.imageSmoothingEnabled = false; + previewCtx.drawImage(this.canvas, 0, 0, width, height); + + return previewCanvas.toDataURL(); + } + + // ============================================= + // COLOR PROCESSING METHODS + // ============================================= + + /** + * Convert RGB to LAB color space + * @param {number} r - Red value (0-255) + * @param {number} g - Green value (0-255) + * @param {number} b - Blue value (0-255) + * @returns {number[]} LAB values [L, a, b] + * @private + */ + _rgbToLab(r, g, b) { + // Normalize RGB values + let x = r / 255.0; + let y = g / 255.0; + let z = b / 255.0; + + // Apply gamma correction + x = x > 0.04045 ? Math.pow((x + 0.055) / 1.055, 2.4) : x / 12.92; + y = y > 0.04045 ? Math.pow((y + 0.055) / 1.055, 2.4) : y / 12.92; + z = z > 0.04045 ? Math.pow((z + 0.055) / 1.055, 2.4) : z / 12.92; + + // Convert to XYZ using sRGB matrix + let X = x * 0.4124564 + y * 0.3575761 + z * 0.1804375; + let Y = x * 0.2126729 + y * 0.7151522 + z * 0.0721750; + let Z = x * 0.0193339 + y * 0.1191920 + z * 0.9503041; + + // Normalize for D65 illuminant + X /= 0.95047; + Y /= 1.00000; + Z /= 1.08883; + + // Convert to LAB + X = X > 0.008856 ? Math.pow(X, 1/3) : (7.787 * X + 16/116); + Y = Y > 0.008856 ? Math.pow(Y, 1/3) : (7.787 * Y + 16/116); + Z = Z > 0.008856 ? Math.pow(Z, 1/3) : (7.787 * Z + 16/116); + + const L = 116 * Y - 16; + const a = 500 * (X - Y); + const b2 = 200 * (Y - Z); + + return [L, a, b2]; + } + + /** + * Get LAB values with caching + * @param {number} r - Red value + * @param {number} g - Green value + * @param {number} b - Blue value + * @returns {number[]} Cached LAB values + * @private + */ + _getLab(r, g, b) { + const key = (r << 16) | (g << 8) | b; + let v = this._labCache.get(key); + if (!v) { + v = this._rgbToLab(r, g, b); + this._labCache.set(key, v); + } + return v; + } + + /** + * Convert RGB to HSV color space + * @param {number} r - Red value (0-255) + * @param {number} g - Green value (0-255) + * @param {number} b - Blue value (0-255) + * @returns {number[]} HSV values [H (0-360), S (0-1), V (0-1)] + * @private + */ + _rgbToHsv(r, g, b) { + r /= 255; + g /= 255; + b /= 255; + + const max = Math.max(r, g, b); + const min = Math.min(r, g, b); + const delta = max - min; + + let h = 0; + const s = max === 0 ? 0 : delta / max; + const v = max; + + if (delta !== 0) { + if (max === r) { + h = ((g - b) / delta) % 6; + } else if (max === g) { + h = (b - r) / delta + 2; + } else { + h = (r - g) / delta + 4; + } + h *= 60; + if (h < 0) h += 360; + } + + return [h, s, v]; + } + + /** + * Convert HSV to RGB color space + * @param {number} h - Hue value (0-360) + * @param {number} s - Saturation value (0-1) + * @param {number} v - Value/Brightness (0-1) + * @returns {number[]} RGB values [R, G, B] (0-255) + * @private + */ + _hsvToRgb(h, s, v) { + const c = v * s; + const x = c * (1 - Math.abs(((h / 60) % 2) - 1)); + const m = v - c; + + let r = 0, g = 0, b = 0; + + if (h >= 0 && h < 60) { + r = c; g = x; b = 0; + } else if (h >= 60 && h < 120) { + r = x; g = c; b = 0; + } else if (h >= 120 && h < 180) { + r = 0; g = c; b = x; + } else if (h >= 180 && h < 240) { + r = 0; g = x; b = c; + } else if (h >= 240 && h < 300) { + r = x; g = 0; b = c; + } else if (h >= 300 && h < 360) { + r = c; g = 0; b = x; + } + + return [ + Math.round((r + m) * 255), + Math.round((g + m) * 255), + Math.round((b + m) * 255) + ]; + } + + /** + * Convert RGB to HSL color space + * @param {number} r - Red value (0-255) + * @param {number} g - Green value (0-255) + * @param {number} b - Blue value (0-255) + * @returns {number[]} HSL values [H (0-360), S (0-1), L (0-1)] + * @private + */ + _rgbToHsl(r, g, b) { + r /= 255; + g /= 255; + b /= 255; + + const max = Math.max(r, g, b); + const min = Math.min(r, g, b); + const delta = max - min; + + let h = 0; + let s = 0; + const l = (max + min) / 2; + + if (delta !== 0) { + s = l > 0.5 ? delta / (2 - max - min) : delta / (max + min); + + if (max === r) { + h = ((g - b) / delta) % 6; + } else if (max === g) { + h = (b - r) / delta + 2; + } else { + h = (r - g) / delta + 4; + } + h *= 60; + if (h < 0) h += 360; + } + + return [h, s, l]; + } + + /** + * Convert HSL to RGB color space + * @param {number} h - Hue value (0-360) + * @param {number} s - Saturation value (0-1) + * @param {number} l - Lightness value (0-1) + * @returns {number[]} RGB values [R, G, B] (0-255) + * @private + */ + _hslToRgb(h, s, l) { + const c = (1 - Math.abs(2 * l - 1)) * s; + const x = c * (1 - Math.abs(((h / 60) % 2) - 1)); + const m = l - c / 2; + + let r = 0, g = 0, b = 0; + + if (h >= 0 && h < 60) { + r = c; g = x; b = 0; + } else if (h >= 60 && h < 120) { + r = x; g = c; b = 0; + } else if (h >= 120 && h < 180) { + r = 0; g = c; b = x; + } else if (h >= 180 && h < 240) { + r = 0; g = x; b = c; + } else if (h >= 240 && h < 300) { + r = x; g = 0; b = c; + } else if (h >= 300 && h < 360) { + r = c; g = 0; b = x; + } + + return [ + Math.round((r + m) * 255), + Math.round((g + m) * 255), + Math.round((b + m) * 255) + ]; + } + + /** + * Convert RGB to XYZ color space + * @param {number} r - Red value (0-255) + * @param {number} g - Green value (0-255) + * @param {number} b - Blue value (0-255) + * @returns {number[]} XYZ values [X, Y, Z] + * @private + */ + _rgbToXyz(r, g, b) { + // Normalize RGB values + let x = r / 255.0; + let y = g / 255.0; + let z = b / 255.0; + + // Apply gamma correction + x = x > 0.04045 ? Math.pow((x + 0.055) / 1.055, 2.4) : x / 12.92; + y = y > 0.04045 ? Math.pow((y + 0.055) / 1.055, 2.4) : y / 12.92; + z = z > 0.04045 ? Math.pow((z + 0.055) / 1.055, 2.4) : z / 12.92; + + // Convert to XYZ using sRGB matrix + const X = x * 0.4124564 + y * 0.3575761 + z * 0.1804375; + const Y = x * 0.2126729 + y * 0.7151522 + z * 0.0721750; + const Z = x * 0.0193339 + y * 0.1191920 + z * 0.9503041; + + return [X, Y, Z]; + } + + /** + * Convert XYZ to RGB color space + * @param {number} x - X value + * @param {number} y - Y value + * @param {number} z - Z value + * @returns {number[]} RGB values [R, G, B] (0-255) + * @private + */ + _xyzToRgb(x, y, z) { + // Convert XYZ to linear RGB + let r = x * 3.2404542 + y * -1.5371385 + z * -0.4985314; + let g = x * -0.9692660 + y * 1.8760108 + z * 0.0415560; + let b = x * 0.0556434 + y * -0.2040259 + z * 1.0572252; + + // Apply gamma correction + r = r > 0.0031308 ? 1.055 * Math.pow(r, 1.0 / 2.4) - 0.055 : 12.92 * r; + g = g > 0.0031308 ? 1.055 * Math.pow(g, 1.0 / 2.4) - 0.055 : 12.92 * g; + b = b > 0.0031308 ? 1.055 * Math.pow(b, 1.0 / 2.4) - 0.055 : 12.92 * b; + + // Clamp and convert to 0-255 range + return [ + Math.round(Math.max(0, Math.min(1, r)) * 255), + Math.round(Math.max(0, Math.min(1, g)) * 255), + Math.round(Math.max(0, Math.min(1, b)) * 255) + ]; + } + + /** + * Convert RGB to LUV color space + * @param {number} r - Red value (0-255) + * @param {number} g - Green value (0-255) + * @param {number} b - Blue value (0-255) + * @returns {number[]} LUV values [L, u, v] + * @private + */ + _rgbToLuv(r, g, b) { + const [X, Y, Z] = this._rgbToXyz(r, g, b); + + // Reference white D65 + const Xn = 0.95047; + const Yn = 1.00000; + const Zn = 1.08883; + + const yr = Y / Yn; + const L = yr > 0.008856 ? 116 * Math.pow(yr, 1/3) - 16 : 903.3 * yr; + + const denom = X + 15 * Y + 3 * Z; + const denomN = Xn + 15 * Yn + 3 * Zn; + + const up = denom === 0 ? 0 : (4 * X) / denom; + const vp = denom === 0 ? 0 : (9 * Y) / denom; + const upN = (4 * Xn) / denomN; + const vpN = (9 * Yn) / denomN; + + const u = 13 * L * (up - upN); + const v = 13 * L * (vp - vpN); + + return [L, u, v]; + } + + /** + * Convert LUV to RGB color space + * @param {number} l - L value + * @param {number} u - u value + * @param {number} v - v value + * @returns {number[]} RGB values [R, G, B] (0-255) + * @private + */ + _luvToRgb(l, u, v) { + // Reference white D65 + const Xn = 0.95047; + const Yn = 1.00000; + const Zn = 1.08883; + + const upN = (4 * Xn) / (Xn + 15 * Yn + 3 * Zn); + const vpN = (9 * Yn) / (Xn + 15 * Yn + 3 * Zn); + + const Y = l > 8 ? Yn * Math.pow((l + 16) / 116, 3) : Yn * l / 903.3; + + const up = u / (13 * l) + upN; + const vp = v / (13 * l) + vpN; + + const X = Y * 9 * up / (4 * vp); + const Z = Y * (12 - 3 * up - 20 * vp) / (4 * vp); + + return this._xyzToRgb(X, Y, Z); + } + + /** + * Convert RGB to YUV color space + * @param {number} r - Red value (0-255) + * @param {number} g - Green value (0-255) + * @param {number} b - Blue value (0-255) + * @returns {number[]} YUV values [Y, U, V] + * @private + */ + _rgbToYuv(r, g, b) { + const Y = 0.299 * r + 0.587 * g + 0.114 * b; + const U = -0.14713 * r - 0.28886 * g + 0.436 * b; + const V = 0.615 * r - 0.51499 * g - 0.10001 * b; + + return [Y, U, V]; + } + + /** + * Convert YUV to RGB color space + * @param {number} y - Y (luminance) value + * @param {number} u - U (chrominance) value + * @param {number} v - V (chrominance) value + * @returns {number[]} RGB values [R, G, B] (0-255) + * @private + */ + _yuvToRgb(y, u, v) { + const r = y + 1.13983 * v; + const g = y - 0.39465 * u - 0.58060 * v; + const b = y + 2.03211 * u; + + return [ + Math.round(Math.max(0, Math.min(255, r))), + Math.round(Math.max(0, Math.min(255, g))), + Math.round(Math.max(0, Math.min(255, b))) + ]; + } + + /** + * Convert RGB to Oklab color space (improved LAB) + * @param {number} r - Red value (0-255) + * @param {number} g - Green value (0-255) + * @param {number} b - Blue value (0-255) + * @returns {number[]} Oklab values [L, a, b] + * @private + */ + _rgbToOklab(r, g, b) { + // Normalize RGB + r /= 255; + g /= 255; + b /= 255; + + // Apply gamma correction + r = r > 0.04045 ? Math.pow((r + 0.055) / 1.055, 2.4) : r / 12.92; + g = g > 0.04045 ? Math.pow((g + 0.055) / 1.055, 2.4) : g / 12.92; + b = b > 0.04045 ? Math.pow((b + 0.055) / 1.055, 2.4) : b / 12.92; + + // Linear RGB to Oklab + const l = 0.4122214708 * r + 0.5363325363 * g + 0.0514459929 * b; + const m = 0.2119034982 * r + 0.6806995451 * g + 0.1073969566 * b; + const s = 0.0883024619 * r + 0.2817188376 * g + 0.6299787005 * b; + + const l_ = Math.cbrt(l); + const m_ = Math.cbrt(m); + const s_ = Math.cbrt(s); + + return [ + 0.2104542553 * l_ + 0.7936177850 * m_ - 0.0040720468 * s_, + 1.9779984951 * l_ - 2.4285922050 * m_ + 0.4505937099 * s_, + 0.0259040371 * l_ + 0.7827717662 * m_ - 0.8086757660 * s_ + ]; + } + + /** + * Convert Oklab to RGB color space + * @param {number} l - L value + * @param {number} a - a value + * @param {number} b - b value + * @returns {number[]} RGB values [R, G, B] (0-255) + * @private + */ + _oklabToRgb(l, a, b) { + const l_ = l + 0.3963377774 * a + 0.2158037573 * b; + const m_ = l - 0.1055613458 * a - 0.0638541728 * b; + const s_ = l - 0.0894841775 * a - 1.2914855480 * b; + + const l3 = l_ * l_ * l_; + const m3 = m_ * m_ * m_; + const s3 = s_ * s_ * s_; + + let r = 4.0767416621 * l3 - 3.3077115913 * m3 + 0.2309699292 * s3; + let g = -1.2684380046 * l3 + 2.6097574011 * m3 - 0.3413193965 * s3; + let b2 = -0.0041960863 * l3 - 0.7034186147 * m3 + 1.7076147010 * s3; + + // Apply gamma correction + r = r > 0.0031308 ? 1.055 * Math.pow(r, 1.0 / 2.4) - 0.055 : 12.92 * r; + g = g > 0.0031308 ? 1.055 * Math.pow(g, 1.0 / 2.4) - 0.055 : 12.92 * g; + b2 = b2 > 0.0031308 ? 1.055 * Math.pow(b2, 1.0 / 2.4) - 0.055 : 12.92 * b2; + + return [ + Math.round(Math.max(0, Math.min(1, r)) * 255), + Math.round(Math.max(0, Math.min(1, g)) * 255), + Math.round(Math.max(0, Math.min(1, b2)) * 255) + ]; + } + + /** + * Convert LAB to LCH color space (cylindrical representation) + * @param {number} l - L value + * @param {number} a - a value + * @param {number} b - b value + * @returns {number[]} LCH values [L, C, H] + * @private + */ + _labToLch(l, a, b) { + const c = Math.sqrt(a * a + b * b); + let h = Math.atan2(b, a) * 180 / Math.PI; + if (h < 0) h += 360; + + return [l, c, h]; + } + + /** + * Convert LCH to LAB color space + * @param {number} l - L value + * @param {number} c - C (chroma) value + * @param {number} h - H (hue) value (0-360) + * @returns {number[]} LAB values [L, a, b] + * @private + */ + _lchToLab(l, c, h) { + const hRad = h * Math.PI / 180; + const a = c * Math.cos(hRad); + const b = c * Math.sin(hRad); + + return [l, a, b]; + } + + /** + * Find closest color in palette using specified algorithm + * @param {number} r - Red value + * @param {number} g - Green value + * @param {number} b - Blue value + * @param {Array} palette - Array of RGB arrays [[r,g,b], ...] + * @param {string} algorithm - 'legacy' or 'lab' + * @param {Object} options - Additional options for color matching + * @returns {number[]} Closest RGB color [r, g, b] + */ + findClosestPaletteColor(r, g, b, palette, algorithm = 'lab', options = {}) { + if (!palette || palette.length === 0) { + return [0, 0, 0]; + } + + const { enableChromaPenalty = false, chromaPenaltyWeight = 0.15 } = options; + + if (algorithm === 'legacy') { + let menorDist = Infinity; + let cor = [0, 0, 0]; + for (let i = 0; i < palette.length; i++) { + const [pr, pg, pb] = palette[i]; + const rmean = (pr + r) / 2; + const rdiff = pr - r; + const gdiff = pg - g; + const bdiff = pb - b; + const dist = Math.sqrt( + (((512 + rmean) * rdiff * rdiff) >> 8) + + 4 * gdiff * gdiff + + (((767 - rmean) * bdiff * bdiff) >> 8) + ); + if (dist < menorDist) { + menorDist = dist; + cor = [pr, pg, pb]; + } + } + return cor; + } + + // HSV algorithm + if (algorithm === 'hsv') { + const [ht, st, vt] = this._rgbToHsv(r, g, b); + let best = null; + let bestDist = Infinity; + + for (let i = 0; i < palette.length; i++) { + const [pr, pg, pb] = palette[i]; + const [hp, sp, vp] = this._rgbToHsv(pr, pg, pb); + const dh = Math.min(Math.abs(ht - hp), 360 - Math.abs(ht - hp)) / 360; // Normalize hue distance + const ds = st - sp; + const dv = vt - vp; + const dist = dh * dh + ds * ds + dv * dv; + + if (dist < bestDist) { + bestDist = dist; + best = palette[i]; + if (bestDist === 0) break; + } + } + return best || [0, 0, 0]; + } + + // HSL algorithm + if (algorithm === 'hsl') { + const [ht, st, lt] = this._rgbToHsl(r, g, b); + let best = null; + let bestDist = Infinity; + + for (let i = 0; i < palette.length; i++) { + const [pr, pg, pb] = palette[i]; + const [hp, sp, lp] = this._rgbToHsl(pr, pg, pb); + const dh = Math.min(Math.abs(ht - hp), 360 - Math.abs(ht - hp)) / 360; // Normalize hue distance + const ds = st - sp; + const dl = lt - lp; + const dist = dh * dh + ds * ds + dl * dl; + + if (dist < bestDist) { + bestDist = dist; + best = palette[i]; + if (bestDist === 0) break; + } + } + return best || [0, 0, 0]; + } + + // XYZ algorithm + if (algorithm === 'xyz') { + const [xt, yt, zt] = this._rgbToXyz(r, g, b); + let best = null; + let bestDist = Infinity; + + for (let i = 0; i < palette.length; i++) { + const [pr, pg, pb] = palette[i]; + const [xp, yp, zp] = this._rgbToXyz(pr, pg, pb); + const dx = xt - xp; + const dy = yt - yp; + const dz = zt - zp; + const dist = dx * dx + dy * dy + dz * dz; + + if (dist < bestDist) { + bestDist = dist; + best = palette[i]; + if (bestDist === 0) break; + } + } + return best || [0, 0, 0]; + } + + // LUV algorithm + if (algorithm === 'luv') { + const [lt, ut, vt] = this._rgbToLuv(r, g, b); + let best = null; + let bestDist = Infinity; + + for (let i = 0; i < palette.length; i++) { + const [pr, pg, pb] = palette[i]; + const [lp, up, vp] = this._rgbToLuv(pr, pg, pb); + const dl = lt - lp; + const du = ut - up; + const dv = vt - vp; + const dist = dl * dl + du * du + dv * dv; + + if (dist < bestDist) { + bestDist = dist; + best = palette[i]; + if (bestDist === 0) break; + } + } + return best || [0, 0, 0]; + } + + // YUV algorithm + if (algorithm === 'yuv') { + const [yt, ut, vt] = this._rgbToYuv(r, g, b); + let best = null; + let bestDist = Infinity; + + for (let i = 0; i < palette.length; i++) { + const [pr, pg, pb] = palette[i]; + const [yp, up, vp] = this._rgbToYuv(pr, pg, pb); + const dy = yt - yp; + const du = ut - up; + const dv = vt - vp; + const dist = dy * dy + du * du + dv * dv; + + if (dist < bestDist) { + bestDist = dist; + best = palette[i]; + if (bestDist === 0) break; + } + } + return best || [0, 0, 0]; + } + + // Oklab algorithm + if (algorithm === 'oklab') { + const [lt, at, bt] = this._rgbToOklab(r, g, b); + let best = null; + let bestDist = Infinity; + + for (let i = 0; i < palette.length; i++) { + const [pr, pg, pb] = palette[i]; + const [lp, ap, bp] = this._rgbToOklab(pr, pg, pb); + const dl = lt - lp; + const da = at - ap; + const db = bt - bp; + const dist = dl * dl + da * da + db * db; + + if (dist < bestDist) { + bestDist = dist; + best = palette[i]; + if (bestDist === 0) break; + } + } + return best || [0, 0, 0]; + } + + // LCH algorithm + if (algorithm === 'lch') { + const [Lt, at, bt] = this._getLab(r, g, b); + const [lt, ct, ht] = this._labToLch(Lt, at, bt); + let best = null; + let bestDist = Infinity; + + for (let i = 0; i < palette.length; i++) { + const [pr, pg, pb] = palette[i]; + const [Lp, ap, bp] = this._getLab(pr, pg, pb); + const [lp, cp, hp] = this._labToLch(Lp, ap, bp); + const dl = lt - lp; + const dc = ct - cp; + const dh = Math.min(Math.abs(ht - hp), 360 - Math.abs(ht - hp)); // Circular hue distance + const dist = dl * dl + dc * dc + (dh * dh) / 360; // Normalize hue + + if (dist < bestDist) { + bestDist = dist; + best = palette[i]; + if (bestDist === 0) break; + } + } + return best || [0, 0, 0]; + } + + // LAB algorithm (default) + const [Lt, at, bt] = this._getLab(r, g, b); + const targetChroma = Math.sqrt(at * at + bt * bt); + let best = null; + let bestDist = Infinity; + + for (let i = 0; i < palette.length; i++) { + const [pr, pg, pb] = palette[i]; + const [Lp, ap, bp] = this._getLab(pr, pg, pb); + const dL = Lt - Lp; + const da = at - ap; + const db = bt - bp; + let dist = dL * dL + da * da + db * db; + + if (enableChromaPenalty && targetChroma > 20) { + const candChroma = Math.sqrt(ap * ap + bp * bp); + if (candChroma < targetChroma) { + const chromaDiff = targetChroma - candChroma; + dist += chromaDiff * chromaDiff * chromaPenaltyWeight; + } + } + + if (dist < bestDist) { + bestDist = dist; + best = palette[i]; + if (bestDist === 0) break; + } + } + + return best || [0, 0, 0]; + } + + /** + * Check if a pixel is considered white based on threshold + * @param {number} r - Red value + * @param {number} g - Green value + * @param {number} b - Blue value + * @param {number} threshold - White threshold (default: 230) + * @returns {boolean} True if pixel is white + */ + isWhitePixel(r, g, b, threshold = this.WHITE_THRESHOLD) { + return r >= threshold && g >= threshold && b >= threshold; + } + + /** + * Resolve target RGB to closest available color with caching + * @param {number[]} targetRgb - Target RGB array [r, g, b] + * @param {Array} availableColors - Available colors with id and rgb properties + * @param {Object} options - Matching options + * @returns {Object} {id: number|null, rgb: number[]} + */ + resolveColor(targetRgb, availableColors, options = {}) { + const { + exactMatch = false, + algorithm = 'lab', + enableChromaPenalty = false, + chromaPenaltyWeight = 0.15, + whiteThreshold = this.WHITE_THRESHOLD + } = options; + + if (!availableColors || availableColors.length === 0) { + return { id: null, rgb: targetRgb }; + } + + const cacheKey = `${targetRgb[0]},${targetRgb[1]},${targetRgb[2]}|${algorithm}|${enableChromaPenalty ? 'c' : 'nc'}|${chromaPenaltyWeight}|${exactMatch ? 'exact' : 'closest'}`; + + if (this._colorCache.has(cacheKey)) { + return this._colorCache.get(cacheKey); + } + + // Check for exact match + if (exactMatch) { + const match = availableColors.find( + (c) => c.rgb[0] === targetRgb[0] && c.rgb[1] === targetRgb[1] && c.rgb[2] === targetRgb[2] + ); + const result = match ? { id: match.id, rgb: [...match.rgb] } : { id: null, rgb: targetRgb }; + this._colorCache.set(cacheKey, result); + return result; + } + + // Check for white pixel matching + if ( + targetRgb[0] >= whiteThreshold && + targetRgb[1] >= whiteThreshold && + targetRgb[2] >= whiteThreshold + ) { + const whiteEntry = availableColors.find( + (c) => c.rgb[0] >= whiteThreshold && c.rgb[1] >= whiteThreshold && c.rgb[2] >= whiteThreshold + ); + if (whiteEntry) { + const result = { id: whiteEntry.id, rgb: [...whiteEntry.rgb] }; + this._colorCache.set(cacheKey, result); + return result; + } + } + + // Find nearest color + let bestId = availableColors[0].id; + let bestRgb = [...availableColors[0].rgb]; + let bestScore = Infinity; + + if (algorithm === 'legacy') { + for (let i = 0; i < availableColors.length; i++) { + const c = availableColors[i]; + const [r, g, b] = c.rgb; + const rmean = (r + targetRgb[0]) / 2; + const rdiff = r - targetRgb[0]; + const gdiff = g - targetRgb[1]; + const bdiff = b - targetRgb[2]; + const dist = Math.sqrt( + (((512 + rmean) * rdiff * rdiff) >> 8) + + 4 * gdiff * gdiff + + (((767 - rmean) * bdiff * bdiff) >> 8) + ); + if (dist < bestScore) { + bestScore = dist; + bestId = c.id; + bestRgb = [...c.rgb]; + if (dist === 0) break; + } + } + } else { + const [Lt, at, bt] = this._getLab(targetRgb[0], targetRgb[1], targetRgb[2]); + const targetChroma = Math.sqrt(at * at + bt * bt); + const penaltyWeight = enableChromaPenalty ? chromaPenaltyWeight : 0; + + for (let i = 0; i < availableColors.length; i++) { + const c = availableColors[i]; + const [r, g, b] = c.rgb; + const [L2, a2, b2] = this._getLab(r, g, b); + const dL = Lt - L2; + const da = at - a2; + const db = bt - b2; + let dist = dL * dL + da * da + db * db; + + if (penaltyWeight > 0 && targetChroma > 20) { + const candChroma = Math.sqrt(a2 * a2 + b2 * b2); + if (candChroma < targetChroma) { + const cd = targetChroma - candChroma; + dist += cd * cd * penaltyWeight; + } + } + + if (dist < bestScore) { + bestScore = dist; + bestId = c.id; + bestRgb = [...c.rgb]; + if (dist === 0) break; + } + } + } + + const result = { id: bestId, rgb: bestRgb }; + this._colorCache.set(cacheKey, result); + + // Limit cache size + if (this._colorCache.size > this.COLOR_CACHE_LIMIT) { + const firstKey = this._colorCache.keys().next().value; + this._colorCache.delete(firstKey); + } + + return result; + } + + // ============================================= + // DITHERING METHODS (Floyd-Steinberg) + // ============================================= + + /** + * Ensure dithering buffers are properly sized + * @param {number} n - Number of pixels + * @returns {Object} {work: Float32Array, eligible: Uint8Array} + * @private + */ + _ensureDitherBuffers(n) { + if (!this._ditherWorkBuf || this._ditherWorkBuf.length !== n * 3) { + this._ditherWorkBuf = new Float32Array(n * 3); + } + if (!this._ditherEligibleBuf || this._ditherEligibleBuf.length !== n) { + this._ditherEligibleBuf = new Uint8Array(n); + } + return { work: this._ditherWorkBuf, eligible: this._ditherEligibleBuf }; + } + + /** + * Apply Floyd-Steinberg dithering to image data + * @param {Uint8ClampedArray} imageData - RGBA image data + * @param {number} width - Image width + * @param {number} height - Image height + * @param {Array} palette - Color palette for dithering + * @param {Object} options - Dithering options + * @returns {Object} {data: Uint8ClampedArray, totalValidPixels: number} + */ + applyFloydSteinbergDithering(imageData, width, height, palette, options = {}) { + const { + paintTransparentPixels = false, + paintWhitePixels = true, + transparencyThreshold = this.TRANSPARENCY_THRESHOLD, + whiteThreshold = this.WHITE_THRESHOLD, + algorithm = 'lab', + enableChromaPenalty = false, + chromaPenaltyWeight = 0.15, + mask = null + } = options; + + const data = new Uint8ClampedArray(imageData); + const n = width * height; + const { work, eligible } = this._ensureDitherBuffers(n); + let totalValidPixels = 0; + + // Initialize working buffers + for (let y = 0; y < height; y++) { + for (let x = 0; x < width; x++) { + const idx = y * width + x; + const i4 = idx * 4; + const r = data[i4]; + const g = data[i4 + 1]; + const b = data[i4 + 2]; + const a = data[i4 + 3]; + const masked = mask && mask[idx]; + + const isEligible = + !masked && + (paintTransparentPixels || a >= transparencyThreshold) && + (paintWhitePixels || !this.isWhitePixel(r, g, b, whiteThreshold)); + + 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; // Make ineligible pixels transparent + } + } + } + + // Error diffusion function + const diffuse = (nx, ny, er, eg, eb, factor) => { + if (nx < 0 || nx >= width || ny < 0 || ny >= height) return; + const nidx = ny * width + 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)); + }; + + // Apply Floyd-Steinberg dithering + for (let y = 0; y < height; y++) { + for (let x = 0; x < width; x++) { + const idx = y * width + x; + if (!eligible[idx]) continue; + + const base = idx * 3; + const r0 = work[base]; + const g0 = work[base + 1]; + const b0 = work[base + 2]; + + const [nr, ng, nb] = this.findClosestPaletteColor( + r0, g0, b0, palette, algorithm, { enableChromaPenalty, chromaPenaltyWeight } + ); + + const i4 = idx * 4; + data[i4] = nr; + data[i4 + 1] = ng; + data[i4 + 2] = nb; + data[i4 + 3] = 255; + totalValidPixels++; + + // Calculate and diffuse error + 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); + } + } + + return { data, totalValidPixels }; + } + + /** + * Apply simple color quantization without dithering + * @param {Uint8ClampedArray} imageData - RGBA image data + * @param {number} width - Image width + * @param {number} height - Image height + * @param {Array} palette - Color palette for quantization + * @param {Object} options - Quantization options + * @returns {Object} {data: Uint8ClampedArray, totalValidPixels: number} + */ + applySimpleQuantization(imageData, width, height, palette, options = {}) { + const { + paintTransparentPixels = false, + paintWhitePixels = true, + transparencyThreshold = this.TRANSPARENCY_THRESHOLD, + whiteThreshold = this.WHITE_THRESHOLD, + algorithm = 'lab', + enableChromaPenalty = false, + chromaPenaltyWeight = 0.15 + } = options; + + const data = new Uint8ClampedArray(imageData); + let totalValidPixels = 0; + + for (let i = 0; i < data.length; i += 4) { + const r = data[i]; + const g = data[i + 1]; + const b = data[i + 2]; + const a = data[i + 3]; + + const isEligible = + (paintTransparentPixels || a >= transparencyThreshold) && + (paintWhitePixels || !this.isWhitePixel(r, g, b, whiteThreshold)); + + if (!isEligible) { + data[i + 3] = 0; // Make ineligible pixels transparent + continue; + } + + const [nr, ng, nb] = this.findClosestPaletteColor( + r, g, b, palette, algorithm, { enableChromaPenalty, chromaPenaltyWeight } + ); + + data[i] = nr; + data[i + 1] = ng; + data[i + 2] = nb; + data[i + 3] = 255; + totalValidPixels++; + } + + return { data, totalValidPixels }; + } + + // ============================================= + // UTILITY METHODS + // ============================================= + + /** + * Extract available colors from DOM elements + * @returns {Array|null} Array of color objects with id and rgb properties + */ + extractAvailableColors() { + const colorElements = document.querySelectorAll('.color-option, [data-color-id]'); + if (colorElements.length === 0) return null; + + const colors = []; + colorElements.forEach((element) => { + const colorId = element.dataset.colorId || element.getAttribute('data-color-id'); + const computedStyle = window.getComputedStyle(element); + const backgroundColor = computedStyle.backgroundColor; + + if (backgroundColor && colorId) { + const rgbMatch = backgroundColor.match(/rgb\((\d+),\s*(\d+),\s*(\d+)\)/); + if (rgbMatch) { + colors.push({ + id: parseInt(colorId), + rgb: [parseInt(rgbMatch[1]), parseInt(rgbMatch[2]), parseInt(rgbMatch[3])] + }); + } + } + }); + + return colors.length > 0 ? colors : null; + } + + /** + * Create file uploader for images + * @returns {Promise} Promise resolving to image data URL + */ + createImageUploader() { + return new Promise((resolve) => { + const input = document.createElement('input'); + input.type = 'file'; + input.accept = 'image/png,image/jpeg'; + input.onchange = () => { + if (input.files && input.files[0]) { + const fr = new FileReader(); + fr.onload = () => resolve(fr.result); + fr.readAsDataURL(input.files[0]); + } else { + resolve(null); + } + }; + input.click(); + }); + } + + /** + * Extract available colors from DOM palette + * @param {Object} colorMap - The color map object (optional, will use window.CONFIG if not provided) + * @returns {Array|null} Array of available colors or null if none found + */ + extractAvailableColors(colorMap = null) { + const colorElements = document.querySelectorAll('.tooltip button[id^="color-"]'); + if (colorElements.length === 0) { + console.log('āŒ No color elements found on page'); + return null; + } + + // Use provided colorMap or fallback to window.CONFIG + const effectiveColorMap = colorMap || (window.CONFIG?.COLOR_MAP); + + // Separate available and unavailable colors + const availableColors = []; + const unavailableColors = []; + + Array.from(colorElements).forEach((el) => { + const id = Number.parseInt(el.id.replace('color-', '')); + if (id === 0) return; // Skip transparent color + + const rgbStr = el.style.backgroundColor.match(/\d+/g); + if (!rgbStr || rgbStr.length < 3) { + console.warn(`Skipping color element ${el.id} — cannot parse RGB`); + return; + } + const rgb = rgbStr.map(Number); + + // Find color name from COLOR_MAP + let name = `Unknown Color ${id}`; + if (effectiveColorMap) { + const colorInfo = Object.values(effectiveColorMap).find((color) => color.id === id); + name = colorInfo ? colorInfo.name : name; + } + + const colorData = { id, name, rgb }; + + // Check if color is available (no SVG overlay means available) + if (!el.querySelector('svg')) { + availableColors.push(colorData); + } else { + unavailableColors.push(colorData); + } + }); + + // Console log detailed color information + console.log('=== CAPTURED COLORS STATUS ==='); + console.log(`Total available colors: ${availableColors.length}`); + console.log(`Total unavailable colors: ${unavailableColors.length}`); + console.log(`Total colors scanned: ${availableColors.length + unavailableColors.length}`); + + if (availableColors.length > 0) { + console.log('\n--- AVAILABLE COLORS ---'); + availableColors.forEach((color, index) => { + console.log( + `${index + 1 + }. ID: ${color.id}, Name: "${color.name}", RGB: (${color.rgb[0]}, ${color.rgb[1]}, ${color.rgb[2]})` + ); + }); + } + + if (unavailableColors.length > 0) { + console.log('\n--- UNAVAILABLE COLORS ---'); + unavailableColors.forEach((color, index) => { + console.log( + `${index + 1 + }. ID: ${color.id}, Name: "${color.name}", RGB: (${color.rgb[0]}, ${color.rgb[1]}, ${color.rgb[2]}) [LOCKED]` + ); + }); + } + + console.log('=== END COLOR STATUS ==='); + + return availableColors; + } + + /** + * Clear all caches to free memory + */ + clearCaches() { + this._colorCache.clear(); + this._labCache.clear(); + this._hsvCache?.clear(); + this._hslCache?.clear(); + this._xyzCache?.clear(); + this._luvCache?.clear(); + this._yuvCache?.clear(); + this._oklabCache?.clear(); + this._lchCache?.clear(); + this._ditherWorkBuf = null; + this._ditherEligibleBuf = null; + } + + /** + * Clean up resources + */ + cleanup() { + this.clearCaches(); + if (this.canvas) { + this.canvas.width = 0; + this.canvas.height = 0; + } + this.img = null; + this.canvas = null; + this.ctx = null; + } +} + +// Create global instance +window.WPlaceImageProcessor = ImageProcessor; + +// Create global instance for Auto-Image.js compatibility +window.globalImageProcessor = new ImageProcessor(); + +// Legacy compatibility - expose key methods globally for backward compatibility +window.ImageProcessor = ImageProcessor; + +console.log('āœ… WPlace Image Processor loaded and ready'); diff --git a/Extensions/scripts/overlay-manager.js b/Extensions/scripts/overlay-manager.js new file mode 100644 index 0000000..b3290ef --- /dev/null +++ b/Extensions/scripts/overlay-manager.js @@ -0,0 +1,837 @@ +// ==UserScript== +// @name WPlace Overlay Manager +// @namespace http://tampermonkey.net/ +// @version 2025-09-16.1 +// @description Overlay management for WPlace AutoBot - handles tile processing, chunking, and rendering +// @author Wbot +// @match https://wplace.live/* +// @grant none +// ==/UserScript== + +/** + * OverlayManager - Handles overlay processing, tile chunking, and canvas operations for WPlace AutoBot + * Extracted from Auto-Image.js for better modularity and reusabilityc + */ +class OverlayManager { + constructor() { + this.isEnabled = false; + this.startCoords = null; // { region: {x, y}, pixel: {x, y} } + this.imageBitmap = null; + this.chunkedTiles = new Map(); // Map<"tileX,tileY", ImageBitmap> + this.originalTiles = new Map(); // Map<"tileX,ttileY", ImageBitmap> store latest original tile bitmaps + this.originalTilesData = new Map(); // Map<"tileX,tileY", {w,h,data:Uint8ClampedArray}> cache full ImageData for fast pixel reads + this.tileSize = 1000; + this.processPromise = null; // Track ongoing processing + this.lastProcessedHash = null; // Cache invalidation + this.workerPool = null; // Web worker pool for heavy processing + this.neededTilesForSave = null; // Set of tiles needed for save file restoration + } + + /** + * Toggle overlay enabled state + * @returns {boolean} New enabled state + */ + toggle() { + this.isEnabled = !this.isEnabled; + console.log(`Overlay ${this.isEnabled ? 'enabled' : 'disabled'}.`); + return this.isEnabled; + } + + /** + * Enable overlay + */ + enable() { + this.isEnabled = true; + + // Force canvas refresh if script was loaded manually (not on startup) + // This ensures overlay works even when tiles are already cached + this._triggerCanvasRefreshIfNeeded(); + + // Also try direct canvas overlay approach as backup + this._attemptDirectCanvasOverlay(); + } + + /** + * Attempt to directly overlay on the existing canvas + * @private + */ + _attemptDirectCanvasOverlay() { + // Only try this if we have overlay data and it's been more than 5 seconds since page load + const timeSinceLoad = Date.now() - (window.performance?.timing?.navigationStart || 0); + + if (timeSinceLoad > 5000 && this.chunkedTiles.size > 0) { + setTimeout(() => { + this._drawDirectOverlay(); + }, 500); + } + } + + /** + * Draw overlay directly on the main canvas + * @private + */ + _drawDirectOverlay() { + try { + const canvas = document.querySelector('canvas'); + if (!canvas) { + return; + } + + const ctx = canvas.getContext('2d'); + if (!ctx) { + return; + } + + // Store original composite operation + const originalComposite = ctx.globalCompositeOperation; + const originalAlpha = ctx.globalAlpha; + + // Set overlay blending + ctx.globalCompositeOperation = 'source-over'; + ctx.globalAlpha = window.state?.overlayOpacity || 0.6; + + // Draw overlay chunks directly on canvas + this.chunkedTiles.forEach((bitmap, tileKey) => { + const [tileX, tileY] = tileKey.split(',').map(Number); + + // Convert tile coordinates to canvas coordinates + // This assumes WPlace uses 1000px tiles + const canvasX = tileX * 1000; + const canvasY = tileY * 1000; + + try { + ctx.drawImage(bitmap, canvasX, canvasY); + } catch (error) { + // Ignore individual tile draw errors + } + }); + + // Restore original settings + ctx.globalCompositeOperation = originalComposite; + ctx.globalAlpha = originalAlpha; + + } catch (error) { + // Ignore direct overlay errors silently + } + } + + /** + * Trigger canvas refresh for manual script execution + * @private + */ + _triggerCanvasRefreshIfNeeded() { + // Only trigger refresh if we have overlay data and it's been more than 5 seconds since page load + // (indicates manual execution rather than startup execution) + const timeSinceLoad = Date.now() - (window.performance?.timing?.navigationStart || 0); + + if (timeSinceLoad > 5000 && this.chunkedTiles.size > 0) { + // Try multiple methods to refresh the canvas tiles + setTimeout(() => { + this._attemptCanvasRefresh(); + }, 100); + } + } + + /** + * Attempt to refresh canvas tiles using various methods + * @private + */ + _attemptCanvasRefresh() { + try { + // Method 1: Clear browser cache for tile URLs + if ('caches' in window) { + caches.keys().then(cacheNames => { + cacheNames.forEach(cacheName => { + caches.delete(cacheName); + }); + }); + } + + // Method 2: Force browser to bypass cache by adding cache-busting parameter + // We need to intercept and modify tile requests to add cache-busting + const originalFetch = window.fetch; + let fetchOverrideActive = true; + + window.fetch = async function(...args) { + const url = args[0] instanceof Request ? args[0].url : args[0]; + + if (fetchOverrideActive && typeof url === 'string' && url.match(/\/\d+\/\d+\.png/)) { + // Add cache-busting parameter + const separator = url.includes('?') ? '&' : '?'; + const cacheBustUrl = `${url}${separator}_cb=${Date.now()}`; + + if (args[0] instanceof Request) { + args[0] = new Request(cacheBustUrl, args[0]); + } else { + args[0] = cacheBustUrl; + } + } + + return originalFetch.apply(this, args); + }; + + // Disable cache-busting after 10 seconds + setTimeout(() => { + fetchOverrideActive = false; + }, 10000); + + // Method 3: Try to trigger a slight zoom change + const canvas = document.querySelector('canvas'); + if (canvas) { + // Dispatch a wheel event to trigger tile refresh + const wheelEvent = new WheelEvent('wheel', { + deltaY: 1, + deltaMode: 0, + bubbles: true, + cancelable: true, + view: window + }); + + canvas.dispatchEvent(wheelEvent); + + // Immediately counter it + setTimeout(() => { + const counterEvent = new WheelEvent('wheel', { + deltaY: -1, + deltaMode: 0, + bubbles: true, + cancelable: true, + view: window + }); + canvas.dispatchEvent(counterEvent); + }, 50); + } + + // Method 4: Try to trigger resize event + window.dispatchEvent(new Event('resize')); + + } catch (error) { + console.warn('āš ļø Could not trigger canvas refresh:', error); + } + } + + /** + * Disable overlay + */ + disable() { + this.isEnabled = false; + } + + /** + * Clear all overlay data and disable + */ + clear() { + this.disable(); + this.imageBitmap = null; + this.chunkedTiles.clear(); + this.originalTiles.clear(); + this.originalTilesData.clear(); + this.lastProcessedHash = null; + if (this.processPromise) { + this.processPromise = null; + } + } + + /** + * Set the image bitmap for overlay processing + * @param {ImageBitmap} imageBitmap - The image to use for overlay + */ + async setImage(imageBitmap) { + this.imageBitmap = imageBitmap; + this.lastProcessedHash = null; // Invalidate cache + if (this.imageBitmap && this.startCoords) { + await this.processImageIntoChunks(); + } + } + + /** + * Set the position for overlay rendering + * @param {Object} startPosition - Starting pixel position {x, y} + * @param {Object} region - Region coordinates {x, y} + */ + async setPosition(startPosition, region) { + if (!startPosition || !region) { + this.startCoords = null; + this.chunkedTiles.clear(); + this.lastProcessedHash = null; + return; + } + this.startCoords = { region, pixel: startPosition }; + this.lastProcessedHash = null; // Invalidate cache + if (this.imageBitmap) { + await this.processImageIntoChunks(); + } + } + + /** + * Generate hash for cache invalidation + * @returns {string|null} Hash string or null if no data + * @private + */ + _generateProcessHash() { + if (!this.imageBitmap || !this.startCoords) return null; + const { width, height } = this.imageBitmap; + const { x: px, y: py } = this.startCoords.pixel; + const { x: rx, y: ry } = this.startCoords.region; + + // Access global state for overlay settings + const blueMarbleEnabled = window.state?.blueMarbleEnabled || false; + const overlayOpacity = window.state?.overlayOpacity || 0.6; + + return `${width}x${height}_${px},${py}_${rx},${ry}_${blueMarbleEnabled}_${overlayOpacity}`; + } + + /** + * Process image into chunks with caching and batch processing + */ + async processImageIntoChunks() { + if (!this.imageBitmap || !this.startCoords) { + console.warn('🚫 Cannot process chunks - missing imageBitmap or startCoords'); + return; + } + + console.log('šŸ”„ Processing image into chunks...', { + imageSize: `${this.imageBitmap.width}x${this.imageBitmap.height}`, + startCoords: this.startCoords + }); + + // Check if we're already processing to avoid duplicate work + if (this.processPromise) { + console.log('ā³ Already processing chunks - waiting...'); + return this.processPromise; + } + + // Check cache validity + const currentHash = this._generateProcessHash(); + if (this.lastProcessedHash === currentHash && this.chunkedTiles.size > 0) { + // Using cached overlay chunks + return; + } + + // Start processing + console.log('šŸš€ Starting new chunk processing...'); + this.processPromise = this._doProcessImageIntoChunks(); + try { + await this.processPromise; + this.lastProcessedHash = currentHash; + console.log(`āœ… Chunk processing complete - ${this.chunkedTiles.size} tiles created`); + } finally { + this.processPromise = null; + } + } + + /** + * Internal method to process image into chunks + * @private + */ + async _doProcessImageIntoChunks() { + const startTime = performance.now(); + this.chunkedTiles.clear(); + + const { width: imageWidth, height: imageHeight } = this.imageBitmap; + const { x: startPixelX, y: startPixelY } = this.startCoords.pixel; + const { x: startRegionX, y: startRegionY } = this.startCoords.region; + + const { startTileX, startTileY, endTileX, endTileY } = this.calculateTileRange( + startRegionX, + startRegionY, + startPixelX, + startPixelY, + imageWidth, + imageHeight, + this.tileSize + ); + + const totalTiles = (endTileX - startTileX + 1) * (endTileY - startTileY + 1); + console.log(`šŸ”„ Processing ${totalTiles} overlay tiles...`); + + // Process tiles in batches to avoid blocking the main thread + const batchSize = 4; // Process 4 tiles at a time + const tilesToProcess = []; + + for (let ty = startTileY; ty <= endTileY; ty++) { + for (let tx = startTileX; tx <= endTileX; tx++) { + tilesToProcess.push({ tx, ty }); + } + } + + // Process tiles in batches with yielding + for (let i = 0; i < tilesToProcess.length; i += batchSize) { + const batch = tilesToProcess.slice(i, i + batchSize); + + await Promise.all( + batch.map(async ({ tx, ty }) => { + const tileKey = `${tx},${ty}`; + const chunkBitmap = await this._processTile( + tx, + ty, + imageWidth, + imageHeight, + startPixelX, + startPixelY, + startRegionX, + startRegionY + ); + if (chunkBitmap) { + this.chunkedTiles.set(tileKey, chunkBitmap); + } + }) + ); + + // Yield control to prevent blocking + if (i + batchSize < tilesToProcess.length) { + await new Promise((resolve) => setTimeout(resolve, 0)); + } + } + + const processingTime = performance.now() - startTime; + console.log( + `āœ… Overlay processed ${this.chunkedTiles.size} tiles in ${Math.round(processingTime)}ms` + ); + } + + /** + * Process a single tile + * @param {number} tx - Tile X coordinate + * @param {number} ty - Tile Y coordinate + * @param {number} imageWidth - Image width + * @param {number} imageHeight - Image height + * @param {number} startPixelX - Starting pixel X + * @param {number} startPixelY - Starting pixel Y + * @param {number} startRegionX - Starting region X + * @param {number} startRegionY - Starting region Y + * @returns {Promise} Processed tile bitmap + * @private + */ + async _processTile( + tx, + ty, + imageWidth, + imageHeight, + startPixelX, + startPixelY, + startRegionX, + startRegionY + ) { + const tileKey = `${tx},${ty}`; + + // Calculate the portion of the image that overlaps with this tile + const imgStartX = (tx - startRegionX) * this.tileSize - startPixelX; + const imgStartY = (ty - startRegionY) * this.tileSize - startPixelY; + + // Crop coordinates within the source image + const sX = Math.max(0, imgStartX); + const sY = Math.max(0, imgStartY); + const sW = Math.min(imageWidth - sX, this.tileSize - (sX - imgStartX)); + const sH = Math.min(imageHeight - sY, this.tileSize - (sY - imgStartY)); + + if (sW <= 0 || sH <= 0) return null; + + // Destination coordinates on the new chunk canvas + const dX = Math.max(0, -imgStartX); + const dY = Math.max(0, -imgStartY); + + const chunkCanvas = new OffscreenCanvas(this.tileSize, this.tileSize); + const chunkCtx = chunkCanvas.getContext('2d'); + chunkCtx.imageSmoothingEnabled = false; + + chunkCtx.drawImage(this.imageBitmap, sX, sY, sW, sH, dX, dY, sW, sH); + + // Blue marble effect with faster pixel manipulation + const blueMarbleEnabled = window.state?.blueMarbleEnabled || false; + if (blueMarbleEnabled) { + const imageData = chunkCtx.getImageData(dX, dY, sW, sH); + const data = imageData.data; + + // Faster pixel manipulation using typed arrays + for (let i = 0; i < data.length; i += 4) { + const pixelIndex = i / 4; + const pixelY = Math.floor(pixelIndex / sW); + const pixelX = pixelIndex % sW; + + if ((pixelX + pixelY) % 2 === 0 && data[i + 3] > 0) { + data[i + 3] = 0; // Set alpha to 0 + } + } + + chunkCtx.putImageData(imageData, dX, dY); + } + + return await chunkCanvas.transferToImageBitmap(); + } + + /** + * Process and respond to tile requests with overlay compositing + * @param {Object} eventData - Event data containing endpoint, blobID, and blobData + */ + async processAndRespondToTileRequest(eventData) { + const { endpoint, blobID, blobData } = eventData; + + let finalBlob = blobData; + + if (this.isEnabled && this.chunkedTiles.size > 0) { + const tileMatch = endpoint.match(/(\d+)\/(\d+)\.png/); + if (tileMatch) { + const tileX = parseInt(tileMatch[1], 10); + const tileY = parseInt(tileMatch[2], 10); + const tileKey = `${tileX},${tileY}`; + + const chunkBitmap = this.chunkedTiles.get(tileKey); + // Also store the original tile bitmap for later pixel color checks + try { + const originalBitmap = await createImageBitmap(blobData); + this.originalTiles.set(tileKey, originalBitmap); + // Cache full ImageData for fast pixel access (avoid repeated drawImage/getImageData) + try { + let canvas, ctx; + if (typeof OffscreenCanvas !== 'undefined') { + canvas = new OffscreenCanvas(originalBitmap.width, originalBitmap.height); + ctx = canvas.getContext('2d'); + } else { + canvas = document.createElement('canvas'); + canvas.width = originalBitmap.width; + canvas.height = originalBitmap.height; + ctx = canvas.getContext('2d'); + } + ctx.imageSmoothingEnabled = false; + ctx.drawImage(originalBitmap, 0, 0); + const imgData = ctx.getImageData(0, 0, originalBitmap.width, originalBitmap.height); + // Store typed array copy to avoid retaining large canvas + this.originalTilesData.set(tileKey, { + w: originalBitmap.width, + h: originalBitmap.height, + data: new Uint8ClampedArray(imgData.data), + }); + } catch (e) { + // If ImageData extraction fails, still keep the bitmap as fallback + console.warn('OverlayManager: could not cache ImageData for', tileKey, e); + } + } catch (e) { + console.warn('OverlayManager: could not create original bitmap for', tileKey, e); + } + + if (chunkBitmap) { + try { + // Use faster compositing for better performance + finalBlob = await this._compositeTileOptimized(blobData, chunkBitmap); + } catch (e) { + console.error('Error compositing overlay:', e); + // Fallback to original tile on error + finalBlob = blobData; + } + } + } + } + + // Send the (possibly modified) blob back to the injected script + window.postMessage( + { + source: 'auto-image-overlay', + blobID: blobID, + blobData: finalBlob, + }, + '*' + ); + } + + /** + * Get pixel color from a tile + * @param {number} tileX - Tile X coordinate + * @param {number} tileY - Tile Y coordinate + * @param {number} pixelX - Pixel X within tile + * @param {number} pixelY - Pixel Y within tile + * @returns {Promise} [r,g,b,a] or null + */ + async getTilePixelColor(tileX, tileY, pixelX, pixelY) { + const tileKey = `${tileX},${tileY}`; + + // Get transparency threshold from global state or config + const alphaThresh = window.state?.customTransparencyThreshold || + window.CONFIG?.TRANSPARENCY_THRESHOLD || 10; + + // 1. Prefer cached ImageData if available + const cached = this.originalTilesData.get(tileKey); + if (cached && cached.data && cached.w > 0 && cached.h > 0) { + const x = Math.max(0, Math.min(cached.w - 1, pixelX)); + const y = Math.max(0, Math.min(cached.h - 1, pixelY)); + const idx = (y * cached.w + x) * 4; + const d = cached.data; + const a = d[idx + 3]; + + const paintTransparentPixels = window.state?.paintTransparentPixels || false; + if (!paintTransparentPixels && a < alphaThresh) { + // Treat as transparent / unavailable + return null; + } + return [d[idx], d[idx + 1], d[idx + 2], a]; + } + + // 2. Fallback: use bitmap, with retry + const maxRetries = 3; + for (let attempt = 1; attempt <= maxRetries; attempt++) { + const bitmap = this.originalTiles.get(tileKey); + if (!bitmap) { + if (attempt === maxRetries) { + console.warn('OverlayManager: no bitmap for', tileKey, 'after', maxRetries, 'attempts'); + } else { + await this._sleep(50 * attempt); // exponential delay + } + continue; + } + + try { + let canvas, ctx; + if (typeof OffscreenCanvas !== 'undefined') { + canvas = new OffscreenCanvas(bitmap.width, bitmap.height); + ctx = canvas.getContext('2d'); + } else { + canvas = document.createElement('canvas'); + canvas.width = bitmap.width; + canvas.height = bitmap.height; + ctx = canvas.getContext('2d'); + } + ctx.imageSmoothingEnabled = false; + ctx.drawImage(bitmap, 0, 0); + + const x = Math.max(0, Math.min(bitmap.width - 1, pixelX)); + const y = Math.max(0, Math.min(bitmap.height - 1, pixelY)); + const data = ctx.getImageData(x, y, 1, 1).data; + const a = data[3]; + + const paintTransparentPixels = window.state?.paintTransparentPixels || false; + if (!paintTransparentPixels && a < alphaThresh) { + return null; + } + + return [data[0], data[1], data[2], a]; + } catch (e) { + console.warn('OverlayManager: failed to read pixel (attempt', attempt, ')', tileKey, e); + if (attempt < maxRetries) { + await this._sleep(50 * attempt); + } else { + console.error( + 'OverlayManager: failed to read pixel after', + maxRetries, + 'attempts', + tileKey + ); + } + } + } + + // 3. If everything fails, return null + return null; + } + + /** + * Composite tile with overlay using optimized rendering + * @param {Blob} originalBlob - Original tile blob + * @param {ImageBitmap} overlayBitmap - Overlay bitmap + * @returns {Promise} Composited tile blob + * @private + */ + async _compositeTileOptimized(originalBlob, overlayBitmap) { + const originalBitmap = await createImageBitmap(originalBlob); + const canvas = new OffscreenCanvas(originalBitmap.width, originalBitmap.height); + const ctx = canvas.getContext('2d'); + + // Disable antialiasing for pixel-perfect rendering + ctx.imageSmoothingEnabled = false; + + // Draw original tile first + ctx.drawImage(originalBitmap, 0, 0); + + // Set opacity and draw overlay with optimized blend mode + const overlayOpacity = window.state?.overlayOpacity || 0.6; + ctx.globalAlpha = overlayOpacity; + ctx.globalCompositeOperation = 'source-over'; + ctx.drawImage(overlayBitmap, 0, 0); + + // Use faster blob conversion with compression settings + return await canvas.convertToBlob({ + type: 'image/png', + quality: 0.95, // Slight compression for faster processing + }); + } + + /** + * Wait until all required tiles are loaded and cached + * @param {number} startRegionX - Starting region X + * @param {number} startRegionY - Starting region Y + * @param {number} pixelWidth - Image width in pixels + * @param {number} pixelHeight - Image height in pixels + * @param {number} startPixelX - Starting pixel X offset + * @param {number} startPixelY - Starting pixel Y offset + * @param {number} timeoutMs - Timeout in milliseconds + * @returns {Promise} True if tiles are ready + */ + async waitForTiles( + startRegionX, + startRegionY, + pixelWidth, + pixelHeight, + startPixelX = 0, + startPixelY = 0, + timeoutMs = 10000 + ) { + const { startTileX, startTileY, endTileX, endTileY } = this.calculateTileRange( + startRegionX, + startRegionY, + startPixelX, + startPixelY, + pixelWidth, + pixelHeight, + this.tileSize + ); + + const requiredTiles = []; + for (let ty = startTileY; ty <= endTileY; ty++) { + for (let tx = startTileX; tx <= endTileX; tx++) { + requiredTiles.push(`${tx},${ty}`); + } + } + + if (requiredTiles.length === 0) return true; + + const startTime = Date.now(); + + while (Date.now() - startTime < timeoutMs) { + const stopFlag = window.state?.stopFlag || false; + if (stopFlag) { + console.log('waitForTiles: stopped by user'); + return false; + } + + const missing = requiredTiles.filter((key) => !this.originalTiles.has(key)); + if (missing.length === 0) { + console.log(`āœ… All ${requiredTiles.length} required tiles are loaded`); + return true; + } + + await this._sleep(100); + } + + console.warn(`āŒ Timeout waiting for tiles: ${requiredTiles.length} required, + ${requiredTiles.filter((k) => this.originalTiles.has(k)).length} loaded`); + return false; + } + + /** + * Calculate tile range for given parameters + * @param {number} startRegionX - Starting region X + * @param {number} startRegionY - Starting region Y + * @param {number} startPixelX - Starting pixel X + * @param {number} startPixelY - Starting pixel Y + * @param {number} width - Width in pixels + * @param {number} height - Height in pixels + * @param {number} tileSize - Size of each tile (default 1000) + * @returns {{startTileX: number, startTileY: number, endTileX: number, endTileY: number}} + */ + calculateTileRange( + startRegionX, + startRegionY, + startPixelX, + startPixelY, + width, + height, + tileSize = 1000 + ) { + const endPixelX = startPixelX + width; + const endPixelY = startPixelY + height; + + return { + startTileX: startRegionX + Math.floor(startPixelX / tileSize), + startTileY: startRegionY + Math.floor(startPixelY / tileSize), + endTileX: startRegionX + Math.floor((endPixelX - 1) / tileSize), + endTileY: startRegionY + Math.floor((endPixelY - 1) / tileSize), + }; + } + + /** + * Preload tiles for save file coordinates to populate originalTiles cache + * This fixes the "no bitmap" error when loading save files + * @param {Object} startPosition - The start position from save file + * @param {Object} region - The region from save file + * @param {number} width - Image width + * @param {number} height - Image height + */ + /** + * Preload tiles for save file coordinates to populate originalTiles cache + * This fixes the "no bitmap" error when loading save files + * For WPlace, we can't manually fetch tiles like r/place - we need to trigger natural tile loading + * @param {Object} startPosition - The start position from save file + * @param {Object} region - The region from save file + * @param {number} width - Image width + * @param {number} height - Image height + */ + async preloadTilesForSaveFile(startPosition, region, width, height) { + if (!startPosition || !region || !width || !height) { + console.warn('OverlayManager: Missing parameters for tile preload'); + return; + } + + try { + // Calculate tiles that need to be loaded based on image area + const tileSize = 1000; // WPlace uses 1000px tiles, not 256px like r/place + const tiles = this.calculateTileRange( + region.x, region.y, + startPosition.x, startPosition.y, + width, height, + tileSize + ); + + console.log(`šŸ”„ WPlace: Need to preload ${(tiles.endTileX - tiles.startTileX + 1) * (tiles.endTileY - tiles.startTileY + 1)} tiles for save file...`); + + // For WPlace, instead of manually fetching tiles, we need to wait for natural tile loading + // The browser will request tiles as the user navigates, and we intercept them + // For now, just log the tiles we need and let the overlay work with what's available + const neededTiles = []; + for (let tileX = tiles.startTileX; tileX <= tiles.endTileX; tileX++) { + for (let tileY = tiles.startTileY; tileY <= tiles.endTileY; tileY++) { + neededTiles.push(`${tileX},${tileY}`); + } + } + + console.log(`šŸ“ WPlace: Will use tiles when available: ${neededTiles.slice(0, 5).join(', ')}${neededTiles.length > 5 ? ` and ${neededTiles.length - 5} more...` : ''}`); + + // Store the needed tiles list for reference + this.neededTilesForSave = new Set(neededTiles); + + console.log('āœ… WPlace: Tile preload setup complete - tiles will populate as user navigates'); + } catch (error) { + console.error('āŒ Failed to setup tile preload for save file:', error); + } + } + + /** + * Sleep utility + * @param {number} ms - Milliseconds to sleep + * @returns {Promise} + * @private + */ + _sleep(ms) { + return new Promise((resolve) => setTimeout(resolve, ms)); + } + + /** + * Clean up resources + */ + cleanup() { + this.clear(); + if (this.workerPool) { + // Clean up worker pool if implemented + this.workerPool = null; + } + } +} + +// Create global instance +window.WPlaceOverlayManager = OverlayManager; + +// Create global instance for Auto-Image.js compatibility +window.globalOverlayManager = new OverlayManager(); + +// Legacy compatibility - expose key methods globally for backward compatibility +window.OverlayManager = OverlayManager; + +console.log('āœ… WPlace Overlay Manager loaded and ready'); \ No newline at end of file diff --git a/Extensions/scripts/token-manager.js b/Extensions/scripts/token-manager.js new file mode 100644 index 0000000..874bfe4 --- /dev/null +++ b/Extensions/scripts/token-manager.js @@ -0,0 +1,846 @@ +// ==UserScript== +// @name WPlace Token Manager +// @namespace http://tampermonkey.net/ +// @version 2025-09-16.1 +// @description Turnstile token management for WPlace AutoBot +// @author Wbot +// @match https://wplace.live/* +// @grant nones +// ==/UserScript== + +/** + * TokenManager - Handles Turnstile token generation, caching, and validation for WPlace AutoBot + * Extracted from Auto-Image.js for better modularity and reusabilitys + */ +class TokenManager { + constructor() { + // Token state + this.turnstileToken = null; + this.tokenExpiryTime = 0; + this.tokenGenerationInProgress = false; + this._resolveToken = null; + this.tokenPromise = new Promise((resolve) => { + this._resolveToken = resolve; + }); + + // Configuration constants + this.TOKEN_LIFETIME = 240000; // 4 minutes (tokens typically last 5 min, use 4 for safety) + this.MAX_RETRIES = 10; + this.MAX_BATCH_RETRIES = 10; // Maximum attempts for batch sending + + // Turnstile widget state + this.turnstileLoaded = false; + this._turnstileContainer = null; + this._turnstileOverlay = null; + this._turnstileWidgetId = null; + this._lastSitekey = null; + this._cachedSitekey = null; + + // Retry counter + this.retryCount = 0; + + // Initialize message listener for token capture + this._initializeTokenCapture(); + } + + /** + * Initialize token capture from injected script + */ + _initializeTokenCapture() { + window.addEventListener('message', (event) => { + const { source, token } = event.data; + if (source === 'turnstile-capture' && token) { + this.setTurnstileToken(token); + // Show success message if UI is expecting it + if (document.querySelector('#statusText')?.textContent.includes('CAPTCHA')) { + this._showAlert('Token captured successfully', 'success'); + } + } + }); + } + + /** + * Set a new Turnstile token with expiry + * @param {string} token - The Turnstile token + */ + setTurnstileToken(token) { + if (this._resolveToken) { + this._resolveToken(token); + this._resolveToken = null; + } + this.turnstileToken = token; + this.tokenExpiryTime = Date.now() + this.TOKEN_LIFETIME; + console.log('āœ… Turnstile token set successfully'); + } + + /** + * Check if current token is valid + * @returns {boolean} True if token exists and not expired + */ + isTokenValid() { + return this.turnstileToken && Date.now() < this.tokenExpiryTime; + } + + /** + * Invalidate current token + */ + invalidateToken() { + this.turnstileToken = null; + this.tokenExpiryTime = 0; + console.log('šŸ—‘ļø Token invalidated, will force fresh generation'); + } + + /** + * Ensure a valid token is available, generating if necessary + * @param {boolean} forceRefresh - Force generation of new token + * @returns {Promise} The token or null if failed + */ + async ensureToken(forceRefresh = false) { + // Return cached token if still valid and not forcing refresh + if (this.isTokenValid() && !forceRefresh) { + return this.turnstileToken; + } + + // Invalidate token if forcing refresh + if (forceRefresh) this.invalidateToken(); + + // Avoid multiple simultaneous token generations + if (this.tokenGenerationInProgress) { + console.log('šŸ”„ Token generation already in progress, waiting...'); + await this._sleep(2000); + return this.isTokenValid() ? this.turnstileToken : null; + } + + this.tokenGenerationInProgress = true; + + try { + console.log('šŸ”„ Token expired or missing, generating new one...'); + const token = await this.handleCaptchaWithRetry(); + if (token && token.length > 20) { + this.setTurnstileToken(token); + console.log('āœ… Token captured and cached successfully'); + return token; + } + + console.log('āš ļø Invisible Turnstile failed, forcing browser automation...'); + const fallbackToken = await this.handleCaptchaFallback(); + if (fallbackToken && fallbackToken.length > 20) { + this.setTurnstileToken(fallbackToken); + console.log('āœ… Fallback token captured successfully'); + return fallbackToken; + } + + console.log('āŒ All token generation methods failed'); + return null; + } finally { + this.tokenGenerationInProgress = false; + } + } + + /** + * Handle captcha generation with retry logic + * @returns {Promise} The token or null if failed + */ + async handleCaptchaWithRetry() { + const startTime = performance.now(); + + try { + const { sitekey, token: preGeneratedToken } = await this.obtainSitekeyAndToken(); + + if (!sitekey) { + throw new Error('No valid sitekey found'); + } + + console.log('šŸ”‘ Using sitekey:', sitekey); + + if (typeof window !== 'undefined' && window.navigator) { + console.log( + '🧭 UA:', + window.navigator.userAgent.substring(0, 50) + '...', + 'Platform:', + window.navigator.platform + ); + } + + let token = null; + + if ( + preGeneratedToken && + typeof preGeneratedToken === 'string' && + preGeneratedToken.length > 20 + ) { + console.log('ā™»ļø Reusing pre-generated Turnstile token'); + token = preGeneratedToken; + } else { + if (this.isTokenValid()) { + console.log('ā™»ļø Using existing cached token (from previous session)'); + token = this.turnstileToken; + } else { + console.log('šŸ” Generating new token with executeTurnstile...'); + token = await this.executeTurnstile(sitekey, 'paint'); + if (token) this.setTurnstileToken(token); + } + } + + if (token && typeof token === 'string' && token.length > 20) { + const elapsed = Math.round(performance.now() - startTime); + console.log(`āœ… Turnstile token generated successfully in ${elapsed}ms`); + return token; + } else { + throw new Error(`Invalid or empty token received - Length: ${token?.length || 0}`); + } + } catch (error) { + const elapsed = Math.round(performance.now() - startTime); + console.error(`āŒ Turnstile token generation failed after ${elapsed}ms:`, error); + throw error; + } + } + + /** + * Fallback token generation method using pixel placement automation + * @returns {Promise} The token or null if failed + */ + async handleCaptchaFallback() { + return new Promise(async (resolve, reject) => { + try { + // Ensure we have a fresh promise to await for a new token capture + if (!this._resolveToken) { + this.tokenPromise = new Promise((res) => { + this._resolveToken = res; + }); + } + + const timeoutPromise = this._sleep(20000).then(() => + reject(new Error('Auto-CAPTCHA timed out.')) + ); + + const solvePromise = (async () => { + // Wait for main paint button + const mainPaintBtn = await this._waitForSelector( + 'button.btn.btn-primary.btn-lg, button.btn.btn-primary.sm\\:btn-xl', + 200, + 10000 + ); + if (!mainPaintBtn) throw new Error('Could not find the main paint button.'); + mainPaintBtn.click(); + await this._sleep(500); + + // Click transparent color button + const transBtn = await this._waitForSelector('button#color-0', 200, 5000); + if (!transBtn) throw new Error('Could not find the transparent color button.'); + transBtn.click(); + await this._sleep(500); + + // Find canvas and simulate painting + const canvas = await this._waitForSelector('canvas', 200, 5000); + if (!canvas) throw new Error('Could not find the canvas element.'); + + canvas.setAttribute('tabindex', '0'); + canvas.focus(); + const rect = canvas.getBoundingClientRect(); + const centerX = Math.round(rect.left + rect.width / 2); + const centerY = Math.round(rect.top + rect.height / 2); + + // Simulate mouse movement and spacebar press + canvas.dispatchEvent( + new MouseEvent('mousemove', { + clientX: centerX, + clientY: centerY, + bubbles: true, + }) + ); + canvas.dispatchEvent( + new KeyboardEvent('keydown', { + key: ' ', + code: 'Space', + bubbles: true, + }) + ); + await this._sleep(50); + canvas.dispatchEvent( + new KeyboardEvent('keyup', { + key: ' ', + code: 'Space', + bubbles: true, + }) + ); + await this._sleep(500); + + // Delay before sending confirmation + await this._sleep(800); + + // Keep confirming until token is captured + const confirmLoop = async () => { + while (!this.turnstileToken) { + let confirmBtn = await this._waitForSelector( + 'button.btn.btn-primary.btn-lg, button.btn.btn-primary.sm\\:btn-xl' + ); + if (!confirmBtn) { + const allPrimary = Array.from(document.querySelectorAll('button.btn-primary')); + confirmBtn = allPrimary.length ? allPrimary[allPrimary.length - 1] : null; + } + if (confirmBtn) { + confirmBtn.click(); + } + await this._sleep(500); // 500ms delay between confirmation attempts + } + }; + + // Start confirmation loop and wait for token + confirmLoop(); + const token = await this.tokenPromise; + await this._sleep(300); // small delay after token is captured + resolve(token); + })(); + + await Promise.race([solvePromise, timeoutPromise]); + } catch (error) { + console.error('Auto-CAPTCHA process failed:', error); + reject(error); + } + }); + } + + /** + * Load Turnstile script if not already loaded + * @returns {Promise} + */ + async loadTurnstile() { + // If Turnstile is already present, just resolve. + if (window.turnstile) { + this.turnstileLoaded = true; + return Promise.resolve(); + } + + return new Promise((resolve, reject) => { + // Avoid adding the script twice + if ( + document.querySelector( + 'script[src^="https://challenges.cloudflare.com/turnstile/v0/api.js"]' + ) + ) { + const checkReady = () => { + if (window.turnstile) { + this.turnstileLoaded = true; + resolve(); + } else { + setTimeout(checkReady, 100); + } + }; + return checkReady(); + } + + const script = document.createElement('script'); + script.src = 'https://challenges.cloudflare.com/turnstile/v0/api.js?render=explicit'; + script.async = true; + script.defer = true; + script.onload = () => { + this.turnstileLoaded = true; + console.log('āœ… Turnstile script loaded successfully'); + resolve(); + }; + script.onerror = () => { + console.error('āŒ Failed to load Turnstile script'); + reject(new Error('Failed to load Turnstile')); + }; + document.head.appendChild(script); + }); + } + + /** + * Create or reuse the turnstile container - completely hidden for token generation + * @returns {HTMLElement} The container element + */ + ensureTurnstileContainer() { + if (!this._turnstileContainer || !document.body.contains(this._turnstileContainer)) { + // Clean up old container if it exists + if (this._turnstileContainer) { + this._turnstileContainer.remove(); + } + + this._turnstileContainer = document.createElement('div'); + this._turnstileContainer.className = 'wplace-turnstile-hidden'; + this._turnstileContainer.setAttribute('aria-hidden', 'true'); + this._turnstileContainer.id = 'turnstile-widget-container'; + document.body.appendChild(this._turnstileContainer); + } + return this._turnstileContainer; + } + + /** + * Interactive overlay container for visible widgets when needed + * @returns {HTMLElement} The overlay element + */ + ensureTurnstileOverlayContainer() { + if (this._turnstileOverlay && document.body.contains(this._turnstileOverlay)) { + return this._turnstileOverlay; + } + + const overlay = document.createElement('div'); + overlay.id = 'turnstile-overlay-container'; + overlay.className = 'wplace-turnstile-overlay wplace-overlay-hidden'; + + const title = document.createElement('div'); + title.textContent = this._getText('turnstileInstructions', 'Complete the verification'); + title.className = 'wplace-turnstile-title'; + + const host = document.createElement('div'); + host.id = 'turnstile-overlay-host'; + host.className = 'wplace-turnstile-host'; + + const hideBtn = document.createElement('button'); + hideBtn.textContent = this._getText('hideTurnstileBtn', 'Hide'); + hideBtn.className = 'wplace-turnstile-hide-btn'; + hideBtn.addEventListener('click', () => overlay.remove()); + + overlay.appendChild(title); + overlay.appendChild(host); + overlay.appendChild(hideBtn); + document.body.appendChild(overlay); + + this._turnstileOverlay = overlay; + return overlay; + } + + /** + * Execute Turnstile widget to generate token + * @param {string} sitekey - The site key + * @param {string} action - The action name (default: 'paint') + * @returns {Promise} The token or null if failed + */ + async executeTurnstile(sitekey, action = 'paint') { + await this.loadTurnstile(); + + // Try reusing existing widget first if sitekey matches + if (this._turnstileWidgetId && this._lastSitekey === sitekey && window.turnstile?.execute) { + try { + console.log('šŸ”„ Reusing existing Turnstile widget...'); + const token = await Promise.race([ + window.turnstile.execute(this._turnstileWidgetId, { action }), + new Promise((_, reject) => + setTimeout(() => reject(new Error('Execute timeout')), 15000) + ), + ]); + if (token && token.length > 20) { + console.log('āœ… Token generated via widget reuse'); + return token; + } + } catch (error) { + console.log('āš ļø Widget reuse failed, will create a fresh widget:', error.message); + } + } + + // Try invisible widget first + const invisibleToken = await this.createTurnstileWidget(sitekey, action); + if (invisibleToken && invisibleToken.length > 20) { + return invisibleToken; + } + + console.log('āš ļø Falling back to interactive Turnstile (visible).'); + return await this.createTurnstileWidgetInteractive(sitekey, action); + } + + /** + * Create invisible Turnstile widget + * @param {string} sitekey - The site key + * @param {string} action - The action name + * @returns {Promise} The token or null if failed + */ + async createTurnstileWidget(sitekey, action) { + return new Promise((resolve) => { + try { + // Force cleanup of any existing widget + if (this._turnstileWidgetId && window.turnstile?.remove) { + try { + window.turnstile.remove(this._turnstileWidgetId); + console.log('🧹 Cleaned up existing Turnstile widget'); + } catch (e) { + console.warn('āš ļø Widget cleanup warning:', e.message); + } + } + + const container = this.ensureTurnstileContainer(); + container.innerHTML = ''; + + // Verify Turnstile is available + if (!window.turnstile?.render) { + console.error('āŒ Turnstile not available for rendering'); + resolve(null); + return; + } + + console.log('šŸ”§ Creating invisible Turnstile widget...'); + const widgetId = window.turnstile.render(container, { + sitekey, + action, + size: 'invisible', + retry: 'auto', + 'retry-interval': 8000, + callback: (token) => { + console.log('āœ… Invisible Turnstile callback'); + resolve(token); + }, + 'error-callback': () => resolve(null), + 'timeout-callback': () => resolve(null), + }); + + this._turnstileWidgetId = widgetId; + this._lastSitekey = sitekey; + + if (!widgetId) { + return resolve(null); + } + + // Execute the widget and race with timeout + Promise.race([ + window.turnstile.execute(widgetId, { action }), + new Promise((_, reject) => + setTimeout(() => reject(new Error('Invisible execute timeout')), 12000) + ), + ]) + .then(resolve) + .catch(() => resolve(null)); + } catch (e) { + console.error('āŒ Invisible Turnstile creation failed:', e); + resolve(null); + } + }); + } + + /** + * Create interactive Turnstile widget (visible) + * @param {string} sitekey - The site key + * @param {string} action - The action name + * @returns {Promise} The token or null if failed + */ + async createTurnstileWidgetInteractive(sitekey, action) { + // Create a visible widget that users can interact with if needed + console.log('šŸ”„ Creating interactive Turnstile widget (visible)'); + + return new Promise((resolve) => { + try { + // Force cleanup of any existing widget + if (this._turnstileWidgetId && window.turnstile?.remove) { + try { + window.turnstile.remove(this._turnstileWidgetId); + } catch (e) { + console.warn('āš ļø Widget cleanup warning:', e.message); + } + } + + const overlay = this.ensureTurnstileOverlayContainer(); + overlay.classList.remove('wplace-overlay-hidden'); + overlay.style.display = 'block'; + + const host = overlay.querySelector('#turnstile-overlay-host'); + host.innerHTML = ''; + + // Set a timeout for interactive mode + const timeout = setTimeout(() => { + console.warn('ā° Interactive Turnstile widget timeout'); + overlay.classList.add('wplace-overlay-hidden'); + overlay.style.display = 'none'; + resolve(null); + }, 60000); // 60 seconds for user interaction + + const widgetId = window.turnstile.render(host, { + sitekey, + action, + size: 'normal', + theme: 'light', + callback: (token) => { + clearTimeout(timeout); + overlay.classList.add('wplace-overlay-hidden'); + overlay.style.display = 'none'; + console.log('āœ… Interactive Turnstile completed successfully'); + + if (typeof token === 'string' && token.length > 20) { + resolve(token); + } else { + console.warn('āŒ Invalid token from interactive widget'); + resolve(null); + } + }, + 'error-callback': (error) => { + clearTimeout(timeout); + overlay.classList.add('wplace-overlay-hidden'); + overlay.style.display = 'none'; + console.warn('āŒ Interactive Turnstile error:', error); + resolve(null); + }, + }); + + this._turnstileWidgetId = widgetId; + this._lastSitekey = sitekey; + + if (!widgetId) { + clearTimeout(timeout); + overlay.classList.add('wplace-overlay-hidden'); + overlay.style.display = 'none'; + console.warn('āŒ Failed to create interactive Turnstile widget'); + resolve(null); + } else { + console.log('āœ… Interactive Turnstile widget created, waiting for user interaction...'); + } + } catch (e) { + console.error('āŒ Interactive Turnstile creation failed:', e); + resolve(null); + } + }); + } + + /** + * Cleanup method for when the script is disabled/reloaded + */ + cleanupTurnstile() { + if (this._turnstileWidgetId && window.turnstile?.remove) { + try { + window.turnstile.remove(this._turnstileWidgetId); + } catch (e) { + console.warn('Failed to cleanup Turnstile widget:', e); + } + } + + if (this._turnstileContainer && document.body.contains(this._turnstileContainer)) { + this._turnstileContainer.remove(); + } + + if (this._turnstileOverlay && document.body.contains(this._turnstileOverlay)) { + this._turnstileOverlay.remove(); + } + + this._turnstileWidgetId = null; + this._turnstileContainer = null; + this._turnstileOverlay = null; + this._lastSitekey = null; + } + + /** + * Obtain sitekey and token + * @param {string} fallback - Fallback sitekey + * @returns {Promise<{sitekey: string, token: string|null}>} + */ + async obtainSitekeyAndToken(fallback = '0x4AAAAAABpqJe8FO0N84q0F') { + // Cache sitekey to avoid repeated DOM queries + if (this._cachedSitekey) { + console.log('šŸ” Using cached sitekey:', this._cachedSitekey); + + return this.isTokenValid() + ? { + sitekey: this._cachedSitekey, + token: this.turnstileToken, + } + : { sitekey: this._cachedSitekey, token: null }; + } + + // List of potential sitekeys to try + const potentialSitekeys = [ + '0x4AAAAAABpqJe8FO0N84q0F', // WPlace common sitekey + '0x4AAAAAAAJ7xjKAp6Mt_7zw', // Alternative WPlace sitekey + '0x4AAAAAADm5QWx6Ov2LNF2g', // Another common sitekey + ]; + + const trySitekey = async (sitekey, source) => { + if (!sitekey || sitekey.length < 10) return null; + + console.log(`šŸ” Testing sitekey from ${source}:`, sitekey); + const token = await this.executeTurnstile(sitekey); + + if (token && token.length >= 20) { + console.log(`āœ… Valid token generated from ${source} sitekey`); + this.setTurnstileToken(token); + this._cachedSitekey = sitekey; + return { sitekey, token }; + } else { + console.log(`āŒ Failed to get token from ${source} sitekey`); + return null; + } + }; + + try { + // 1ļøāƒ£ data-sitekey attribute + const sitekeySel = document.querySelector('[data-sitekey]'); + if (sitekeySel) { + const sitekey = sitekeySel.getAttribute('data-sitekey'); + const result = await trySitekey(sitekey, 'data attribute'); + if (result) { + return result; + } + } + + // 2ļøāƒ£ Turnstile element + const turnstileEl = document.querySelector('.cf-turnstile'); + if (turnstileEl?.dataset?.sitekey) { + const sitekey = turnstileEl.dataset.sitekey; + const result = await trySitekey(sitekey, 'turnstile element'); + if (result) { + return result; + } + } + + // 3ļøāƒ£ Meta tags + const metaTags = document.querySelectorAll( + 'meta[name*="turnstile"], meta[property*="turnstile"]' + ); + for (const meta of metaTags) { + const content = meta.getAttribute('content'); + const result = await trySitekey(content, 'meta tag'); + if (result) { + return result; + } + } + + // 4ļøāƒ£ Global variable + if (window.__TURNSTILE_SITEKEY) { + const result = await trySitekey(window.__TURNSTILE_SITEKEY, 'global variable'); + if (result) { + return result; + } + } + + // 5ļøāƒ£ Script tags + const scripts = document.querySelectorAll('script'); + for (const script of scripts) { + const content = script.textContent || script.innerHTML; + const match = content.match( + /(?:sitekey|data-sitekey)['"\s\[\]:\=\(]*['"]?([0-9a-zA-Z_-]{20,})['"]?/i + ); + if (match && match[1]) { + const extracted = match[1].replace(/['"]/g, ''); + const result = await trySitekey(extracted, 'script content'); + if (result) { + return result; + } + } + } + + // 6ļøāƒ£ Known potential sitekeys + console.log('šŸ” Testing known potential sitekeys...'); + for (const testSitekey of potentialSitekeys) { + const result = await trySitekey(testSitekey, 'known list'); + if (result) { + return result; + } + } + } catch (error) { + console.warn('āš ļø Error during sitekey detection:', error); + } + + // 7ļøāƒ£ Fallback + console.log('šŸ”§ Trying fallback sitekey:', fallback); + const fallbackResult = await trySitekey(fallback, 'fallback'); + if (fallbackResult) { + return fallbackResult; + } + + // 8ļøāƒ£ Ultimate fallback with no token + console.warn('āŒ All sitekey detection methods failed, using fallback without token validation'); + this._cachedSitekey = fallback; + return { sitekey: fallback, token: null }; + } + + // ============================================= + // UTILITY METHODS + // ============================================= + + /** + * Sleep utility + * @param {number} ms - Milliseconds to sleep + * @returns {Promise} + */ + _sleep(ms) { + return new Promise((resolve) => setTimeout(resolve, ms)); + } + + /** + * Wait for DOM element to appear + * @param {string} selector - CSS selector + * @param {number} pollInterval - Polling interval in ms + * @param {number} timeout - Timeout in ms + * @returns {Promise} The element or null if timeout + * @private + */ + _waitForSelector(selector, pollInterval = 200, timeout = 10000) { + return new Promise((resolve) => { + const endTime = Date.now() + timeout; + const poll = () => { + const element = document.querySelector(selector); + if (element) { + resolve(element); + } else if (Date.now() < endTime) { + setTimeout(poll, pollInterval); + } else { + resolve(null); + } + }; + poll(); + }); + } + + /** + * Simple translation helper (fallback to English) + * @param {string} key - Translation key + * @param {string} fallback - Fallback text + * @returns {string} Translated text or fallback + */ + _getText(key, fallback = '') { + // Try to use external translation function if available + if (typeof window.Utils?.t === 'function') { + return window.Utils.t(key); + } + + // Fallback translations + const translations = { + turnstileInstructions: 'Complete the verification', + hideTurnstileBtn: 'Hide', + tokenCapturedSuccess: 'Token captured successfully' + }; + + return translations[key] || fallback || key; + } + + /** + * Show alert helper (uses external alert if available) + * @param {string} message - Message to show + * @param {string} type - Alert type + */ + _showAlert(message, type = 'info') { + // Try to use external alert function if available + if (typeof window.Utils?.showAlert === 'function') { + window.Utils.showAlert(message, type); + } else { + console.log(`${type.toUpperCase()}: ${message}`); + } + } + + /** + * Generate random string utility + * @param {number} len - Length of string + * @param {string} chars - Character set + * @returns {string} Random string + */ + _randStr(len, chars = 'abcdefghijklmnopqrstuvwxyz0123456789') { + return [...Array(len)] + .map(() => + chars[ + (crypto?.getRandomValues?.(new Uint32Array(1))[0] % chars.length) || + Math.floor(Math.random() * chars.length) + ] + ) + .join(''); + } +} + +// Create global instance +window.WPlaceTokenManager = new TokenManager(); + +// Create global instance alias for Auto-Image.js compatibility +window.globalTokenManager = window.WPlaceTokenManager; + +// Legacy compatibility - expose key methods globally for backward compatibility +window.setTurnstileToken = (token) => window.WPlaceTokenManager.setTurnstileToken(token); +window.ensureToken = (forceRefresh) => window.WPlaceTokenManager.ensureToken(forceRefresh); +window.isTokenValid = () => window.WPlaceTokenManager.isTokenValid(); +window.invalidateToken = () => window.WPlaceTokenManager.invalidateToken(); + +console.log('āœ… WPlace Token Manager loaded and ready'); \ No newline at end of file diff --git a/Extensions/scripts/utils-manager.js b/Extensions/scripts/utils-manager.js new file mode 100644 index 0000000..e0f458a --- /dev/null +++ b/Extensions/scripts/utils-manager.js @@ -0,0 +1,1258 @@ +/** + * WPlace AutoBOT - Utils Manager + * Centralized utility functions for the WPlace automation systemds + */ + +class WPlaceUtilsManager { + constructor() { + this.translationCache = new Map(); + this._labCache = new Map(); + + // Available languages + this.AVAILABLE_LANGUAGES = [ + 'en', 'ru', 'pt', 'vi', 'fr', 'id', 'tr', + 'zh-CN', 'zh-TW', 'ja', 'ko', 'uk' + ]; + } + + // Basic utility functions + sleep = (ms) => new Promise((r) => setTimeout(r, ms)); + + async dynamicSleep(tickAndGetRemainingMs) { + let remaining = Math.max(0, await tickAndGetRemainingMs()); + while (remaining > 0) { + const interval = remaining > 5000 ? 2000 : remaining > 1000 ? 500 : 100; + await this.sleep(Math.min(interval, remaining)); + remaining = Math.max(0, await tickAndGetRemainingMs()); + } + } + + async waitForSelector(selector, interval = 200, timeout = 5000) { + const start = Date.now(); + while (Date.now() - start < timeout) { + const el = document.querySelector(selector); + if (el) return el; + await this.sleep(interval); + } + return null; + } + + msToTimeText(ms) { + const totalSeconds = Math.ceil(ms / 1000); + const hours = Math.floor(totalSeconds / 3600); + const minutes = Math.floor((totalSeconds % 3600) / 60); + const seconds = totalSeconds % 60; + + if (hours > 0) return `${hours}h ${minutes}m ${seconds}s`; + if (minutes > 0) return `${minutes}m ${seconds}s`; + return `${seconds}s`; + } + + // Debounced scroll-to-adjust handler for sliders + createScrollToAdjust(element, updateCallback, min, max, step = 1) { + let debounceTimer = null; + + const handleWheel = (e) => { + if (e.target !== element) return; + + e.preventDefault(); + e.stopPropagation(); + + if (debounceTimer) { + clearTimeout(debounceTimer); + } + + debounceTimer = setTimeout(() => { + const currentValue = parseInt(element.value) || 0; + const delta = e.deltaY > 0 ? -step : step; + const newValue = Math.max(min, Math.min(max, currentValue + delta)); + + if (newValue !== currentValue) { + element.value = newValue; + updateCallback(newValue); + } + }, 50); + }; + + element.addEventListener('wheel', handleWheel, { passive: false }); + + return () => { + if (debounceTimer) clearTimeout(debounceTimer); + element.removeEventListener('wheel', handleWheel); + }; + } + + // Tile range calculation delegation + calculateTileRange(startRegionX, startRegionY, startPixelX, startPixelY, width, height, tileSize = 1000) { + return window.globalOverlayManager.calculateTileRange( + startRegionX, startRegionY, startPixelX, startPixelY, width, height, tileSize + ); + } + + // Token management delegation + loadTurnstile() { return window.globalTokenManager.loadTurnstile(); } + ensureTurnstileContainer() { return window.globalTokenManager.ensureTurnstileContainer(); } + ensureTurnstileOverlayContainer() { return window.globalTokenManager.ensureTurnstileOverlayContainer(); } + executeTurnstile(sitekey, action) { return window.globalTokenManager.executeTurnstile(sitekey, action); } + createTurnstileWidget(sitekey, action) { return window.globalTokenManager.createTurnstileWidget(sitekey, action); } + createTurnstileWidgetInteractive(sitekey, action) { return window.globalTokenManager.createTurnstileWidgetInteractive(sitekey, action); } + cleanupTurnstile() { return window.globalTokenManager.cleanupTurnstile(); } + obtainSitekeyAndToken(fallback) { return window.globalTokenManager.obtainSitekeyAndToken(fallback); } + + // DOM utilities + createElement(tag, props = {}, children = []) { + const element = document.createElement(tag); + + Object.entries(props).forEach(([key, value]) => { + if (key === 'style' && typeof value === 'object') { + Object.assign(element.style, value); + } else if (key === 'className') { + element.className = value; + } else if (key === 'innerHTML') { + element.innerHTML = value; + } else { + element.setAttribute(key, value); + } + }); + + if (typeof children === 'string') { + element.textContent = children; + } else if (Array.isArray(children)) { + children.forEach((child) => { + if (typeof child === 'string') { + element.appendChild(document.createTextNode(child)); + } else { + element.appendChild(child); + } + }); + } + + return element; + } + + createButton(id, text, icon, onClick, style = window.CONFIG.CSS_CLASSES.BUTTON_PRIMARY) { + const button = this.createElement('button', { + id: id, + style: style, + innerHTML: `${icon ? `` : ''}${text}`, + }); + if (onClick) button.addEventListener('click', onClick); + return button; + } + + // Translation function + t(key, params = {}) { + const state = window.state; + const translationCache = this.translationCache; + const loadedTranslations = window.loadedTranslations || {}; + + // Safety check: ensure state and language exist + if (!state || !state.language) { + // Fallback: try to get translation directly or return key + if (loadedTranslations[key]) { + let text = loadedTranslations[key]; + Object.keys(params).forEach((param) => { + text = text.replace(`{${param}}`, params[param]); + }); + return text; + } + return key; // Return the key as fallback + } + + const cacheKey = `${state.language}_${key}`; + if (translationCache.has(cacheKey)) { + let text = translationCache.get(cacheKey); + Object.keys(params).forEach((param) => { + text = text.replace(`{${param}}`, params[param]); + }); + return text; + } + + if (loadedTranslations[state.language]?.[key]) { + let text = loadedTranslations[state.language][key]; + translationCache.set(cacheKey, text); + Object.keys(params).forEach((param) => { + text = text.replace(`{${param}}`, params[param]); + }); + return text; + } + + // Try English fallback if available and not using English + if (state.language && state.language !== 'en' && loadedTranslations['en']?.[key]) { + let text = loadedTranslations['en'][key]; + Object.keys(params).forEach((param) => { + text = text.replace(`{${param}}`, params[param]); + }); + return text; + } + + // Final fallback to FALLBACK_TEXT or key + const language = state.language || 'en'; + let text = window.FALLBACK_TEXT?.[language]?.[key] || window.FALLBACK_TEXT?.en?.[key] || key; + Object.keys(params).forEach((param) => { + text = text.replace(new RegExp(`\\{${param}\\}`, 'g'), params[param]); + }); + + if (text === key && key !== 'undefined') { + console.warn(`āš ļø Missing translation for key: ${key} (language: ${language})`); + } + + return text; + } + + showAlert(message, type = 'info') { + const alertDiv = document.createElement('div'); + alertDiv.className = `wplace-alert-base wplace-alert-${type}`; + alertDiv.textContent = message; + document.body.appendChild(alertDiv); + + setTimeout(() => { + alertDiv.style.animation = 'slide-down 0.3s ease-out reverse'; + setTimeout(() => { + document.body.removeChild(alertDiv); + }, 300); + }, 4000); + } + + // Color utilities + colorDistance(a, b) { + return Math.sqrt(Math.pow(a[0] - b[0], 2) + Math.pow(a[1] - b[1], 2) + Math.pow(a[2] - b[2], 2)); + } + + _rgbToLab(r, g, b) { + const srgbToLinear = (v) => { + v /= 255; + return v <= 0.04045 ? v / 12.92 : Math.pow((v + 0.055) / 1.055, 2.4); + }; + const rl = srgbToLinear(r); + const gl = srgbToLinear(g); + const bl = srgbToLinear(b); + let X = rl * 0.4124 + gl * 0.3576 + bl * 0.1805; + let Y = rl * 0.2126 + gl * 0.7152 + bl * 0.0722; + let Z = rl * 0.0193 + gl * 0.1192 + bl * 0.9505; + X /= 0.95047; + Y /= 1.0; + Z /= 1.08883; + const f = (t) => (t > 0.008856 ? Math.cbrt(t) : 7.787 * t + 16 / 116); + const fX = f(X), fY = f(Y), fZ = f(Z); + const L = 116 * fY - 16; + const a = 500 * (fX - fY); + const b2 = 200 * (fY - fZ); + return [L, a, b2]; + } + + _lab(r, g, b) { + const key = (r << 16) | (g << 8) | b; + let v = this._labCache.get(key); + if (!v) { + v = this._rgbToLab(r, g, b); + this._labCache.set(key, v); + } + return v; + } + + findClosestPaletteColor(r, g, b, palette) { + // Use provided palette or derive from COLOR_MAP + if (!palette || palette.length === 0) { + palette = Object.values(window.CONFIG.COLOR_MAP) + .filter((c) => c.rgb) + .map((c) => [c.rgb.r, c.rgb.g, c.rgb.b]); + } + + // Use modular ImageProcessor for color matching if available + if (window.globalImageProcessor) { + return window.globalImageProcessor.findClosestPaletteColor( + r, g, b, palette, + window.state.colorMatchingAlgorithm || 'lab', + { + enableChromaPenalty: window.state.enableChromaPenalty || false, + chromaPenaltyWeight: window.state.chromaPenaltyWeight || 0.15 + } + ); + } + + // Fallback: simple color distance matching + let closestColor = null; + let minDistance = Infinity; + + for (const color of palette) { + const distance = this.colorDistance([r, g, b], color); + if (distance < minDistance) { + minDistance = distance; + closestColor = color; + } + } + + return closestColor || [r, g, b]; + } + + isWhitePixel(r, g, b) { + const wt = window.state.customWhiteThreshold || window.CONFIG.WHITE_THRESHOLD; + if (window.globalImageProcessor) { + return window.globalImageProcessor.isWhitePixel(r, g, b, wt); + } + // Fallback calculation if image processor not available + return r >= wt && g >= wt && b >= wt; + } + + resolveColor(targetRgb, availableColors, exactMatch = false) { + if (window.globalImageProcessor) { + return window.globalImageProcessor.resolveColor(targetRgb, availableColors, { + exactMatch, + algorithm: window.state.colorMatchingAlgorithm || 'lab', + enableChromaPenalty: window.state.enableChromaPenalty || false, + chromaPenaltyWeight: window.state.chromaPenaltyWeight || 0.15, + whiteThreshold: window.state.customWhiteThreshold || window.CONFIG.WHITE_THRESHOLD + }); + } + + // Fallback: simple distance-based matching + if (!availableColors || availableColors.length === 0) return null; + + let closestColor = null; + let minDistance = Infinity; + + for (const color of availableColors) { + const distance = this.colorDistance(targetRgb, color.rgb || [color.rgb.r, color.rgb.g, color.rgb.b]); + if (distance < minDistance) { + minDistance = distance; + closestColor = color; + } + } + + return closestColor; + } + + createImageUploader() { + if (window.globalImageProcessor) { + return window.globalImageProcessor.createImageUploader(); + } + + // Fallback implementation if image processor not available + return new Promise((resolve, reject) => { + const input = document.createElement('input'); + input.type = 'file'; + input.accept = 'image/*'; + input.onchange = (e) => { + const file = e.target.files[0]; + if (file) { + const reader = new FileReader(); + reader.onload = () => resolve(reader.result); + reader.onerror = () => reject(new Error('File reading error')); + reader.readAsDataURL(file); + } else { + reject(new Error('No file selected')); + } + }; + input.click(); + }); + } + + // File utilities + createFileDownloader(data, filename) { + const blob = new Blob([data], { type: 'application/json' }); + const url = URL.createObjectURL(blob); + const a = document.createElement('a'); + a.href = url; + a.download = filename; + document.body.appendChild(a); + a.click(); + document.body.removeChild(a); + URL.revokeObjectURL(url); + } + + createFileUploader() { + return new Promise((resolve, reject) => { + const input = document.createElement('input'); + input.type = 'file'; + input.accept = '.json'; + input.onchange = (e) => { + const file = e.target.files[0]; + if (file) { + const reader = new FileReader(); + reader.onload = () => { + try { + const data = JSON.parse(reader.result); + resolve(data); + } catch (error) { + reject(new Error('Invalid JSON file')); + } + }; + reader.onerror = () => reject(new Error('File reading error')); + reader.readAsText(file); + } else { + reject(new Error('No file selected')); + } + }; + input.click(); + }); + } + + extractAvailableColors() { + if (window.globalImageProcessor) { + return window.globalImageProcessor.extractAvailableColors(window.CONFIG.COLOR_MAP); + } + + // Fallback: extract colors directly from CONFIG if image processor not available + if (window.CONFIG && window.CONFIG.COLOR_MAP) { + return Object.values(window.CONFIG.COLOR_MAP) + .filter(color => color.rgb !== null) + .map(color => ({ + id: color.id, + name: color.name, + rgb: [color.rgb.r, color.rgb.g, color.rgb.b] + })); + } + + return []; + } + + formatTime(ms) { + // Handle invalid or infinite values + if (!Number.isFinite(ms) || ms < 0) { + return '--:--:--'; + } + + // Handle very large values (more than 999 days) + if (ms > 999 * 24 * 60 * 60 * 1000) { + return '999d+'; + } + + const seconds = Math.floor((ms / 1000) % 60); + const minutes = Math.floor((ms / (1000 * 60)) % 60); + const hours = Math.floor((ms / (1000 * 60 * 60)) % 24); + const days = Math.floor(ms / (1000 * 60 * 60 * 24)); + + let result = ''; + if (days > 0) result += `${days}d `; + if (hours > 0 || days > 0) result += `${hours}h `; + if (minutes > 0 || hours > 0 || days > 0) result += `${minutes}m `; + result += `${seconds}s`; + + return result; + } + + calculateEstimatedTime(remainingPixels, charges, cooldown) { + // Safety checks for input parameters + if (!Number.isFinite(remainingPixels) || remainingPixels <= 0) return 0; + if (!Number.isFinite(charges) || charges <= 0) charges = 1; + if (!Number.isFinite(cooldown) || cooldown <= 0) cooldown = 30000; // Default 30s + + const paintingSpeed = window.state?.paintingSpeed || 5; + if (!Number.isFinite(paintingSpeed) || paintingSpeed <= 0) { + // Fallback calculation without painting speed + const cyclesNeeded = Math.ceil(remainingPixels / Math.max(charges, 1)); + const timeFromCharges = cyclesNeeded * cooldown; + return Math.min(timeFromCharges, 999 * 24 * 60 * 60 * 1000); // Cap at 999 days + } + + const paintingSpeedDelay = 1000 / paintingSpeed; + const timeFromSpeed = remainingPixels * paintingSpeedDelay; + + const cyclesNeeded = Math.ceil(remainingPixels / Math.max(charges, 1)); + const timeFromCharges = cyclesNeeded * cooldown; + + const totalTime = timeFromSpeed + timeFromCharges; + + // Safety check to prevent infinity and cap at reasonable maximum + if (!Number.isFinite(totalTime) || totalTime < 0) { + return 0; + } + + // Cap at 999 days to prevent display issues + return Math.min(totalTime, 999 * 24 * 60 * 60 * 1000); + } + + // Painted pixel tracking helpers + initializePaintedMap(width, height) { + if (!window.state.paintedMap || window.state.paintedMap.length !== height) { + window.state.paintedMap = Array(height) + .fill() + .map(() => Array(width).fill(false)); + } + } + + markPixelPainted(x, y, regionX = 0, regionY = 0) { + const actualX = x + regionX; + const actualY = y + regionY; + + if ( + window.state.paintedMap && + window.state.paintedMap[actualY] && + actualX >= 0 && + actualX < window.state.paintedMap[actualY].length + ) { + window.state.paintedMap[actualY][actualX] = true; + } + } + + isPixelPainted(x, y, regionX = 0, regionY = 0) { + const actualX = x + regionX; + const actualY = y + regionY; + + if ( + window.state.paintedMap && + window.state.paintedMap[actualY] && + actualX >= 0 && + actualX < window.state.paintedMap[actualY].length + ) { + return window.state.paintedMap[actualY][actualX]; + } + return false; + } + + // Alias for isPixelPainted - used in pre-filtering logic + isPixelMarkedPainted(x, y, regionX = 0, regionY = 0) { + return this.isPixelPainted(x, y, regionX, regionY); + } + + // Smart save + shouldAutoSave() { + const now = Date.now(); + const pixelsSinceLastSave = window.state.paintedPixels - window.state._lastSavePixelCount; + const timeSinceLastSave = now - window.state._lastSaveTime; + + return !window.state._saveInProgress && pixelsSinceLastSave >= 25 && timeSinceLastSave >= 30000; + } + + performSmartSave() { + if (!this.shouldAutoSave()) return false; + + window.state._saveInProgress = true; + const success = this.saveProgress(); + + if (success) { + window.state._lastSavePixelCount = window.state.paintedPixels; + window.state._lastSaveTime = Date.now(); + } + + window.state._saveInProgress = false; + return success; + } + + // Data compression helpers + packPaintedMapToBase64(paintedMap, width, height) { + if (!paintedMap || !width || !height) return null; + const totalBits = width * height; + const byteLen = Math.ceil(totalBits / 8); + const bytes = new Uint8Array(byteLen); + let bitIndex = 0; + for (let y = 0; y < height; y++) { + const row = paintedMap[y]; + for (let x = 0; x < width; x++) { + const bit = row && row[x] ? 1 : 0; + const b = bitIndex >> 3; + const o = bitIndex & 7; + if (bit) bytes[b] |= 1 << o; + bitIndex++; + } + } + let binary = ''; + const chunk = 0x8000; + for (let i = 0; i < bytes.length; i += chunk) { + binary += String.fromCharCode.apply( + null, + bytes.subarray(i, Math.min(i + chunk, bytes.length)) + ); + } + return btoa(binary); + } + + unpackPaintedMapFromBase64(base64, width, height) { + if (!base64 || !width || !height) return null; + const binary = atob(base64); + const bytes = new Uint8Array(binary.length); + for (let i = 0; i < binary.length; i++) bytes[i] = binary.charCodeAt(i); + const map = Array(height) + .fill() + .map(() => Array(width).fill(false)); + let bitIndex = 0; + for (let y = 0; y < height; y++) { + for (let x = 0; x < width; x++) { + const b = bitIndex >> 3; + const o = bitIndex & 7; + map[y][x] = ((bytes[b] >> o) & 1) === 1; + bitIndex++; + } + } + return map; + } + + // Migration helpers + migrateProgressToV2(saved) { + if (!saved) return saved; + const isV1 = !saved.version || saved.version === '1' || saved.version === '1.0' || saved.version === '1.1'; + if (!isV1) return saved; + + try { + const migrated = { ...saved }; + const width = migrated.imageData?.width; + const height = migrated.imageData?.height; + if (migrated.paintedMap && width && height) { + const data = this.packPaintedMapToBase64(migrated.paintedMap, width, height); + migrated.paintedMapPacked = { width, height, data }; + } + delete migrated.paintedMap; + migrated.version = '2'; + return migrated; + } catch (e) { + console.warn('Migration to v2 failed, using original data:', e); + return saved; + } + } + + migrateProgressToV21(saved) { + if (!saved) return saved; + if (saved.version === '2.1') return saved; + const isV2 = saved.version === '2' || saved.version === '2.0'; + const isV1 = !saved.version || saved.version === '1' || saved.version === '1.0' || saved.version === '1.1'; + if (!isV2 && !isV1) return saved; + try { + const migrated = { ...saved }; + if (isV1) { + const width = migrated.imageData?.width; + const height = migrated.imageData?.height; + if (migrated.paintedMap && width && height) { + const data = this.packPaintedMapToBase64(migrated.paintedMap, width, height); + migrated.paintedMapPacked = { width, height, data }; + } + delete migrated.paintedMap; + } + migrated.version = '2.1'; + return migrated; + } catch (e) { + console.warn('Migration to v2.1 failed, using original data:', e); + return saved; + } + } + + migrateProgressToV22(data) { + try { + const migrated = { ...data }; + migrated.version = '2.2'; + + if (!migrated.state.coordinateMode) { + migrated.state.coordinateMode = window.CONFIG.COORDINATE_MODE; + } + if (!migrated.state.coordinateDirection) { + migrated.state.coordinateDirection = window.CONFIG.COORDINATE_DIRECTION; + } + if (!migrated.state.coordinateSnake) { + migrated.state.coordinateSnake = window.CONFIG.COORDINATE_SNAKE; + } + if (!migrated.state.blockWidth) { + migrated.state.blockWidth = window.CONFIG.COORDINATE_BLOCK_WIDTH; + } + if (!migrated.state.blockHeight) { + migrated.state.blockHeight = window.CONFIG.COORDINATE_BLOCK_HEIGHT; + } + + return migrated; + } catch (e) { + console.warn('Migration to v2.2 failed, using original data:', e); + return data; + } + } + + buildPaintedMapPacked() { + if (window.state.paintedMap && window.state.imageData) { + const data = this.packPaintedMapToBase64( + window.state.paintedMap, + window.state.imageData.width, + window.state.imageData.height + ); + if (data) { + return { + width: window.state.imageData.width, + height: window.state.imageData.height, + data: data, + }; + } + } + return null; + } + + buildProgressData() { + return { + timestamp: Date.now(), + version: '2.2', + state: { + totalPixels: window.state.totalPixels, + paintedPixels: window.state.paintedPixels, + lastPosition: window.state.lastPosition, + startPosition: window.state.startPosition, + region: window.state.region, + imageLoaded: window.state.imageLoaded, + colorsChecked: window.state.colorsChecked, + coordinateMode: window.state.coordinateMode, + coordinateDirection: window.state.coordinateDirection, + coordinateSnake: window.state.coordinateSnake, + blockWidth: window.state.blockWidth, + blockHeight: window.state.blockHeight, + availableColors: window.state.availableColors, + }, + imageData: window.state.imageData + ? { + width: window.state.imageData.width, + height: window.state.imageData.height, + pixels: Array.from(window.state.imageData.pixels), + totalPixels: window.state.imageData.totalPixels, + } + : null, + paintedMapPacked: this.buildPaintedMapPacked(), + }; + } + + migrateProgress(saved) { + if (!saved) return null; + + let data = saved; + const ver = data.version; + + if (!ver || ver === '1' || ver === '1.0' || ver === '1.1') { + data = this.migrateProgressToV2(data); + } + + if (data.version === '2' || data.version === '2.0') { + data = this.migrateProgressToV21(data); + } + + if (data.version === '2.1') { + data = this.migrateProgressToV22(data); + } + + return data; + } + + saveProgress() { + try { + const progressData = this.buildProgressData(); + localStorage.setItem('wplace-bot-progress', JSON.stringify(progressData)); + return true; + } catch (error) { + console.error('Error saving progress:', error); + return false; + } + } + + loadProgress() { + try { + const saved = localStorage.getItem('wplace-bot-progress'); + if (!saved) return null; + let data = JSON.parse(saved); + const migrated = this.migrateProgress(data); + + if (migrated && migrated !== data) { + try { + localStorage.setItem('wplace-bot-progress', JSON.stringify(migrated)); + } catch { } + } + return migrated; + } catch (error) { + console.error('Error loading progress:', error); + return null; + } + } + + clearProgress() { + try { + localStorage.removeItem('wplace-bot-progress'); + window.state.paintedMap = null; + window.state._lastSavePixelCount = 0; + window.state._lastSaveTime = 0; + window.state.coordinateMode = window.CONFIG.COORDINATE_MODE; + window.state.coordinateDirection = window.CONFIG.COORDINATE_DIRECTION; + window.state.coordinateSnake = window.CONFIG.COORDINATE_SNAKE; + window.state.blockWidth = window.CONFIG.COORDINATE_BLOCK_WIDTH; + window.state.blockHeight = window.CONFIG.COORDINATE_BLOCK_HEIGHT; + return true; + } catch (error) { + console.error('Error clearing progress:', error); + return false; + } + } + + restoreProgress(savedData) { + try { + Object.assign(window.state, savedData.state); + + // Restore coordinate generation settings + if (savedData.state.coordinateMode) { + window.state.coordinateMode = savedData.state.coordinateMode; + } + if (savedData.state.coordinateDirection) { + window.state.coordinateDirection = savedData.state.coordinateDirection; + } + if (savedData.state.coordinateSnake !== undefined) { + window.state.coordinateSnake = savedData.state.coordinateSnake; + } + if (savedData.state.blockWidth) { + window.state.blockWidth = savedData.state.blockWidth; + } + if (savedData.state.blockHeight) { + window.state.blockHeight = savedData.state.blockHeight; + } + + // Restore available colors from old save files (backward compatibility) + if (savedData.state.availableColors && Array.isArray(savedData.state.availableColors)) { + window.state.availableColors = savedData.state.availableColors; + window.state.colorsChecked = true; // Mark colors as checked + } + + if (savedData.imageData) { + window.state.imageData = { + width: savedData.imageData.width, + height: savedData.imageData.height, + pixels: savedData.imageData.pixels, // Keep as Array - will convert in ImageData constructor + totalPixels: savedData.imageData.totalPixels, + }; + + try { + const canvas = document.createElement('canvas'); + canvas.width = window.state.imageData.width; + canvas.height = window.state.imageData.height; + const ctx = canvas.getContext('2d'); + const imageData = new ImageData( + window.state.imageData.pixels, + window.state.imageData.width, + window.state.imageData.height + ); + ctx.putImageData(imageData, 0, 0); + + // Create ImageProcessor directly like the original - no delegation + if (window.ImageProcessor) { + const proc = new window.ImageProcessor(''); + proc.img = canvas; + proc.canvas = canvas; + proc.ctx = ctx; + window.state.imageData.processor = proc; + } else if (window.WPlaceImageProcessor) { + const proc = new window.WPlaceImageProcessor(''); + proc.img = canvas; + proc.canvas = canvas; + proc.ctx = ctx; + window.state.imageData.processor = proc; + } + } catch (e) { + console.warn('Could not rebuild processor from saved image data:', e); + } + } + + // Prefer packed form if available; fallback to legacy paintedMap array for backward compatibility + if (savedData.paintedMapPacked && savedData.paintedMapPacked.data) { + const { width, height, data } = savedData.paintedMapPacked; + window.state.paintedMap = this.unpackPaintedMapFromBase64(data, width, height); + } else if (savedData.paintedMap) { + window.state.paintedMap = savedData.paintedMap.map((row) => Array.from(row)); + } + + // CRITICAL FIX: Notify all modules that state has been restored + this._syncModulesAfterStateRestore(); + + return true; + } catch (error) { + console.error('Error restoring progress:', error); + return false; + } + } + + /** + * Synchronize all modules after state restore to prevent timing issues + * This ensures modules reflect the updated state properly + * @private + */ + _syncModulesAfterStateRestore() { + try { + console.log('šŸ”„ Synchronizing modules after state restore...'); + + // Sync image processor if available + if (window.globalImageProcessor && window.state.imageData) { + // Force refresh image processor state + if (typeof window.globalImageProcessor.updateState === 'function') { + window.globalImageProcessor.updateState(window.state); + } + } + + // Sync overlay manager if available + if (window.globalOverlayManager) { + // Force refresh overlay state - this ensures overlay knows about the new state + if (typeof window.globalOverlayManager.updateState === 'function') { + window.globalOverlayManager.updateState(window.state); + } + } + + // Extract available colors if not already set (required for color palette functionality) + if (!window.state.availableColors || window.state.availableColors.length === 0) { + console.log('šŸŽØ Extracting available colors for save file...'); + try { + const availableColors = this.extractAvailableColors(); + if (availableColors && availableColors.length > 0) { + window.state.availableColors = availableColors; + window.state.colorsChecked = true; + console.log(`āœ… Extracted ${availableColors.length} available colors`); + } else { + console.warn('āš ļø No colors available - this might cause issues with overlay display'); + } + } catch (error) { + console.warn('āš ļø Failed to extract colors:', error); + } + } + + // Force window.state reference update for any stale references + window.state = window.state; // Trigger any watchers + + console.log('āœ… Module synchronization complete'); + } catch (error) { + console.warn('āš ļø Module synchronization failed:', error); + } + } + + /** + * Force navigation to saved position to display overlay immediately + * @param {Object} region - The region coordinates + * @param {Object} position - The pixel position within region + * @private + */ + async _forceNavigateToSavedPosition(region, position) { + try { + console.log(`🧭 Forcing navigation to region (${region.x}, ${region.y}) pixel (${position.x}, ${position.y})...`); + + // Calculate world coordinates (region * 1000 + pixel position) + const worldX = region.x * 1000 + position.x; + const worldY = region.y * 1000 + position.y; + + console.log(`šŸŒ World coordinates: (${worldX}, ${worldY})`); + + // Method 1: Try to find WPlace's camera/viewport controls + const attemptCameraControl = () => { + // Look for WPlace's camera control functions in global scope + const possibleControls = [ + 'camera', 'viewport', 'map', 'game', 'wplace', 'app' + ]; + + for (const control of possibleControls) { + if (window[control]) { + console.log(`šŸŽÆ Found potential control: ${control}`, window[control]); + + // Try common navigation method names + const methods = ['setPosition', 'moveTo', 'panTo', 'setView', 'goTo', 'navigate']; + for (const method of methods) { + if (typeof window[control][method] === 'function') { + console.log(`šŸ“ Attempting ${control}.${method}(${worldX}, ${worldY})`); + try { + window[control][method](worldX, worldY); + return true; + } catch (e) { + console.warn(`āŒ ${control}.${method} failed:`, e); + } + } + } + } + } + return false; + }; + + // Method 2: Try updating URL hash (common for canvas-based apps) + const attemptUrlNavigation = () => { + try { + const newHash = `#${worldX},${worldY}`; + console.log(`šŸ”— Attempting URL navigation: ${newHash}`); + window.location.hash = newHash; + + // Also try alternative formats + setTimeout(() => { + const altHash = `#x=${worldX}&y=${worldY}`; + console.log(`šŸ”— Attempting alternative URL: ${altHash}`); + window.location.hash = altHash; + }, 100); + + return true; + } catch (e) { + console.warn('āŒ URL navigation failed:', e); + return false; + } + }; + + // Method 3: Try triggering scroll/wheel events to force tile loading + const attemptScrollTrigger = () => { + try { + console.log('šŸ–±ļø Attempting scroll trigger to force tile loading...'); + + // Find canvas or main container + const canvas = document.querySelector('canvas') || document.body; + + // Simulate mouse events at target coordinates + ['mousedown', 'mousemove', 'mouseup', 'wheel'].forEach(eventType => { + const event = new MouseEvent(eventType, { + bubbles: true, + clientX: worldX % window.innerWidth, + clientY: worldY % window.innerHeight, + deltaY: eventType === 'wheel' ? 1 : 0 + }); + canvas.dispatchEvent(event); + }); + + return true; + } catch (e) { + console.warn('āŒ Scroll trigger failed:', e); + return false; + } + }; + + // Try all methods + const success = attemptCameraControl() || attemptUrlNavigation() || attemptScrollTrigger(); + + if (success) { + console.log('āœ… Navigation triggered - overlay should display in a moment'); + + // Wait a bit for tiles to load, then show success message + setTimeout(() => { + if (typeof this.showAlert === 'function') { + this.showAlert('āœ… Overlay loaded and positioned automatically!', 'success'); + } + }, 1000); + } else { + console.warn('āš ļø Automatic navigation failed - showing manual instruction'); + if (typeof this.showAlert === 'function') { + this.showAlert(`šŸ“ Navigate to region (${region.x}, ${region.y}) to see your restored overlay`, 'info'); + } + } + + } catch (error) { + console.error('āŒ Failed to force navigation:', error); + if (typeof this.showAlert === 'function') { + this.showAlert(`šŸ“ Navigate to region (${region.x}, ${region.y}) to see your overlay`, 'info'); + } + } + } + + saveProgressToFile() { + try { + const progressData = this.buildProgressData(); + const filename = `wplace-bot-progress-${new Date() + .toISOString() + .slice(0, 19) + .replace(/:/g, '-')}.json`; + this.createFileDownloader(JSON.stringify(progressData, null, 2), filename); + return true; + } catch (error) { + console.error('Error saving to file:', error); + return false; + } + } + + async loadProgressFromFile() { + try { + const data = await this.createFileUploader(); + if (!data || !data.state) { + throw new Error('Invalid file format'); + } + const migrated = this.migrateProgress(data); + const success = this.restoreProgress(migrated); + return success; + } catch (error) { + console.error('Error loading from file:', error); + throw error; + } + } + + async restoreOverlayFromData() { + if (!window.state.imageLoaded || !window.state.imageData || !window.state.startPosition || !window.state.region) { + console.warn('āŒ Missing required data for overlay restoration'); + return false; + } + + try { + console.log('šŸ”„ Restoring overlay from saved data...'); + + // Recreate ImageBitmap from loaded pixel data (match old version exactly) + const imageData = new ImageData( + window.state.imageData.pixels, + window.state.imageData.width, + window.state.imageData.height + ); + + const canvas = new OffscreenCanvas(window.state.imageData.width, window.state.imageData.height); + const ctx = canvas.getContext('2d'); + ctx.putImageData(imageData, 0, 0); + const imageBitmap = await canvas.transferToImageBitmap(); + + // Set up overlay with restored data (use the Auto-Image.js overlayManager instance) + if (window.autoImageOverlayManager) { + console.log('šŸ“‹ Setting image bitmap...'); + await window.autoImageOverlayManager.setImage(imageBitmap); + + console.log('šŸ“ Setting position and region...'); + await window.autoImageOverlayManager.setPosition(window.state.startPosition, window.state.region); + + console.log('ļæ½ Enabling overlay...'); + window.autoImageOverlayManager.enable(); + + // Update overlay button state + const toggleOverlayBtn = document.getElementById('toggleOverlayBtn'); + if (toggleOverlayBtn) { + toggleOverlayBtn.disabled = false; + toggleOverlayBtn.classList.add('active'); + } + + console.log('āœ… Overlay restored from data'); + return true; + } else { + console.error('āŒ autoImageOverlayManager not available'); + return false; + } + } catch (error) { + console.error('āŒ Failed to restore overlay from data:', error); + return false; + } + } + + updateCoordinateUI({ mode, directionControls, snakeControls, blockControls }) { + const isLinear = mode === 'rows' || mode === 'columns'; + const isBlock = mode === 'blocks' || mode === 'shuffle-blocks'; + + if (directionControls) directionControls.style.display = isLinear ? 'block' : 'none'; + if (snakeControls) snakeControls.style.display = isLinear ? 'block' : 'none'; + if (blockControls) blockControls.style.display = isBlock ? 'block' : 'none'; + } + + /** + * Force navigation to saved position to display overlay immediately + * @param {Object} region - The region coordinates + * @param {Object} position - The pixel position within region + * @private + */ + async _forceNavigateToSavedPosition(region, position) { + try { + console.log(`🧭 Forcing navigation to region (${region.x}, ${region.y}) pixel (${position.x}, ${position.y})...`); + + // Calculate world coordinates (region * 1000 + pixel position) + const worldX = region.x * 1000 + position.x; + const worldY = region.y * 1000 + position.y; + + console.log(`šŸŒ World coordinates: (${worldX}, ${worldY})`); + + // Method 1: Try to find WPlace's camera/viewport controls + const attemptCameraControl = () => { + // Look for WPlace's camera control functions in global scope + const possibleControls = [ + 'camera', 'viewport', 'map', 'game', 'wplace', 'app' + ]; + + for (const control of possibleControls) { + if (window[control]) { + console.log(`šŸŽÆ Found potential control: ${control}`, window[control]); + + // Try common navigation method names + const methods = ['setPosition', 'moveTo', 'panTo', 'setView', 'goTo', 'navigate']; + for (const method of methods) { + if (typeof window[control][method] === 'function') { + console.log(`šŸ“ Attempting ${control}.${method}(${worldX}, ${worldY})`); + try { + window[control][method](worldX, worldY); + return true; + } catch (e) { + console.warn(`āŒ ${control}.${method} failed:`, e); + } + } + } + } + } + return false; + }; + + // Method 2: Try updating URL hash (common for canvas-based apps) + const attemptUrlNavigation = () => { + try { + const newHash = `#${worldX},${worldY}`; + console.log(`šŸ”— Attempting URL navigation: ${newHash}`); + window.location.hash = newHash; + + // Also try alternative formats + setTimeout(() => { + const altHash = `#x=${worldX}&y=${worldY}`; + console.log(`šŸ”— Attempting alternative URL: ${altHash}`); + window.location.hash = altHash; + }, 100); + + return true; + } catch (e) { + console.warn('āŒ URL navigation failed:', e); + return false; + } + }; + + // Method 3: Try triggering scroll/wheel events to force tile loading + const attemptScrollTrigger = () => { + try { + console.log('šŸ–±ļø Attempting scroll trigger to force tile loading...'); + + // Find canvas or main container + const canvas = document.querySelector('canvas') || document.body; + + // Simulate mouse events at target coordinates + ['mousedown', 'mousemove', 'mouseup', 'wheel'].forEach(eventType => { + const event = new MouseEvent(eventType, { + bubbles: true, + clientX: worldX % window.innerWidth, + clientY: worldY % window.innerHeight, + deltaY: eventType === 'wheel' ? 1 : 0 + }); + canvas.dispatchEvent(event); + }); + + return true; + } catch (e) { + console.warn('āŒ Scroll trigger failed:', e); + return false; + } + }; + + // Try all methods + const success = attemptCameraControl() || attemptUrlNavigation() || attemptScrollTrigger(); + + if (success) { + console.log('āœ… Navigation triggered - overlay should display in a moment'); + + // Wait a bit for tiles to load, then show success message + setTimeout(() => { + if (typeof this.showAlert === 'function') { + this.showAlert('āœ… Overlay loaded and positioned automatically!', 'success'); + } + }, 1000); + } else { + console.warn('āš ļø Automatic navigation failed - showing manual instruction'); + if (typeof this.showAlert === 'function') { + this.showAlert(`šŸ“ Navigate to region (${region.x}, ${region.y}) to see your restored overlay`, 'info'); + } + } + + } catch (error) { + console.error('āŒ Failed to force navigation:', error); + if (typeof this.showAlert === 'function') { + this.showAlert(`šŸ“ Navigate to region (${region.x}, ${region.y}) to see your overlay`, 'info'); + } + } + } +} + +// Global instance with safety checks +try { + window.globalUtilsManager = new WPlaceUtilsManager(); + console.log('āœ… WPlaceUtilsManager initialized successfully'); +} catch (error) { + console.error('āŒ Failed to initialize WPlaceUtilsManager:', error); +} + +// Additional safety: ensure the instance is available for other scripts +if (!window.globalUtilsManager) { + console.warn('āš ļø globalUtilsManager not available, attempting to reinitialize...'); + setTimeout(() => { + try { + window.globalUtilsManager = new WPlaceUtilsManager(); + console.log('āœ… WPlaceUtilsManager reinitialized successfully'); + } catch (error) { + console.error('āŒ Failed to reinitialize WPlaceUtilsManager:', error); + } + }, 100); +} \ No newline at end of file diff --git a/Extensions/themes/acrylic.css b/Extensions/themes/acrylic.css new file mode 100644 index 0000000..edee14c --- /dev/null +++ b/Extensions/themes/acrylic.css @@ -0,0 +1,883 @@ +/* WPlace Auto-Image Bot - Acrylic Theme */ +/* Beautiful theme with translucent blurred panels */ + +@import url('https://fonts.googleapis.com/css2?family=Inter:ital,opsz,wght@0,14..32,100..900;1,14..32,100..900&display=swap'); + +@font-face { + font-family: 'Apple Color Emoji'; + 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); + 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: 300px; +} + +.wplace-theme-acrylic #wplace-image-bot-container * { + font-size: 15px; + line-height: normal; +} + +.wplace-theme-acrylic .wplace-content { + display: flex; + flex-direction: column; + /* gap: 10px; */ + padding: 10px; +} + +#wplace-image-bot-container.wplace-dragging { + transition: none !important; + box-shadow: none !important; + transform: none !important; +} + +/* Stats */ +.wplace-theme-acrylic #wplace-stats-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); +} + +.wplace-theme-acrylic .wplace-stats { + margin-bottom: 0; +} + +.wplace-theme-acrylic .wplace-colors-section { + border-top: none; + margin: 0; +} + +/* Settings Container */ +.wplace-theme-acrylic #wplace-settings-container { + background: var(--wplace-primary) !important; + color: var(--wplace-text) !important; + border-radius: var(--wplace-radius) !important; + box-shadow: none !important; + backdrop-filter: var(--wplace-backdrop) !important; + border: var(--wplace-border-width) var(--wplace-border-style) var(--wplace-border-color) !important; + max-width: 400px !important; +} + +.wplace-theme-acrylic .wplace-settings-header { + background: transparent; + padding: 0; +} + +.wplace-theme-acrylic .wplace-settings-header:hover { + background: transparent !important; +} + +.wplace-theme-acrylic .wplace-settings-icon { + animation: none; + font-size: 15px; +} + +.wplace-theme-acrylic .wplace-settings-content { + padding: 10px; + display: flex; + flex-direction: column; + gap: 10px; +} + +.wplace-theme-acrylic .wplace-settings-footer { + border-top: none; + padding: 10px; +} + +.wplace-theme-acrylic .wplace-settings-section { + margin: 0; +} + +.wplace-theme-acrylic .wplace-settings-section-wrapper, +.wplace-theme-acrylic .wplace-overlay-wrapper, +.wplace-theme-acrylic .wplace-batch-controls { + padding: 0 !important; + background: transparent !important; + border: none !important; +} + +/* Headers */ +.wplace-theme-acrylic .wplace-header, +.wplace-theme-acrylic .wplace-settings-title-wrapper { + height: 40px; + padding: 7px 7px 7px 10px; + font-weight: 600; + background: var(--wplace-accent); + color: var(--wplace-highlight); +} + +.wplace-theme-acrylic .wplace-header span, +.wplace-theme-acrylic .wplace-settings-title { + max-width: 150px; + font-size: 15px; + font-weight: 600; + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; +} + +.wplace-theme-acrylic .wplace-header-controls #compactBtn { + display: none; +} + +.wplace-theme-acrylic .wplace-header-btn, +.wplace-theme-acrylic .wplace-settings-close-btn { + background: #2e2e2e; + color: var(--wplace-highlight); + border: none; + font-size: 12px; + border-radius: 4px; + width: 24px; + height: 24px; +} + +.wplace-theme-acrylic .wplace-header-btn i { + font-size: 12px !important; +} + +.wplace-theme-acrylic .wplace-header-btn:hover, +.wplace-theme-acrylic .wplace-settings-close-btn:hover { + background: transparent; + transform: none; +} + +/* Sections */ +.wplace-theme-acrylic .wplace-section { + display: flex; + flex-direction: column; + gap: 8px; + background: transparent; + padding: 0; + border: none; + margin: 0; +} + +.wplace-theme-acrylic .wplace-section-title { + color: var(--wplace-highlight); + text-transform: capitalize; + font-size: 13px; + font-weight: 600; + pointer-events: none; + letter-spacing: unset; +} + +.wplace-theme-acrylic .wplace-section-title i { + display: none; +} + +.wplace-theme-acrylic .wplace-status-section { + background: transparent; + padding: 0; + margin: 0; + border: none; + display: flex; + flex-direction: column; + gap: 8px; +} + +/* Status Styling */ +.wplace-theme-acrylic .wplace-stats { + background: transparent; + padding: 0; + border: none; +} + +.wplace-theme-acrylic .wplace-stat-value { + color: var(--wplace-highlight); +} + +.wplace-theme-acrylic .wplace-status { + border-radius: var(--wplace-radius); + border: none; + box-shadow: none; + padding: 10px; + font-weight: 500; +} + +.wplace-theme-acrylic .status-default { + background: color-mix(in srgb, white 15%, transparent) !important; +} + +.wplace-theme-acrylic .status-success { + background: color-mix(in srgb, var(--wplace-success) 15%, transparent); + color: var(--wplace-success); +} + +.wplace-theme-acrylic .status-error { + background: color-mix(in srgb, var(--wplace-error) 15%, transparent); + color: var(--wplace-error); +} + +.wplace-theme-acrylic .status-warning { + background: color-mix(in srgb, var(--wplace-warning) 15%, transparent); + color: var(--wplace-warning); +} + +/* Buttons */ +.wplace-theme-acrylic .wplace-btn { + border-radius: 8px; + border: none; + height: max-content; + padding: 10px; + gap: 5px; + flex-direction: column; + align-items: flex-start; + line-height: normal; + text-align: left; + font-size: 15px !important; + box-shadow: none; +} + +.wplace-theme-acrylic .wplace-btn::before { + display: none; +} + +.wplace-theme-acrylic .wplace-btn:disabled { + box-shadow: none !important; +} + +.wplace-theme-acrylic .wplace-btn:hover:not(:disabled) { + transform: none; + animation: none; + box-shadow: none !important; + filter: brightness(0.9); +} + +.wplace-theme-acrylic .wplace-btn-primary, +.wplace-theme-acrylic .wplace-btn-upload { + background: white !important; + color: black !important; + border: none !important; +} + +.wplace-theme-acrylic .wplace-btn-upload:hover:not(:disabled) { + background: revert !important; +} + +.wplace-theme-acrylic .wplace-btn-secondary, +.wplace-theme-acrylic .wplace-btn-select, +.wplace-theme-acrylic .wplace-btn-file, +.wplace-theme-acrylic .wplace-btn-overlay { + background: var(--wplace-secondary); +} + +.wplace-theme-acrylic .wplace-btn-start { + background: var(--wplace-success); + color: white; +} + +.wplace-theme-acrylic .wplace-btn-stop { + background: var(--wplace-error); + color: white; +} + +.wplace-theme-acrylic .wplace-btn.active { + background: white; + color: black; + box-shadow: none !important; +} + +.wplace-theme-acrylic .wplace-btn.active i { + filter: none; +} + +.wplace-theme-acrylic .wplace-settings-apply-btn { + background: white; + color: black; + border-radius: 8px; +} + +/* Compact Input Buttons */ +.wplace-theme-acrylic .wplace-input-btn-small, +.wplace-theme-acrylic .wplace-input-btn-small:hover, +.wplace-theme-acrylic .wplace-input-btn-small:active { + background: white; + color: black; + border: none; + transform: none; + border-radius: 4px; + box-shadow: none; + font-weight: 500; + width: 25px; + height: 25px; +} + +/* Alerts */ +.wplace-theme-acrylic .wplace-alert-base { + border-radius: var(--wplace-radius); + backdrop-filter: var(--wplace-backdrop); + border: none; + box-shadow: none; +} + +.wplace-theme-acrylic .wplace-alert-info { + background: color-mix(in srgb, #0000e5 50%, transparent); +} + +.wplace-theme-acrylic .wplace-alert-success { + background: color-mix(in srgb, var(--wplace-success) 50%, transparent); +} + +.wplace-theme-acrylic .wplace-alert-error { + background: color-mix(in srgb, var(--wplace-error) 50%, transparent); +} + +.wplace-theme-acrylic .wplace-alert-warning { + background: color-mix(in srgb, var(--wplace-warning) 50%, transparent); +} + +/* Progress Bar */ +.wplace-theme-acrylic .wplace-progress { + background: var(--wplace-secondary); + border-radius: 4px; + margin: 0; + border: none; +} + +.wplace-theme-acrylic .wplace-progress-bar { + background: white; +} + +.wplace-theme-acrylic .wplace-progress-bar::after { + display: none; +} + +/* Form Controls */ +.wplace-theme-acrylic .wplace-settings-checkbox, +.wplace-theme-acrylic .wplace-notification-checkbox, +.wplace-theme-acrylic .wplace-speed-checkbox { + cursor: pointer; + width: 20px; + height: 20px; + flex-shrink: 0; + accent-color: var(--wplace-highlight); +} + +.wplace-theme-acrylic .wplace-settings-select, +.wplace-theme-acrylic .wplace-settings-number-input, +.wplace-theme-acrylic .wplace-cooldown-input, +.wplace-theme-acrylic .wplace-notification-interval-input { + background: rgb(255 255 255 / 10%); + color: white; + border: 1px solid rgb(255 255 255 / 25%); + border-radius: 8px; + box-shadow: none; + transition-duration: 0.3s; +} + +.wplace-theme-acrylic .wplace-settings-select:hover, +.wplace-theme-acrylic .wplace-settings-number-input:hover, +.wplace-theme-acrylic .wplace-cooldown-input:hover, +.wplace-theme-acrylic .wplace-notification-interval-input:hover { + border-color: rgb(255 255 255 / 50%); +} + +.wplace-theme-acrylic .wplace-settings-select:focus, +.wplace-theme-acrylic .wplace-settings-number-input:focus, +.wplace-theme-acrylic .wplace-cooldown-input:focus, +.wplace-theme-acrylic .wplace-notification-interval-input:focus { + border-color: white; +} + +.wplace-theme-acrylic .wplace-settings-option { + background: #2d3748; + color: white; +} + +.wplace-theme-acrylic .wplace-settings-select option { + background: #2d3748; + color: white; +} + +.wplace-theme-acrylic .wplace-cooldown-input { + border-radius: 4px !important; +} + +.wplace-theme-acrylic .wplace-cooldown-input:focus { + background: rgb(255 255 255 / 10%); + box-shadow: none; +} + +.wplace-theme-acrylic .wplace-overlay-opacity-value { + background-color: var(--wplace-secondary) !important; + border: none !important; +} + +/* Sliders */ +.wplace-theme-acrylic .wplace-slider, +.wplace-theme-acrylic .wplace-speed-slider, +.wplace-theme-acrylic .wplace-overlay-opacity-slider { + background: var(--wplace-secondary) !important; + border-radius: 3px !important; + height: 6px !important; +} + +.wplace-theme-acrylic .wplace-slider::-webkit-slider-thumb, +.wplace-theme-acrylic .resize-slider::-webkit-slider-thumb, +.wplace-theme-acrylic .wplace-speed-slider::-webkit-slider-thumb, +.wplace-theme-acrylic .wplace-overlay-opacity-slider::-webkit-slider-thumb { + width: 16px !important; + height: 16px !important; + border-radius: 4px !important; + background: white !important; + box-shadow: none !important; + transform: none !important; +} + +.wplace-theme-acrylic .wplace-speed-value { + background: linear-gradient(135deg, #4facfe 0%, #00f2fe 100%); + border-radius: 8px; + color: white; + box-shadow: 0 3px 10px rgb(79 172 254 / 30%); + border: 1px solid rgb(255 255 255 / 20%); +} + +.wplace-theme-acrylic .resize-slider { + -webkit-appearance: revert; +} + +/* Layout Controls */ +.wplace-theme-acrylic .wplace-slider-container { + display: flex; + flex-direction: row; + align-items: center; + gap: 8px; + margin: 0; +} + +.wplace-theme-acrylic .wplace-input-label-compact { + color: rgba(255, 255, 255, 0.75); + margin-left: 4px; + text-transform: lowercase; + white-space: nowrap; +} + +.wplace-theme-acrylic .wplace-cooldown-control { + margin: 0; + display: flex; + flex-direction: column; + gap: 8px; +} + +.wplace-theme-acrylic .wplace-cooldown-control label { + font-weight: 400; + margin-bottom: 0; + display: block; +} + +.wplace-theme-acrylic .wplace-cooldown-input-group { + margin: 0; + background: transparent; +} + +/* Color Elements */ +.wplace-theme-acrylic .wplace-color-item-name { + color: #ccc; +} + +.wplace-theme-acrylic .wplace-color-swatch { + border: 1px solid rgb(255 255 255 / 20%); + border-radius: 4px; +} + +.wplace-theme-acrylic .wplace-color-swatch:not(.active) { + filter: grayscale(80%); +} + +.wplace-theme-acrylic .wplace-color-swatch.unavailable { + border-color: #666; +} + +.wplace-theme-acrylic .wplace-color-swatch.unavailable:not(.active) { + filter: grayscale(90%); +} + +.wplace-theme-acrylic .wplace-color-swatch.active::after { + text-shadow: 0 0 3px black; +} + +.wplace-theme-acrylic .wplace-stat-color-swatch { + border-radius: 3px; + border: 1px solid rgb(255 255 255 / 10%); + box-shadow: inset 0 0 2px rgb(0 0 0 / 50%); +} + +.wplace-theme-acrylic .wplace-stat-colors-grid { + background: var(--wplace-secondary); + border-radius: 8px; +} + +.wplace-theme-acrylic .wplace-color-divider { + background: rgb(255 255 255 / 10%); +} + +/* Icons */ +.wplace-theme-acrylic .wplace-icon-key, +.wplace-theme-acrylic .wplace-icon-robot, +.wplace-theme-acrylic .wplace-icon-speed, +.wplace-theme-acrylic .wplace-icon-bell, +.wplace-theme-acrylic .wplace-icon-palette, +.wplace-theme-acrylic .wplace-icon-globe, +.wplace-theme-acrylic .wplace-icon-paint, +.wplace-theme-acrylic .wplace-icon-eye, +.wplace-theme-acrylic .wplace-icon-compass, +.wplace-theme-acrylic .wplace-icon-route, +.wplace-theme-acrylic .wplace-icon-table, +.wplace-theme-acrylic .wplace-icon-dice { + color: white; +} + +/* Typography */ +.wplace-theme-acrylic .wplace-cooldown-unit { + display: none; +} + +.wplace-theme-acrylic .wplace-settings-toggle-title, +.wplace-theme-acrylic .wplace-overlay-opacity-label { + font-size: 15px !important; +} + +.wplace-theme-acrylic .wplace-settings-description, +.wplace-theme-acrylic .wplace-random-batch-description, +.wplace-theme-acrylic .wplace-settings-toggle-description { + font-size: 12px; + color: rgb(255 255 255 / 75%) !important; +} + +/* Turnstile Overlay */ +.wplace-theme-acrylic .wplace-turnstile-overlay { + background: var(--wplace-primary); + border-radius: var(--wplace-radius); + box-shadow: none; + backdrop-filter: var(--wplace-backdrop); + border: none; + padding: 16px !important; + color: var(--wplace-text) !important; +} + +.wplace-theme-acrylic .wplace-turnstile-hide-btn { + border-radius: 8px !important; + backdrop-filter: var(--wplace-backdrop) !important; + background: var(--wplace-primary) !important; + top: -25px !important; + right: 0 !important; + transition: all 0.3s ease !important; +} + +.wplace-theme-acrylic .wplace-turnstile-hide-btn:hover { + background: white !important; + color: black !important; +} + +/* Resize Tools */ +.wplace-theme-acrylic .resize-tools button { + border-radius: 6px; + border: 1px solid rgb(255 255 255 / 20%); + background: rgb(255 255 255 / 6%); + color: #fff; +} + +.wplace-theme-acrylic .resize-overlay { + background: rgb(0 0 0 / 50%); +} + +/* TODO: More resize menu styles */ +.wplace-theme-acrylic .resize-container { + background: var(--wplace-secondary); + backdrop-filter: var(--wplace-backdrop); + border: none; + border-radius: var(--wplace-radius); + color: var(--wplace-text); +} + +/* Account switching UI styles */ +.wplace-theme-acrylic .wplace-section { + margin: 5px; + border-radius: var(--wplace-radius); + background: rgba(255, 255, 255, 0.08); + padding: 5px; + border: 1px solid rgba(255, 255, 255, 0.15); + backdrop-filter: var(--wplace-backdrop); +} + +.wplace-theme-acrylic .wplace-section-title { + display: flex; + align-items: center; + gap: 8px; + font-weight: 600; + color: var(--wplace-text); + margin: 2px; + font-size: 14px; +} + +/* Auto-swap toggle switch */ +.wplace-theme-acrylic .wplace-switch { + position: relative; + display: inline-block; + width: 50px; + height: 24px; +} + +.wplace-theme-acrylic .wplace-switch input { + opacity: 0; + width: 0; + height: 0; +} + +.wplace-theme-acrylic .wplace-slider-round { + position: absolute; + cursor: pointer; + top: 0; + left: 0; + right: 0; + bottom: 0; + background-color: rgba(255, 255, 255, 0.2); + transition: 0.4s; + border-radius: 24px; + backdrop-filter: blur(10px); +} + +.wplace-theme-acrylic .wplace-slider-round:before { + position: absolute; + content: ""; + height: 18px; + width: 18px; + left: 3px; + bottom: 3px; + background-color: white; + transition: 0.4s; + border-radius: 50%; + box-shadow: 0 2px 8px rgba(0, 0, 0, 0.3); +} + +.wplace-theme-acrylic .wplace-switch input:checked+.wplace-slider-round { + background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); +} + +.wplace-theme-acrylic .wplace-switch input:checked+.wplace-slider-round:before { + transform: translateX(26px); +} + +/* Account list container */ +.wplace-theme-acrylic .accounts-list-container { + max-height: 500px; + overflow-y: auto; + /* margin-top: 10px; */ + padding: 0 6px; +} + +/* Individual account item */ +.wplace-theme-acrylic .wplace-account-item { + display: flex; + justify-content: space-between; + align-items: center; + padding: 6px 8px; + margin: 4px 0; + background: rgba(255, 255, 255, 0.05); + border-radius: 8px; + border: 1px solid rgba(255, 255, 255, 0.15); + transition: all 0.3s ease; + backdrop-filter: blur(10px); + position: relative; +} + +/* Account ordering number */ +.wplace-theme-acrylic .wplace-account-number { + position: absolute; + top: 3px; + left: 3px; + width: 16px; + height: 16px; + background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); + color: white; + border-radius: 50%; + display: flex; + align-items: center; + justify-content: center; + font-size: 9px; + font-weight: bold; + z-index: 1; + box-shadow: 0 2px 4px rgba(0, 0, 0, 0.3); +} + +.wplace-theme-acrylic .wplace-account-item:hover { + background: rgba(255, 255, 255, 0.12); + border-color: rgba(255, 255, 255, 0.3); + transform: translateY(-1px); + box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15); +} + +.wplace-theme-acrylic .wplace-account-item.is-current { + background: linear-gradient(135deg, rgba(102, 126, 234, 0.3) 0%, rgba(118, 75, 162, 0.3) 100%); + border-color: #667eea; + box-shadow: 0 0 20px rgba(102, 126, 234, 0.4); +} + +/* Account details */ +.wplace-theme-acrylic .wplace-account-details { + flex: 1; + margin-left: 25px; +} + +.wplace-theme-acrylic .wplace-account-name { + font-weight: 600; + color: var(--wplace-text); + font-size: 13px; + margin-bottom: 2px; +} + +/* Account statistics */ +.wplace-theme-acrylic .wplace-account-stats { + display: flex; + gap: 12px; + align-items: center; + font-size: 12px; + color: rgba(255, 255, 255, 0.75); +} + +.wplace-theme-acrylic .wplace-account-stats i { + margin-right: 4px; +} + +.wplace-theme-acrylic .wplace-account-stats .fas.fa-bolt { + color: #ffd700; +} + +.wplace-theme-acrylic .wplace-account-stats .fas.fa-tint { + color: #00bfff; +} + +/* Move Artwork Panel Styles for Acrylic Theme */ +.wplace-theme-acrylic .wplace-move-panel { + position: fixed; + top: 50%; + left: 50%; + transform: translate(-50%, -50%); + background: var(--wplace-primary); + border: var(--wplace-border-width) var(--wplace-border-style) var(--wplace-border-color); + border-radius: var(--wplace-radius); + padding: 24px; + z-index: 10000; + backdrop-filter: var(--wplace-backdrop); + min-width: 240px; + text-align: center; + font-family: var(--wplace-font); + box-shadow: 0 20px 50px rgba(0, 0, 0, 0.15); +} + +.wplace-theme-acrylic .wplace-move-panel-title { + color: var(--wplace-text); + margin: 0 0 20px 0; + font-size: 18px; + font-weight: 600; + letter-spacing: 0.5px; +} + +.wplace-theme-acrylic .wplace-move-controls { + display: grid; + grid-template-columns: 1fr 1fr 1fr; + gap: 12px; + margin-bottom: 20px; +} + +.wplace-theme-acrylic .wplace-move-btn { + padding: 12px; + background: rgba(255, 255, 255, 0.2); + color: var(--wplace-text); + border: 1px solid rgba(255, 255, 255, 0.3); + border-radius: 12px; + cursor: pointer; + font-family: var(--wplace-font); + font-size: 16px; + font-weight: 600; + transition: all 0.3s ease; + backdrop-filter: blur(10px); +} + +.wplace-theme-acrylic .wplace-move-btn:hover { + background: rgba(255, 255, 255, 0.3); + border-color: rgba(255, 255, 255, 0.5); + transform: translateY(-2px); + box-shadow: 0 8px 20px rgba(0, 0, 0, 0.1); +} + +.wplace-theme-acrylic .wplace-move-btn:active { + transform: translateY(0); + background: rgba(255, 255, 255, 0.4); +} + +.wplace-theme-acrylic .wplace-move-center { + color: var(--wplace-text); + padding: 12px; + font-size: 14px; + font-weight: 600; + display: flex; + align-items: center; + justify-content: center; + opacity: 0.8; +} + +.wplace-theme-acrylic .wplace-move-close-btn { + background: rgba(255, 59, 48, 0.8); + color: white; + border: 1px solid rgba(255, 59, 48, 0.3); + padding: 12px 20px; + border-radius: 12px; + cursor: pointer; + font-family: var(--wplace-font); + font-size: 15px; + font-weight: 600; + transition: all 0.3s ease; + backdrop-filter: blur(10px); +} + +.wplace-theme-acrylic .wplace-move-close-btn:hover { + background: rgba(255, 59, 48, 1); + transform: translateY(-1px); + box-shadow: 0 8px 20px rgba(255, 59, 48, 0.3); +} + +.wplace-theme-acrylic .wplace-move-overlay { + position: fixed; + top: 0; + left: 0; + width: 100%; + height: 100%; + background: rgba(0, 0, 0, 0.4); + z-index: 9999; + backdrop-filter: blur(8px); +} \ No newline at end of file diff --git a/Extensions/themes/classic-light.css b/Extensions/themes/classic-light.css new file mode 100644 index 0000000..5d2ecff --- /dev/null +++ b/Extensions/themes/classic-light.css @@ -0,0 +1,877 @@ +/* WPlace Auto-Image Bot - Classic Light Theme */ + +/* Clean, bright theme based on classic design with light backgrounds */ + +.wplace-theme-classic-light { + /* === CORE COLORS === */ + --wplace-primary: #E0E0E1; /* Quite white */ + --wplace-secondary: #FBFBFB; /* Light white */ + --wplace-accent: #F3F3F3; /* Darker white */ + --wplace-text: #203C5D; /* Dark blue text */ + --wplace-highlight: #203C5D; /* Dark blue highlight */ + --wplace-success: #28a745; /* Bootstrap green */ + --wplace-error: #dc3545; /* Bootstrap red */ + --wplace-warning: #ffc107; /* Bootstrap yellow */ + + /* === UI PROPERTIES === */ + --wplace-radius: 8px; + --wplace-btn-radius: 12px; + --wplace-border-style: solid; + --wplace-border-width: 1px; + --wplace-border-color: #415a77; + --wplace-shadow: 0 8px 32px rgb(0 0 0 / 15%), 0 0 0 1px rgb(0 0 0 / 8%); + --wplace-backdrop: blur(10px); + --wplace-font: 'Segoe UI', roboto, sans-serif; + + /* === EXTENDED COLOR PALETTE === */ + + /* Icon colors */ + --wplace-icon-primary: #4facfe; /* Light blue */ + --wplace-icon-secondary: #00f2fe; /* Cyan */ + --wplace-icon-palette: #003BFF; /* Pink */ + + /* Additional UI colors */ + --wplace-danger: #dc3545; /* Bootstrap red */ + --wplace-danger-dark: #c82333; /* Darker red */ + --wplace-muted-text: rgb(33 37 41 / 70%); + --wplace-highlight-secondary: #203C5D; + + /* Text variants */ + --wplace-text-secondary: rgb(33 37 41 / 90%); + --wplace-text-muted: rgb(33 37 41 / 70%); + --wplace-text-dim: rgb(33 37 41 / 60%); + --wplace-text-faded: rgb(33 37 41 / 80%); + + /* Background variants */ + --wplace-bg-input: #f3f3f3; /* neutral-100 */ + --wplace-bg-subtle: #f3f3f3; /* neutral-100 — hover */ + --wplace-bg-faint: #e0e0e0; /* neutral-200 */ + --wplace-bg-ghost: #d1d1d1; /* neutral-300 */ + --wplace-bg-whisper: #e0e0e0; /* can be used as whisper on hover */ + + /* Border variants */ + --wplace-border-subtle: #d1d1d1; /* neutral-300 */ + --wplace-border-faint: #e0e0e0; /* neutral-200 */ + --wplace-border-ghost: #e0e0e0; /* neutral-200 */ + --wplace-border-ultra-faint: #e0e0e0; /* neutral-200 — very faint */ + + /* Shadow variants */ + --wplace-shadow-drag: 0 12px 40px rgb(0 0 0 / 20%), 0 0 0 2px rgb(32 60 93 / 30%); + --wplace-shadow-notification: 0 4px 12px rgb(0 0 0 / 15%); + --wplace-shadow-slider-thumb: 0 3px 6px rgb(0 0 0 / 20%), 0 0 0 2px var(--wplace-icon-primary); + --wplace-shadow-slider-hover: 0 4px 8px rgb(0 0 0 / 25%), 0 0 0 3px var(--wplace-icon-primary); + + /* Animation colors */ + --wplace-pulse-start: rgb(40 167 69 / 70%); + --wplace-pulse-mid: rgb(40 167 69 / 0%); + --wplace-pulse-end: rgb(40 167 69 / 0%); + + /* Slider colors */ + --wplace-slider-thumb-bg: #203C5D; + --wplace-slider-track-bg: linear-gradient(to right, #4facfe 0%, #00f2fe 100%); +} + +/* === 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, #E0E0E1 0%, #FBFBFB 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; +} + + +/* Stats container with proper contrast */ +.wplace-theme-classic-light #wplace-stats-container { + background: linear-gradient(135deg, #FBFBFB 0%, #F3F3F3 100%) !important; + border: 1px solid rgb(0 0 0 / 20%) !important; + border-radius: 12px !important; + box-shadow: 0 8px 32px rgb(0 0 0 / 15%) !important; + font-family: 'Segoe UI', Roboto, sans-serif !important; + color: var(--wplace-text) !important; + position: fixed !important; + overflow: hidden !important; + z-index: 9998 !important; +} + +/* Headers with light gradient */ +.wplace-theme-classic-light .wplace-header { + background: linear-gradient(135deg, #FBFBFB 0%, #F3F3F3 100%) !important; + color: var(--wplace-highlight) !important; + border-bottom: 1px solid rgb(0 0 0 / 15%) !important; + text-shadow: none !important; +} + +/* Stats header styling */ +.wplace-theme-classic-light #wplace-stats-container .wplace-header { + background: linear-gradient(135deg, #F3F3F3 0%, #E0E0E1 100%) !important; + color: var(--wplace-text) !important; + border-bottom: 1px solid rgb(0 0 0 / 20%) !important; + text-shadow: none !important; + font-weight: 600 !important; +} + +/* Stats title specific styling */ +.wplace-theme-classic-light #wplace-stats-container .wplace-header .wplace-stats-title { + color: var(--wplace-text) !important; + text-shadow: none !important; +} + +/* Comprehensive text and element styling for light theme - scoped to bot containers only */ + +.wplace-theme-classic-light .wplace-status, +.wplace-theme-classic-light .wplace-stats, +.wplace-theme-classic-light .wplace-section, +.wplace-theme-classic-light .wplace-controls, +.wplace-theme-classic-light .wplace-data-management, +.wplace-theme-classic-light .wplace-cooldown-settings { + color: var(--wplace-text) !important; +} + +/* Buttons with light styling */ +.wplace-theme-classic-light .wplace-btn { + background: linear-gradient(135deg, #FBFBFB 0%, #F3F3F3 100%) !important; + border: 1px solid rgb(0 0 0 / 20%) !important; + border-radius: 12px !important; + color: var(--wplace-text) !important; + font-family: 'Segoe UI', Roboto, sans-serif !important; + font-weight: 500 !important; + transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1) !important; +} + +/* Data management section buttons */ +.wplace-theme-classic-light .wplace-data-management .wplace-btn { + background: linear-gradient(135deg, #FBFBFB 0%, #F3F3F3 100%) !important; + border: 1px solid rgb(0 0 0 / 20%) !important; + color: #203C5D !important; + font-weight: 500 !important; +} + +.wplace-theme-classic-light .wplace-btn:hover:not(:disabled) { + background: linear-gradient(135deg, #F3F3F3 0%, #E0E0E1 100%) !important; + box-shadow: 0 4px 12px rgb(0 0 0 / 15%) !important; + transform: translateY(-1px) !important; +} + +.wplace-theme-classic-light .wplace-data-management .wplace-btn:hover:not(:disabled) { + background: linear-gradient(135deg, #F3F3F3 0%, #E0E0E1 100%) !important; + box-shadow: 0 4px 12px rgb(0 0 0 / 15%) !important; +} + +/* Settings dialog */ +.wplace-theme-classic-light #wplace-settings-container { + background: linear-gradient(135deg, #E0E0E1 0%, #FBFBFB 100%) !important; + border: 1px solid rgb(0 0 0 / 15%) !important; + border-radius: 12px !important; + box-shadow: 0 16px 48px rgb(0 0 0 / 20%), 0 0 0 1px rgb(0 0 0 / 8%) !important; + font-family: 'Segoe UI', Roboto, sans-serif !important; + backdrop-filter: blur(10px) !important; + color: var(--wplace-text) !important; +} + +/* Settings dialog text elements */ +.wplace-theme-classic-light .wplace-stat-colors-grid * { + color: var(--wplace-text) !important; + font-weight: 500 !important; +} + +.wplace-theme-classic-light #wplace-settings-container * { + color: var(--wplace-text) !important; +} + +.wplace-theme-classic-light #wplace-settings-container .wplace-settings-section-wrapper * { + color: var(--wplace-text) !important; +} + +/* Color palette text labels */ +.wplace-theme-classic-light .wplace-color-label { + color: var(--wplace-text) !important; + font-weight: 500 !important; + text-shadow: none !important; +} + +.wplace-theme-classic-light .wplace-color-name { + color: var(--wplace-text) !important; + font-weight: 500 !important; +} + +.wplace-theme-classic-light .wplace-color-item-name { + color: #000 !important; + font-weight: 600 !important; + text-shadow: none !important; +} + +/* Resize dialog */ +.wplace-theme-classic-light .resize-container { + background: linear-gradient(135deg, #E0E0E1 0%, #FBFBFB 100%) !important; + border: 1px solid rgb(0 0 0 / 15%) !important; + border-radius: 12px !important; + box-shadow: 0 16px 48px rgb(0 0 0 / 20%), 0 0 0 1px rgb(0 0 0 / 8%) !important; + font-family: 'Segoe UI', Roboto, sans-serif !important; + backdrop-filter: blur(10px) !important; +} + +/* Settings header */ +.wplace-theme-classic-light .wplace-settings-header { + background: linear-gradient(135deg, #FBFBFB 0%, #F3F3F3 100%) !important; + border-bottom: 1px solid rgb(0 0 0 / 15%) !important; +} + +.wplace-theme-classic-light .wplace-settings-title { + color: var(--wplace-text) !important; + text-shadow: none !important; +} + +.wplace-theme-classic-light .wplace-settings-close-btn { + background: rgb(0 0 0 / 5%) !important; + border: 1px solid rgb(0 0 0 / 20%) !important; + border-radius: 50% !important; + color: var(--wplace-text) !important; + transition: all 0.3s ease !important; +} + +.wplace-theme-classic-light .wplace-settings-close-btn:hover { + background: rgb(220 53 69 / 10%) !important; + border-color: var(--wplace-error) !important; + box-shadow: 0 0 12px rgb(220 53 69 / 30%) !important; +} + +/* Section titles */ +.wplace-theme-classic-light .wplace-section-title { + color: var(--wplace-highlight) !important; + text-shadow: none !important; + font-weight: 600 !important; +} + +/* Button variants with classic light colors */ +.wplace-theme-classic-light .wplace-btn-start { + background: linear-gradient(135deg, #28a745 0%, #20c997 100%) !important; + color: white !important; + font-weight: 600 !important; +} + +.wplace-theme-classic-light .wplace-btn-start:hover:not(:disabled) { + box-shadow: 0 4px 12px rgb(40 167 69 / 40%) !important; +} + +.wplace-theme-classic-light .wplace-btn-stop { + background: linear-gradient(135deg, #dc3545 0%, #c82333 100%) !important; + color: white !important; + font-weight: 600 !important; +} + +.wplace-theme-classic-light .wplace-btn-stop:hover:not(:disabled) { + box-shadow: 0 4px 12px rgb(220 53 69 / 40%) !important; +} + +.wplace-theme-classic-light .wplace-btn-upload { + background: rgb(32 60 93 / 10%) !important; + border: 2px dashed var(--wplace-highlight) !important; + color: var(--wplace-highlight) !important; +} + +.wplace-theme-classic-light .wplace-btn-upload:hover:not(:disabled) { + background: rgb(32 60 93 / 15%) !important; + box-shadow: 0 4px 12px rgb(32 60 93 / 20%) !important; +} + +/* Progress bars with clean light styling */ +.wplace-theme-classic-light .wplace-progress { + background: rgb(0 0 0 / 10%) !important; + border: 1px solid rgb(0 0 0 / 15%) !important; + border-radius: 12px !important; +} + +.wplace-theme-classic-light .wplace-progress-bar { + background: linear-gradient(135deg, #203C5D 0%, #203C5D 100%) !important; + box-shadow: none !important; +} + +.wplace-theme-classic-light .wplace-progress-bar::after { + background: linear-gradient(90deg, transparent, rgb(255 255 255 / 30%), transparent) !important; +} + +/* Status indicators with light colors */ +.wplace-theme-classic-light .status-success { + background: rgb(40 167 69 / 10%) !important; + border-color: var(--wplace-success) !important; + color: var(--wplace-success) !important; + box-shadow: 0 0 15px rgb(40 167 69 / 20%) !important; + text-shadow: none !important; +} + +.wplace-theme-classic-light .status-error { + background: rgb(220 53 69 / 10%) !important; + border-color: var(--wplace-error) !important; + color: var(--wplace-error) !important; + box-shadow: 0 0 15px rgb(220 53 69 / 20%) !important; + text-shadow: none !important; +} + +.wplace-theme-classic-light .status-default { + background: rgb(32 60 93 / 10%) !important; + border-color: var(--wplace-highlight) !important; + color: var(--wplace-highlight) !important; + text-shadow: none !important; +} + +.wplace-theme-classic-light .wplace-stat-label { + color: var(--wplace-text) !important; + text-shadow: none !important; + font-weight: 500 !important; +} + +.wplace-theme-classic-light .wplace-stat-value { + color: var(--wplace-highlight) !important; + text-shadow: none !important; + font-weight: 600 !important; +} + +/* Sections with light styling */ +.wplace-theme-classic-light .wplace-section { + background: rgb(0 0 0 / 3%) !important; + border: 1px solid rgb(0 0 0 / 10%) !important; + border-radius: 12px !important; +} + +.wplace-theme-classic-light .wplace-status-section { + background: rgb(0 0 0 / 3%) !important; + border: 1px solid rgb(0 0 0 / 10%) !important; + border-radius: 12px !important; +} + +.wplace-theme-classic-light .wplace-settings-section-wrapper { + background: rgb(0 0 0 / 3%) !important; + border: 1px solid rgb(0 0 0 / 10%) !important; + border-radius: 12px !important; +} + +/* Form controls with light styling */ +.wplace-theme-classic-light .wplace-settings-select { + background: #E0E0E1 !important; + border: 1px solid rgb(0 0 0 / 20%) !important; + border-radius: 8px !important; + color: var(--wplace-text) !important; + font-family: 'Segoe UI', Roboto, sans-serif !important; + box-shadow: 0 2px 8px rgb(0 0 0 / 10%) !important; +} + +.wplace-theme-classic-light .wplace-settings-select:focus { + border-color: var(--wplace-highlight) !important; + box-shadow: 0 0 0 2px rgb(32 60 93 / 30%) !important; +} + +/* Dropdown menu options */ +.wplace-theme-classic-light .wplace-settings-select option { + background: #E0E0E1 !important; + color: var(--wplace-text) !important; +} + +.wplace-theme-classic-light .wplace-settings-option { + background: #E0E0E1 !important; + color: var(--wplace-text) !important; +} + +/* Sliders with classic gradient */ +.wplace-theme-classic-light .wplace-speed-slider { + background: linear-gradient(to right, #4facfe 0%, #32C8FE 100%) !important; + border-radius: 4px !important; +} + +.wplace-theme-classic-light .wplace-overlay-opacity-slider { + background: linear-gradient(to right, #4facfe 0%, #32C8FE 100%) !important; + border-radius: 4px !important; +} + +.wplace-theme-classic-light .wplace-slider { + background: linear-gradient(to right, #4facfe 0%, #32C8FE 100%) !important; + border-radius: 4px !important; +} + +.wplace-theme-classic-light .wplace-slider::-webkit-slider-thumb { + -webkit-appearance: none; + width: 16px; + height: 16px; + border-radius: 50%; + background: var(--wplace-slider-thumb-bg); + border: 1px solid rgb(0 0 0 / 20%); + cursor: pointer; + transition: all 0.2s ease; + box-shadow: 0 2px 4px rgb(0 0 0 / 20%); +} + +.wplace-theme-classic-light .wplace-speed-value { + background: linear-gradient(135deg, #4facfe 0%, #32C8FE 100%) !important; + border-radius: 8px !important; + color: white !important; + font-weight: 600 !important; + box-shadow: 0 3px 10px rgb(79 172 254 / 30%) !important; + border: 1px solid rgb(0 0 0 / 10%) !important; +} + +/* Settings labels */ +.wplace-theme-classic-light .wplace-settings-section-label { + color: var(--wplace-highlight) !important; + text-shadow: none !important; + font-family: 'Segoe UI', Roboto, sans-serif !important; + font-weight: 600 !important; +} + +/* Icon colors for classic light theme */ +.wplace-theme-classic-light .wplace-icon-key { color: #4facfe; } +.wplace-theme-classic-light .wplace-icon-robot { color: #4facfe; } +.wplace-theme-classic-light .wplace-icon-speed { color: #4facfe; } +.wplace-theme-classic-light .wplace-icon-bell { color: #ffc107; } +.wplace-theme-classic-light .wplace-icon-palette { color: #003BFF; } +.wplace-theme-classic-light .wplace-icon-globe { color: #ffeaa7; } +.wplace-theme-classic-light .wplace-icon-paint { color: #4facfe; } +.wplace-theme-classic-light .wplace-icon-eye { color: #203C5D; } + +/* Clean light theme animations */ +@keyframes light-shimmer { + 0% { + transform: translateX(-100%); + } + + 100% { + transform: translateX(200%); + } +} + +/* Turnstile/CF checkbox overlay - CRITICAL FIX */ +.wplace-theme-classic-light .wplace-turnstile-overlay { + background: rgb(255 255 255 / 98%) !important; + border-radius: 12px !important; + box-shadow: 0 8px 32px rgb(0 0 0 / 30%) !important; + backdrop-filter: blur(10px) !important; + border: 1px solid rgb(0 0 0 / 20%) !important; + color: var(--wplace-text) !important; + font-family: 'Segoe UI', Roboto, sans-serif !important; +} + +.wplace-theme-classic-light .wplace-turnstile-title { + color: var(--wplace-text) !important; +} + +.wplace-theme-classic-light .wplace-turnstile-hide-btn { + color: var(--wplace-text) !important; + border: 1px solid rgb(0 0 0 / 20%) !important; + border-radius: 6px !important; + background: rgb(0 0 0 / 5%) !important; +} + +.wplace-theme-classic-light .wplace-turnstile-hide-btn:hover { + background: rgb(0 0 0 / 10%) !important; +} + +/* Dual control layout styling for classic light theme */ +.wplace-theme-classic-light .wplace-dual-control-compact { + display: flex; + flex-direction: row; + align-items: center; + gap: 12px; + margin: 8px 0; + flex-wrap: wrap; +} + +.wplace-theme-classic-light .wplace-slider-container-compact { + display: flex; + align-items: center; + gap: 8px; + flex: 1; + min-width: 160px; +} + +.wplace-theme-classic-light .wplace-speed-slider-container-compact { + display: flex; + align-items: center; + gap: 8px; + flex: 1; + min-width: 160px; +} + +.wplace-theme-classic-light .wplace-input-group-compact { + display: flex; + align-items: center; + gap: 4px; + flex-shrink: 0; +} + +.wplace-theme-classic-light .wplace-input-btn-compact { + background: linear-gradient(135deg, #4facfe 0%, #32C8FE 100%); + color: white; + border: none; + border-radius: 4px; + width: 22px; + height: 22px; + cursor: pointer; + font-weight: bold; + font-size: 12px; + display: flex; + align-items: center; + justify-content: center; + transition: all 0.2s ease; + box-shadow: 0 2px 4px rgba(0,0,0,0.2); +} + +.wplace-theme-classic-light .wplace-input-btn-compact:hover { + transform: translateY(-1px); + box-shadow: 0 4px 8px rgba(0,0,0,0.3); + background: linear-gradient(135deg, #32C8FE 0%, #4facfe 100%); +} + +.wplace-theme-classic-light .wplace-input-btn-compact:active { + transform: translateY(0); + box-shadow: 0 2px 4px rgba(0,0,0,0.2); +} + +.wplace-theme-classic-light .wplace-number-input-compact { + background: #E0E0E1; + border: 1px solid rgb(0 0 0 / 20%); + border-radius: 4px; + color: var(--wplace-text); + padding: 4px 8px; + font-size: 12px; + width: 65px; + text-align: center; + transition: all 0.2s ease; + height: 22px; +} + +.wplace-theme-classic-light .wplace-number-input-compact:focus { + outline: none; + border-color: #4facfe; + box-shadow: 0 0 0 3px rgba(79, 172, 254, 0.3); + background: #FBFBFB; +} + +.wplace-theme-classic-light .wplace-input-label-compact { + color: var(--wplace-text); + font-size: 11px; + margin-left: 4px; + white-space: nowrap; +} + +.wplace-theme-classic-light .wplace-batch-size-header { + margin-bottom: 8px; +} + +.wplace-theme-classic-light .wplace-batch-size-label { + color: var(--wplace-highlight); + font-weight: 500; + font-size: 13px; +} + +/* Cooldown control styling for classic light theme */ +.wplace-theme-classic-light .wplace-cooldown-control { + margin: 12px 0; +} + +.wplace-theme-classic-light .wplace-cooldown-control label { + color: var(--wplace-highlight); + font-weight: 500; + font-size: 13px; + margin-bottom: 8px; + display: block; +} + +/* Color swatches with light styling */ +.wplace-theme-classic-light .wplace-color-swatch { + border: 1px solid rgb(0 0 0 / 20%) !important; + border-radius: 4px !important; + box-shadow: 0 2px 8px rgb(0 0 0 / 10%) !important; +} + +.wplace-theme-classic-light .wplace-color-swatch:hover { + box-shadow: 0 4px 16px rgb(32 60 93 / 30%) !important; + transform: translateY(-1px); +} + +.wplace-theme-classic-light .wplace-stat-colors-grid { + background: rgb(0 0 0 / 5%) !important; + border-radius: 8px !important; + border: 1px solid rgb(0 0 0 / 10%) !important; +} + +/* Account switching UI styles */ +.wplace-theme-classic-light .wplace-section { + margin: 15px 0; + border-radius: var(--wplace-radius); + background: rgba(0, 0, 0, 0.03); + padding: 15px; + border: 1px solid rgba(0, 0, 0, 0.1); +} + +.wplace-theme-classic-light .wplace-section-title { + display: flex; + align-items: center; + gap: 8px; + font-weight: 600; + color: var(--wplace-text); + margin-bottom: 10px; + font-size: 14px; +} + +/* Auto-swap toggle switch */ +.wplace-theme-classic-light .wplace-switch { + position: relative; + display: inline-block; + width: 50px; + height: 24px; +} + +.wplace-theme-classic-light .wplace-switch input { + opacity: 0; + width: 0; + height: 0; +} + +.wplace-theme-classic-light .wplace-slider-round { + position: absolute; + cursor: pointer; + top: 0; + left: 0; + right: 0; + bottom: 0; + background-color: #ccc; + transition: 0.4s; + border-radius: 24px; +} + +.wplace-theme-classic-light .wplace-slider-round:before { + position: absolute; + content: ""; + height: 18px; + width: 18px; + left: 3px; + bottom: 3px; + background-color: white; + transition: 0.4s; + border-radius: 50%; + box-shadow: 0 2px 4px rgba(0, 0, 0, 0.2); +} + +.wplace-theme-classic-light .wplace-switch input:checked + .wplace-slider-round { + background-color: #4facfe; +} + +.wplace-theme-classic-light .wplace-switch input:checked + .wplace-slider-round:before { + transform: translateX(26px); +} + +/* Account list container */ +.wplace-theme-classic-light .accounts-list-container { + max-height: 200px; + overflow-y: auto; + margin-top: 10px; + padding: 0 6px; +} + +/* Individual account item */ +.wplace-theme-classic-light .wplace-account-item { + display: flex; + justify-content: space-between; + align-items: center; + padding: 6px 8px; + margin: 4px 0; + background: rgba(0, 0, 0, 0.02); + border-radius: 8px; + border: 1px solid rgba(0, 0, 0, 0.08); + transition: all 0.3s ease; + position: relative; +} + +/* Account ordering number */ +.wplace-theme-classic-light .wplace-account-number { + position: absolute; + top: 3px; + left: 3px; + width: 16px; + height: 16px; + background: #4facfe; + color: white; + border-radius: 50%; + display: flex; + align-items: center; + justify-content: center; + font-size: 9px; + font-weight: bold; + z-index: 1; + box-shadow: 0 2px 4px rgba(0, 0, 0, 0.2); +} + +.wplace-theme-classic-light .wplace-account-item:hover { + background: rgba(0, 0, 0, 0.05); + border-color: rgba(0, 0, 0, 0.15); + box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1); +} + +.wplace-theme-classic-light .wplace-account-item.is-current { + background: rgba(79, 172, 254, 0.1); + border-color: #4facfe; + box-shadow: 0 0 10px rgba(79, 172, 254, 0.2); +} + +/* Account details */ +.wplace-theme-classic-light .wplace-account-details { + flex: 1; + margin-left: 20px; + min-width: 0; +} + +.wplace-theme-classic-light .wplace-account-name { + font-weight: 600; + color: var(--wplace-text); + font-size: 12px; + margin-bottom: 1px; + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; +} + +/* Account statistics */ +.wplace-theme-classic-light .wplace-account-stats { + display: flex; + gap: 8px; + align-items: center; + font-size: 11px; + color: rgba(0, 0, 0, 0.6); + flex-shrink: 0; +} + +.wplace-theme-classic-light .wplace-account-stats i { + margin-right: 4px; +} + +.wplace-theme-classic-light .wplace-account-stats .fas.fa-bolt { + color: #ff9800; +} + +.wplace-theme-classic-light .wplace-account-stats .fas.fa-tint { + color: #2196f3; +} + +/* Move Artwork Panel Styles for Classic Light Theme */ +.wplace-theme-classic-light .wplace-move-panel { + position: fixed; + top: 50%; + left: 50%; + transform: translate(-50%, -50%); + background: var(--wplace-primary); + border: 2px solid var(--wplace-border-color); + border-radius: var(--wplace-radius); + padding: 20px; + z-index: 10000; + box-shadow: var(--wplace-shadow); + min-width: 200px; + text-align: center; + backdrop-filter: var(--wplace-backdrop); + font-family: var(--wplace-font); +} + +.wplace-theme-classic-light .wplace-move-panel-title { + color: var(--wplace-text); + margin: 0 0 15px 0; + font-size: 16px; + font-weight: 600; +} + +.wplace-theme-classic-light .wplace-move-controls { + display: grid; + grid-template-columns: 1fr 1fr 1fr; + gap: 8px; + margin-bottom: 15px; +} + +.wplace-theme-classic-light .wplace-move-btn { + padding: 10px; + background: linear-gradient(135deg, #4CAF50 0%, #45a049 100%); + color: white; + border: 1px solid var(--wplace-border-color); + border-radius: var(--wplace-btn-radius); + cursor: pointer; + font-family: var(--wplace-font); + font-size: 14px; + transition: all 0.2s ease; + box-shadow: 0 2px 4px rgb(0 0 0 / 10%); +} + +.wplace-theme-classic-light .wplace-move-btn:hover { + background: linear-gradient(135deg, #45a049 0%, #4CAF50 100%); + transform: translateY(-1px); + box-shadow: 0 4px 8px rgb(0 0 0 / 15%); +} + +.wplace-theme-classic-light .wplace-move-btn:active { + transform: translateY(0); + box-shadow: 0 2px 4px rgb(0 0 0 / 10%); +} + +.wplace-theme-classic-light .wplace-move-center { + color: var(--wplace-highlight); + padding: 10px; + font-size: 12px; + font-weight: bold; + display: flex; + align-items: center; + justify-content: center; +} + +.wplace-theme-classic-light .wplace-move-close-btn { + background: linear-gradient(135deg, #f44336 0%, #d32f2f 100%); + color: white; + border: 1px solid var(--wplace-border-color); + padding: 10px 16px; + border-radius: var(--wplace-btn-radius); + cursor: pointer; + font-family: var(--wplace-font); + transition: all 0.2s ease; + box-shadow: 0 2px 4px rgb(0 0 0 / 10%); +} + +.wplace-theme-classic-light .wplace-move-close-btn:hover { + background: linear-gradient(135deg, #d32f2f 0%, #f44336 100%); + transform: translateY(-1px); + box-shadow: 0 4px 8px rgb(0 0 0 / 15%); +} + +.wplace-theme-classic-light .wplace-move-overlay { + position: fixed; + top: 0; + left: 0; + width: 100%; + height: 100%; + background: rgba(0, 0, 0, 0.5); + z-index: 9999; + backdrop-filter: blur(4px); +} + + + + + + diff --git a/Extensions/themes/classic.css b/Extensions/themes/classic.css new file mode 100644 index 0000000..8aca765 --- /dev/null +++ b/Extensions/themes/classic.css @@ -0,0 +1,1028 @@ +/* WPlace Auto-Image Bot - Classic Theme */ +/* Clean, modern UI with gradients and subtle effects */ + +.wplace-theme-classic { + --wplace-primary: linear-gradient(135deg, #000 0%, #1a1a1a 100%); + --wplace-secondary: linear-gradient(135deg, #111 0%, #2a2a2a 100%); + --wplace-accent: #222; + --wplace-text: #fff; + --wplace-highlight: #203C5D; + --wplace-success: #0f0; + --wplace-error: #f00; + --wplace-warning: #fa0; + --wplace-radius: 12px; + --wplace-btn-radius: 16px; + --wplace-shadow: 0 8px 32px rgb(0 0 0 / 60%), 0 0 0 1px rgb(255 255 255 / 10%); + --wplace-backdrop: blur(10px); + --wplace-font: 'Segoe UI', roboto, sans-serif; + --wplace-scanline: 0; + --wplace-pixel-blink: 0; + + /* Icon colors */ + --wplace-icon-primary: #4facfe; + --wplace-icon-secondary: #00f2fe; + --wplace-icon-palette: #f093fb; + + /* Additional UI colors */ + --wplace-danger: #ff6a6a; + --wplace-danger-dark: #ff4757; + --wplace-muted-text: #fffB; + --wplace-highlight-secondary: #203C5D; + + /* Slider colors */ + --wplace-slider-track-bg: linear-gradient(to right, #4facfe 0%, #00f2fe 100%); +} + +/* 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); + 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); +} + +:root #wplace-stats-container, +.wplace-theme-classic #wplace-stats-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); +} + +:root #wplace-settings-container, +.wplace-theme-classic #wplace-settings-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); +} + +:root .wplace-header, +.wplace-theme-classic .wplace-header { + background: var(--wplace-secondary); + color: var(--wplace-highlight); + text-shadow: 0 1px 2px rgb(0 0 0 / 50%); +} + +:root .wplace-section-title, +.wplace-theme-classic .wplace-section-title { + color: var(--wplace-highlight); +} + +:root .wplace-stat-value, +.wplace-theme-classic .wplace-stat-value { + color: var(--wplace-highlight); +} + +:root .wplace-status-section, +.wplace-theme-classic .wplace-status-section { + border-radius: var(--wplace-radius); + background: rgb(255 255 255 / 3%); + border: 1px solid rgb(255 255 255 / 10%); +} + +:root .wplace-section, +.wplace-theme-classic .wplace-section { + border-radius: var(--wplace-radius); + background: rgb(255 255 255 / 3%); +} + +:root .wplace-stats, +.wplace-theme-classic .wplace-stats { + border-radius: var(--wplace-radius); + background: rgb(255 255 255 / 3%); + border: 1px solid rgb(255 255 255 / 10%); +} + +:root .wplace-status, +.wplace-theme-classic .wplace-status { + border-radius: var(--wplace-radius); +} + +:root .wplace-alert-base, +.wplace-theme-classic .wplace-alert-base { + border-radius: var(--wplace-radius); + font-family: var(--wplace-font); +} + +:root .wplace-settings-container-base, +.wplace-theme-classic .wplace-settings-container-base { + font-family: var(--wplace-font); + border-radius: var(--wplace-radius); + background: var(--wplace-primary); + color: var(--wplace-text); + box-shadow: var(--wplace-shadow); + backdrop-filter: var(--wplace-backdrop); +} + +/* Button styling for classic theme */ +:root .wplace-btn, +.wplace-theme-classic .wplace-btn { + background: linear-gradient(135deg, #222 0%, #4a4a4a 100%); + border: 1px solid rgb(255 255 255 / 10%); + color: var(--wplace-text); + border-radius: var(--wplace-btn-radius); + font-family: var(--wplace-font); +} + +:root .wplace-btn::before, +.wplace-theme-classic .wplace-btn::before { + background: linear-gradient(90deg, transparent, rgb(255 255 255 / 10%), transparent); +} + +:root .wplace-btn:disabled, +.wplace-theme-classic .wplace-btn:disabled { + box-shadow: none !important; +} + +:root .wplace-btn:hover:not(:disabled), +.wplace-theme-classic .wplace-btn:hover:not(:disabled) { + box-shadow: 0 4px 12px rgb(0 0 0 / 40%); +} + +:root .wplace-btn-overlay.active, +.wplace-theme-classic .wplace-btn-overlay.active { + background: linear-gradient(135deg, #29b6f6 0%, #8e2de2 100%); + box-shadow: 0 0 15px #8e2de2; +} + +/* Button variants - exact upstream main colors */ +:root .wplace-btn-primary, +.wplace-theme-classic .wplace-btn-primary { + background: linear-gradient(135deg, #222 0%, #6a5acd 100%); + color: var(--wplace-text); +} + +:root .wplace-btn-upload, +.wplace-theme-classic .wplace-btn-upload { + background: linear-gradient(135deg, #111 0%, #4a4a4a 100%); + color: var(--wplace-text); + border: 1px dashed var(--wplace-highlight) !important; +} + +:root .wplace-btn-start, +.wplace-theme-classic .wplace-btn-start { + background: linear-gradient(135deg, var(--wplace-success) 0%, #228b22 100%); + color: white; +} + +:root .wplace-btn-stop, +.wplace-theme-classic .wplace-btn-stop { + background: linear-gradient(135deg, var(--wplace-error) 0%, #dc143c 100%); + color: white; +} + +:root .wplace-btn-select, +.wplace-theme-classic .wplace-btn-select { + background: linear-gradient(135deg, var(--wplace-highlight) 0%, #9370db 100%); + color: white; +} + +:root .wplace-btn-file, +.wplace-theme-classic .wplace-btn-file { + background: linear-gradient(135deg, #ff8c00 0%, #ff7f50 100%); + color: white; +} + +:root .wplace-btn.active, +:root .wplace-btn[aria-pressed="true"], +.wplace-theme-classic .wplace-btn.active, +.wplace-theme-classic .wplace-btn[aria-pressed="true"] { + background: var(--wplace-highlight) !important; + color: #000 !important; + border-color: var(--wplace-text) !important; + box-shadow: 0 0 8px rgb(0 0 0 / 25%) inset, 0 0 6px rgb(0 0 0 / 20%) !important; +} + +:root .wplace-btn.active i, +:root .wplace-btn[aria-pressed="true"] i, +.wplace-theme-classic .wplace-btn.active i, +.wplace-theme-classic .wplace-btn[aria-pressed="true"] i { + filter: drop-shadow(0 0 3px #000); +} + +:root .mask-mode-group .wplace-btn.active, +:root .mask-mode-group .wplace-btn[aria-pressed="true"], +.wplace-theme-classic .mask-mode-group .wplace-btn.active, +.wplace-theme-classic .mask-mode-group .wplace-btn[aria-pressed="true"] { + background: var(--wplace-highlight); + color: #000; + border-color: var(--wplace-text); + box-shadow: 0 0 8px rgb(0 0 0 / 25%) inset, 0 0 6px rgb(0 0 0 / 20%); +} + +/* Status styling for classic theme */ +:root .status-default, +.wplace-theme-classic .status-default { + background: rgb(255 255 255 / 10%); + border-color: var(--wplace-text); + color: var(--wplace-text); +} + +:root .status-success, +.wplace-theme-classic .status-success { + background: rgb(0 255 0 / 10%); + border-color: var(--wplace-success); + color: var(--wplace-success); + box-shadow: 0 0 15px var(--wplace-success); +} + +:root .status-error, +.wplace-theme-classic .status-error { + background: rgb(255 0 0 / 10%); + border-color: var(--wplace-error); + color: var(--wplace-error); + box-shadow: 0 0 15px var(--wplace-error); +} + +:root .status-warning, +.wplace-theme-classic .status-warning { + background: rgb(255 165 0 / 10%); + border-color: var(--wplace-warning); + color: var(--wplace-warning); + box-shadow: 0 0 15px var(--wplace-warning); +} + +:root .wplace-status.status-default, +.wplace-theme-classic .wplace-status.status-default { + color: var(--wplace-text); + background: rgb(255 255 255 / 10%); + border-color: var(--wplace-text); +} + +/* Alert styling for classic theme */ +:root .wplace-alert-info, +.wplace-theme-classic .wplace-alert-info { + background: linear-gradient(135deg, #3498db, #2980b9); + box-shadow: 0 4px 12px rgb(52 152 219 / 30%); +} + +:root .wplace-alert-success, +.wplace-theme-classic .wplace-alert-success { + background: linear-gradient(135deg, #27ae60, #229954); + box-shadow: 0 4px 12px rgb(39 174 96 / 30%); +} + +:root .wplace-alert-warning, +.wplace-theme-classic .wplace-alert-warning { + background: linear-gradient(135deg, #f39c12, #e67e22); + box-shadow: 0 4px 12px rgb(243 156 18 / 30%); +} + +:root .wplace-alert-error, +.wplace-theme-classic .wplace-alert-error { + background: linear-gradient(135deg, #e74c3c, #c0392b); + box-shadow: 0 4px 12px rgb(231 76 60 / 30%); +} + +/* Progress bar styling for classic theme */ +:root .wplace-progress, +.wplace-theme-classic .wplace-progress { + background: rgb(0 0 0 / 30%); + border: 1px solid rgb(255 255 255 / 10%); + border-radius: var(--wplace-radius); +} + +:root .wplace-progress-bar, +.wplace-theme-classic .wplace-progress-bar { + background: linear-gradient(135deg, var(--wplace-highlight) 0%, #9370db 100%); +} + +:root .wplace-progress-bar::after, +.wplace-theme-classic .wplace-progress-bar::after { + background: linear-gradient(90deg, transparent, rgb(255 255 255 / 30%), transparent); +} + +/* Header and sections styling for classic theme */ +:root .wplace-header-btn, +.wplace-theme-classic .wplace-header-btn { + background: rgb(255 255 255 / 10%); + color: var(--wplace-highlight); + border-radius: 4px; + font-family: var(--wplace-font); +} + +:root .wplace-header-btn:hover, +.wplace-theme-classic .wplace-header-btn:hover { + background: #222; + color: var(--wplace-text); +} + +/* Font and typography for classic theme */ +:root .wplace-turnstile-overlay, +.wplace-theme-classic .wplace-turnstile-overlay { + background: rgb(0 0 0 / 90%) !important; + border-radius: var(--wplace-radius) !important; + box-shadow: 0 8px 32px rgb(0 0 0 / 40%) !important; + backdrop-filter: var(--wplace-backdrop) !important; + border: 1px solid rgb(255 255 255 / 20%) !important; + color: var(--wplace-text) !important; + font-family: var(--wplace-font) !important; +} + +/* Auto light/dark support for classic theme */ +@media (prefers-color-scheme: light) { + :root .theme-auto, + .wplace-theme-classic .theme-auto { + --wplace-primary: #fff; + --wplace-secondary: #f5f5f5; + --wplace-accent: #007acc; + --wplace-text: #203C5D; + } +} + +@media (prefers-color-scheme: dark) { + :root .theme-auto, + .wplace-theme-classic .theme-auto { + --wplace-primary: #1e1e1e; + --wplace-secondary: #203C5D; + --wplace-accent: var(--wplace-highlight); + --wplace-text: #fff; + } +} + +/* Border and color styling for classic theme */ +:root .wplace-color-item-name, +.wplace-theme-classic .wplace-color-item-name { + color: #ccc; +} + +:root .wplace-color-swatch, +.wplace-theme-classic .wplace-color-swatch { + border: 1px solid rgb(255 255 255 / 20%); + border-radius: 4px; +} + +:root .wplace-stat-color-swatch, +.wplace-theme-classic .wplace-stat-color-swatch { + border-radius: 3px; + border: 1px solid rgb(255 255 255 / 10%); + box-shadow: inset 0 0 2px rgb(0 0 0 / 50%); +} + +:root .resize-tools button, +.wplace-theme-classic .resize-tools button { + border-radius: 6px; + border: 1px solid rgb(255 255 255 / 20%); + background: rgb(255 255 255 / 6%); + color: #fff; +} + +:root .resize-slider, +.wplace-theme-classic .resize-slider { + background: #ccc; + border-radius: var(--wplace-radius); +} + +/* Text effects and filters for classic theme */ +:root .wplace-color-swatch:not(.active), +.wplace-theme-classic .wplace-color-swatch:not(.active) { + filter: grayscale(80%); +} + +:root .wplace-color-swatch.unavailable, +.wplace-theme-classic .wplace-color-swatch.unavailable { + border-color: #666; +} + +:root .wplace-color-swatch.unavailable:not(.active), +.wplace-theme-classic .wplace-color-swatch.unavailable:not(.active) { + filter: grayscale(90%); +} + +:root .wplace-color-swatch.active::after, +.wplace-theme-classic .wplace-color-swatch.active::after { + text-shadow: 0 0 3px black; +} + +/* Icon colors for classic theme */ +:root .wplace-icon-key, +.wplace-theme-classic .wplace-icon-key { + color: #4facfe; +} + +:root .wplace-icon-robot, +.wplace-theme-classic .wplace-icon-robot { + color: #4facfe; +} + +:root .wplace-icon-speed, +.wplace-theme-classic .wplace-icon-speed { + color: #4facfe; +} + +:root .wplace-icon-bell, +.wplace-theme-classic .wplace-icon-bell { + color: #ffd166; +} + +:root .wplace-icon-palette, +.wplace-theme-classic .wplace-icon-palette { + color: #f093fb; +} + +:root .wplace-icon-globe, +.wplace-theme-classic .wplace-icon-globe { + color: #ffeaa7; +} + +:root .wplace-icon-paint, +.wplace-theme-classic .wplace-icon-paint { + color: #4facfe; +} + +:root .wplace-icon-eye, +.wplace-theme-classic .wplace-icon-eye { + color: var(--wplace-highlight); +} + +/* Form controls and sliders for classic theme */ +:root .wplace-slider, +.wplace-theme-classic .wplace-slider { + background: linear-gradient(to right, #4facfe 0%, #00f2fe 100%); + border-radius: 4px; +} + +:root .wplace-slider::-webkit-slider-thumb, +.wplace-theme-classic .wplace-slider::-webkit-slider-thumb { + -webkit-appearance: none; + width: 16px; + height: 16px; + border-radius: 50%; + background: var(--wplace-slider-thumb-bg); + border: none; + cursor: pointer; + transition: all 0.2s ease; +} + +:root .wplace-speed-slider, +.wplace-theme-classic .wplace-speed-slider { + background: linear-gradient(to right, #4facfe 0%, #00f2fe 100%); + border-radius: 4px; +} + +:root .wplace-speed-slider::-webkit-slider-thumb, +.wplace-theme-classic .wplace-speed-slider::-webkit-slider-thumb { + border-radius: 50%; + border: 2px solid; +} + +:root .wplace-speed-slider::-moz-range-thumb, +.wplace-theme-classic .wplace-speed-slider::-moz-range-thumb { + border-radius: 50%; + border: 2px solid; +} + +:root .wplace-turnstile-title, +.wplace-theme-classic .wplace-turnstile-title { + color: var(--wplace-text) !important; +} + +:root .wplace-turnstile-hide-btn, +.wplace-theme-classic .wplace-turnstile-hide-btn { + color: var(--wplace-text) !important; + border: 1px solid rgb(255 255 255 / 20%) !important; + border-radius: 6px !important; +} + +:root .wplace-turnstile-hide-btn:hover, +.wplace-theme-classic .wplace-turnstile-hide-btn:hover { + background: rgb(255 255 255 / 10%) !important; +} + +:root .wplace-settings-select, +.wplace-theme-classic .wplace-settings-select { + background: rgb(255 255 255 / 15%); + color: white; + border: 1px solid rgb(255 255 255 / 20%); + border-radius: 8px; + box-shadow: 0 3px 10px rgb(0 0 0 / 10%); +} + +:root .wplace-settings-option, +.wplace-theme-classic .wplace-settings-option { + background: #2d3748; + color: white; +} + +:root .wplace-stat-colors-grid, +.wplace-theme-classic .wplace-stat-colors-grid { + background: rgb(0 0 0 / 20%); + border-radius: 4px; +} + +:root .resize-overlay, +.wplace-theme-classic .resize-overlay { + background: rgb(0 0 0 / 80%); +} + +:root .wplace-overlay, +.wplace-theme-classic .wplace-overlay { + background: rgb(0 0 0 / 80%); +} + +:root .wplace-settings-error, +.wplace-theme-classic .wplace-settings-error { + background: rgb(255 0 0 / 40%) !important; +} + +/* Settings sections and batch controls for classic theme */ +:root .wplace-settings-section-wrapper, +.wplace-theme-classic .wplace-settings-section-wrapper { + background: rgb(255 255 255 / 10%); + border-radius: var(--wplace-radius); + border: 1px solid rgb(255 255 255 / 10%); +} + +:root .wplace-batch-controls, +.wplace-theme-classic .wplace-batch-controls { + background: rgb(255 255 255 / 10%); + border-radius: var(--wplace-radius); + border: 1px solid rgb(255 255 255 / 10%); +} + +:root .wplace-overlay-opacity-slider, +.wplace-theme-classic .wplace-overlay-opacity-slider { + background: linear-gradient(to right, #4facfe 0%, #00f2fe 100%); + border-radius: 4px; +} + +:root .wplace-speed-value, +.wplace-theme-classic .wplace-speed-value { + background: linear-gradient(135deg, #4facfe 0%, #00f2fe 100%); + border-radius: 8px; + color: white; + box-shadow: 0 3px 10px rgb(79 172 254 / 30%); + border: 1px solid rgb(255 255 255 / 20%); +} + +/* Additional elements for classic theme */ +:root .wplace-color-divider, +.wplace-theme-classic .wplace-color-divider { + background: rgb(255 255 255 / 10%); +} + +:root .wplace-settings-select option, +.wplace-theme-classic .wplace-settings-select option { + background: #2d3748; + color: white; +} + +:root .wplace-settings-description, +.wplace-theme-classic .wplace-settings-description { + color: rgb(255 255 255 / 70%); +} + +:root .wplace-settings-apply-btn, +.wplace-theme-classic .wplace-settings-apply-btn { + background: linear-gradient(135deg, #4facfe 0%, #00f2fe 100%); + color: white; + border-radius: 8px; +} + +/* Dual control layout styling for classic theme */ +:root .wplace-dual-control-compact, +.wplace-theme-classic .wplace-dual-control-compact { + display: flex; + flex-direction: row; + align-items: center; + gap: 12px; + margin: 8px 0; + flex-wrap: wrap; +} + +:root .wplace-slider-container-compact, +.wplace-theme-classic .wplace-slider-container-compact { + display: flex; + align-items: center; + gap: 8px; + flex: 1; + min-width: 160px; +} + +:root .wplace-speed-slider-container-compact, +.wplace-theme-classic .wplace-speed-slider-container-compact { + display: flex; + align-items: center; + gap: 8px; + flex: 1; + min-width: 160px; +} + +:root .wplace-input-group-compact, +.wplace-theme-classic .wplace-input-group-compact { + display: flex; + align-items: center; + gap: 4px; + flex-shrink: 0; +} + +:root .wplace-input-btn-compact, +.wplace-theme-classic .wplace-input-btn-compact { + background: linear-gradient(135deg, #4facfe 0%, #00f2fe 100%); + color: white; + border: none; + border-radius: 4px; + width: 22px; + height: 22px; + cursor: pointer; + font-weight: bold; + font-size: 12px; + display: flex; + align-items: center; + justify-content: center; + transition: all 0.2s ease; + box-shadow: 0 2px 4px rgba(0,0,0,0.2); +} + +:root .wplace-input-btn-compact:hover, +.wplace-theme-classic .wplace-input-btn-compact:hover { + transform: translateY(-1px); + box-shadow: 0 4px 8px rgba(0,0,0,0.3); + background: linear-gradient(135deg, #00f2fe 0%, #4facfe 100%); +} + +:root .wplace-input-btn-compact:active, +.wplace-theme-classic .wplace-input-btn-compact:active { + transform: translateY(0); + box-shadow: 0 2px 4px rgba(0,0,0,0.2); +} + +:root .wplace-number-input-compact, +.wplace-theme-classic .wplace-number-input-compact { + background: rgba(255,255,255,0.1); + border: 1px solid rgba(255,255,255,0.2); + border-radius: 4px; + color: white; + padding: 4px 8px; + font-size: 12px; + width: 65px; + text-align: center; + transition: all 0.2s ease; + height: 22px; +} + +:root .wplace-number-input-compact:focus, +.wplace-theme-classic .wplace-number-input-compact:focus { + outline: none; + border-color: #4facfe; + box-shadow: 0 0 0 3px rgba(79, 172, 254, 0.3); + background: rgba(255,255,255,0.15); +} + +:root .wplace-input-label-compact, +.wplace-theme-classic .wplace-input-label-compact { + color: rgba(255,255,255,0.8); + font-size: 11px; + margin-left: 4px; + white-space: nowrap; +} + +:root .wplace-batch-size-header, +.wplace-theme-classic .wplace-batch-size-header { + margin-bottom: 8px; +} + +:root .wplace-batch-size-label, +.wplace-theme-classic .wplace-batch-size-label { + color: var(--wplace-highlight); + font-weight: 500; + font-size: 13px; +} + +/* Cooldown control styling for classic theme */ +:root .wplace-cooldown-control, +.wplace-theme-classic .wplace-cooldown-control { + margin: 12px 0; +} + +:root .wplace-cooldown-control label, +.wplace-theme-classic .wplace-cooldown-control label { + color: var(--wplace-highlight); + font-weight: 500; + font-size: 13px; + margin-bottom: 8px; + display: block; +} + +/* Resize dialog styling for classic theme */ +:root .resize-container, +.wplace-theme-classic .resize-container { + background: #000; + border: 1px solid #fff; + border-radius: 12px; + box-shadow: 0 0 20px rgb(0 0 0 / 50%); +} + +/* Account switching UI styles */ +:root .wplace-section, +.wplace-theme-classic .wplace-section { + margin: 15px 0; + border-radius: var(--wplace-radius); + background: rgba(255, 255, 255, 0.05); + padding: 15px; + border: 1px solid rgba(255, 255, 255, 0.1); +} + +:root .wplace-section-title, +.wplace-theme-classic .wplace-section-title { + display: flex; + align-items: center; + gap: 8px; + font-weight: 600; + color: var(--wplace-text); + margin-bottom: 10px; + font-size: 14px; +} + +/* Auto-swap toggle switch */ +:root .wplace-switch, +.wplace-theme-classic .wplace-switch { + position: relative; + display: inline-block; + width: 50px; + height: 24px; +} + +:root .wplace-switch input, +.wplace-theme-classic .wplace-switch input { + opacity: 0; + width: 0; + height: 0; +} + +:root .wplace-slider-round, +.wplace-theme-classic .wplace-slider-round { + position: absolute; + cursor: pointer; + top: 0; + left: 0; + right: 0; + bottom: 0; + background-color: #444; + transition: 0.4s; + border-radius: 24px; +} + +:root .wplace-slider-round:before, +.wplace-theme-classic .wplace-slider-round:before { + position: absolute; + content: ""; + height: 18px; + width: 18px; + left: 3px; + bottom: 3px; + background-color: white; + transition: 0.4s; + border-radius: 50%; +} + +:root .wplace-switch input:checked + .wplace-slider-round, +.wplace-theme-classic .wplace-switch input:checked + .wplace-slider-round { + background: var(--wplace-slider-track-bg); +} + +:root .wplace-switch input:checked + .wplace-slider-round:before, +.wplace-theme-classic .wplace-switch input:checked + .wplace-slider-round:before { + transform: translateX(26px); +} + +/* Account list container */ +:root .accounts-list-container, +.wplace-theme-classic .accounts-list-container { + max-height: 200px; + overflow-y: auto; + margin-top: 10px; + padding: 0 6px; +} + +/* Individual account item */ +:root .wplace-account-item, +.wplace-theme-classic .wplace-account-item { + display: flex; + justify-content: space-between; + align-items: center; + padding: 6px 8px; + margin: 4px 0; + background: rgba(255, 255, 255, 0.03); + border-radius: 8px; + border: 1px solid rgba(255, 255, 255, 0.1); + transition: all 0.3s ease; + position: relative; +} + +/* Account ordering number */ +:root .wplace-account-number, +.wplace-theme-classic .wplace-account-number { + position: absolute; + top: 3px; + left: 3px; + width: 16px; + height: 16px; + background: var(--wplace-highlight); + color: white; + border-radius: 50%; + display: flex; + align-items: center; + justify-content: center; + font-size: 9px; + font-weight: bold; + z-index: 1; +} + +:root .wplace-account-item:hover, +.wplace-theme-classic .wplace-account-item:hover { + background: rgba(255, 255, 255, 0.08); + border-color: var(--wplace-highlight); +} + +:root .wplace-account-item.is-current, +.wplace-theme-classic .wplace-account-item.is-current { + background: rgba(79, 172, 254, 0.2); + border-color: #4facfe; + box-shadow: 0 0 10px rgba(79, 172, 254, 0.3); +} + +/* Account details */ +:root .wplace-account-details, +.wplace-theme-classic .wplace-account-details { + flex: 1; + margin-left: 20px; + min-width: 0; +} + +:root .wplace-account-name, +.wplace-theme-classic .wplace-account-name { + font-weight: 600; + color: var(--wplace-text); + font-size: 12px; + margin-bottom: 1px; + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; +} + +/* Account statistics */ +:root .wplace-account-stats, +.wplace-theme-classic .wplace-account-stats { + display: flex; + gap: 8px; + align-items: center; + font-size: 11px; + color: var(--wplace-muted-text); + flex-shrink: 0; +} + +:root .wplace-account-stats i, +.wplace-theme-classic .wplace-account-stats i { + margin-right: 4px; +} + +:root .wplace-account-stats .fas.fa-bolt, +.wplace-theme-classic .wplace-account-stats .fas.fa-bolt { + color: #ffd700; +} + +:root .wplace-account-stats .fas.fa-tint, +.wplace-theme-classic .wplace-account-stats .fas.fa-tint { + color: #00bfff; +} + +/* Move Artwork Panel Styles for Classic Theme */ +:root .wplace-move-panel, +.wplace-theme-classic .wplace-move-panel { + position: fixed; + top: 50%; + left: 50%; + transform: translate(-50%, -50%); + background: var(--wplace-primary); + border: 2px solid rgb(255 255 255 / 20%); + border-radius: var(--wplace-radius); + padding: 20px; + z-index: 10000; + box-shadow: var(--wplace-shadow); + min-width: 200px; + text-align: center; + backdrop-filter: var(--wplace-backdrop); + font-family: var(--wplace-font); +} + +:root .wplace-move-panel-title, +.wplace-theme-classic .wplace-move-panel-title { + color: var(--wplace-text); + margin: 0 0 15px 0; + font-size: 16px; + text-shadow: 0 1px 2px rgb(0 0 0 / 50%); +} + +:root .wplace-move-controls, +.wplace-theme-classic .wplace-move-controls { + display: grid; + grid-template-columns: 1fr 1fr 1fr; + gap: 8px; + margin-bottom: 15px; +} + +:root .wplace-move-btn, +.wplace-theme-classic .wplace-move-btn { + padding: 10px; + background: linear-gradient(135deg, #4CAF50 0%, #45a049 100%); + color: white; + border: 1px solid rgb(255 255 255 / 20%); + border-radius: var(--wplace-btn-radius); + cursor: pointer; + font-family: var(--wplace-font); + font-size: 14px; + transition: all 0.2s ease; + box-shadow: 0 2px 4px rgb(0 0 0 / 20%); +} + +:root .wplace-move-btn:hover, +.wplace-theme-classic .wplace-move-btn:hover { + background: linear-gradient(135deg, #45a049 0%, #4CAF50 100%); + transform: translateY(-1px); + box-shadow: 0 4px 8px rgb(0 0 0 / 30%); +} + +:root .wplace-move-btn:active, +.wplace-theme-classic .wplace-move-btn:active { + transform: translateY(0); + box-shadow: 0 2px 4px rgb(0 0 0 / 20%); +} + +:root .wplace-move-center, +.wplace-theme-classic .wplace-move-center { + color: var(--wplace-highlight); + padding: 10px; + font-size: 12px; + font-weight: bold; + display: flex; + align-items: center; + justify-content: center; +} + +:root .wplace-move-close-btn, +.wplace-theme-classic .wplace-move-close-btn { + background: linear-gradient(135deg, #f44336 0%, #d32f2f 100%); + color: white; + border: 1px solid rgb(255 255 255 / 20%); + padding: 10px 16px; + border-radius: var(--wplace-btn-radius); + cursor: pointer; + font-family: var(--wplace-font); + transition: all 0.2s ease; + box-shadow: 0 2px 4px rgb(0 0 0 / 20%); +} + +:root .wplace-move-close-btn:hover, +.wplace-theme-classic .wplace-move-close-btn:hover { + background: linear-gradient(135deg, #d32f2f 0%, #f44336 100%); + transform: translateY(-1px); + box-shadow: 0 4px 8px rgb(0 0 0 / 30%); +} + +:root .wplace-move-overlay, +.wplace-theme-classic .wplace-move-overlay { + position: fixed; + top: 0; + left: 0; + width: 100%; + height: 100%; + background: rgba(0, 0, 0, 0.8); + z-index: 9999; + backdrop-filter: blur(4px); +} + diff --git a/Extensions/themes/neon-cyan.css b/Extensions/themes/neon-cyan.css new file mode 100644 index 0000000..6f6a155 --- /dev/null +++ b/Extensions/themes/neon-cyan.css @@ -0,0 +1,815 @@ +/* WPlace Auto-Image Bot - Neon Cyan Theme */ +/* Retro cyberpunk aesthetic with cyan neon glow effects */ + +.wplace-theme-neon-cyan { + /* Neon theme colors - matching upstream main neon theme */ + --wplace-primary: #1959A1; + --wplace-secondary: #3C74AF; + --wplace-accent: #538CC0; + --wplace-text: #81DCF7; + --wplace-highlight: #EA9C00; + --wplace-success: #39ff14; + --wplace-error: #ff073a; + --wplace-warning: #ff0; + + /* Neon UI properties */ + --wplace-radius: 0; + --wplace-border-style: solid; + --wplace-border-width: 2px; + --wplace-border-color: #81DCF7; + --wplace-shadow: 0 0 20px rgb(234 156 0 / 40%), inset 0 0 20px rgb(234 156 0 / 10%); + --wplace-backdrop: none; + --wplace-font: 'Press Start 2P', monospace, 'Courier New'; + + /* Z-index layers */ + --wplace-z-overlay: 10000; + --wplace-z-alert: 10002; + --wplace-z-settings: 10002; + + /* Feature toggles */ + --wplace-scanline: 1; + --wplace-pixel-blink: 1; + + /* Icon colors - neon variants */ + --wplace-icon-primary: #81DCF7; + --wplace-icon-secondary: #39ff14; + --wplace-icon-palette: #EA9C00; + + /* Additional UI colors - neon variants */ + --wplace-danger: #ff073a; + --wplace-danger-dark: #cc0531; + --wplace-muted-text: #00ff4180; + --wplace-highlight-secondary: #fa0; + + /* Text variants - neon style */ + --wplace-text-secondary: #3C74AF; + --wplace-text-muted: #00ff41; + --wplace-text-dim: #00ff41; + --wplace-text-faded: #00ff41cc; + + /* Background variants - neon style */ + --wplace-bg-input: rgb(234 156 0 / 15%); + --wplace-bg-subtle: rgb(234 156 0 / 8%); + --wplace-bg-faint: rgb(234 156 0 / 5%); + --wplace-bg-ghost: rgb(234 156 0 / 3%); + --wplace-bg-whisper: rgb(234 156 0 / 2%); + + /* Border variants - neon style */ + --wplace-border-subtle: rgb(234 156 0 / 40%); + --wplace-border-faint: rgb(234 156 0 / 25%); + --wplace-border-ghost: rgb(234 156 0 / 15%); + --wplace-border-ultra-faint: rgb(234 156 0 / 8%); + + /* Shadow variants - neon style */ + --wplace-shadow-drag: 0 12px 40px rgb(234 156 0 / 60%), 0 0 0 2px rgb(234 156 0 / 80%), 0 0 20px rgb(234 156 0 / 30%); + --wplace-shadow-notification: 0 4px 12px rgb(234 156 0 / 40%), 0 0 15px rgb(234 156 0 / 20%); + --wplace-shadow-slider-thumb: 0 3px 6px rgb(234 156 0 / 50%), 0 0 0 2px var(--wplace-icon-primary), 0 0 10px rgb(234 156 0 / 30%); + --wplace-shadow-slider-hover: 0 4px 8px rgb(234 156 0 / 60%), 0 0 0 3px var(--wplace-icon-primary), 0 0 15px rgb(234 156 0 / 40%); + + /* Animation colors - neon style */ + --wplace-pulse-start: rgb(234 156 0 / 80%); + --wplace-pulse-mid: rgb(234 156 0 / 0%); + --wplace-pulse-end: rgb(234 156 0 / 0%); + + /* Slider colors - neon style */ + --wplace-slider-thumb-bg: #81DCF7; +} + +/* Neon-specific styling overrides */ +.wplace-theme-neon-cyan #wplace-image-bot-container { + background: var(--wplace-primary) !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(234 156 0 / 10%) !important; + font-family: var(--wplace-font) !important; +} + +.wplace-theme-neon-cyan #wplace-stats-container { + background: var(--wplace-primary) !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(234 156 0 / 10%) !important; + font-family: var(--wplace-font) !important; + position: fixed !important; + overflow: hidden !important; + z-index: 9998 !important; +} + +.wplace-theme-neon-cyan .wplace-header { + background: var(--wplace-secondary) !important; + border-bottom: 1px solid var(--wplace-text) !important; + color: var(--wplace-text) !important; + text-shadow: 0 0 10px var(--wplace-text) !important; + font-family: var(--wplace-font) !important; +} + +.wplace-theme-neon-cyan .wplace-section { + background: rgb(22 33 62 / 50%) !important; + border: 1px solid var(--wplace-text) !important; + border-radius: 0 !important; +} + +.wplace-theme-neon-cyan .wplace-status-section { + background: rgb(22 33 62 / 50%) !important; + border: 1px solid var(--wplace-text) !important; + border-radius: 0 !important; +} + +.wplace-theme-neon-cyan .wplace-section-title { + color: var(--wplace-text) !important; + text-shadow: 0 0 8px var(--wplace-text) !important; + text-transform: uppercase !important; + font-family: var(--wplace-font) !important; + font-size: 10px !important; +} + +.wplace-theme-neon-cyan .wplace-btn { + background: var(--wplace-secondary) !important; + border: 1px solid var(--wplace-text) !important; + border-radius: 0 !important; + color: var(--wplace-text) !important; + text-shadow: 0 0 5px var(--wplace-text) !important; + font-family: var(--wplace-font) !important; + font-size: 9px !important; + text-transform: uppercase !important; +} + +/* Pixel blinking and neon glow animation for buttons */ +.wplace-theme-neon-cyan .wplace-btn:hover:not(:disabled) { + box-shadow: 0 0 15px var(--wplace-text), inset 0 0 15px rgb(234 156 0 / 10%) !important; + animation: pixel-blink 0.5s infinite, neon-glow 1s ease-in-out infinite alternate !important; +} + +.wplace-theme-neon-cyan .wplace-btn-start { + background: var(--wplace-secondary) !important; + border-color: var(--wplace-success) !important; + color: var(--wplace-success) !important; + text-shadow: 0 0 8px var(--wplace-success) !important; +} + +.wplace-theme-neon-cyan .wplace-btn-stop { + background: var(--wplace-secondary) !important; + border-color: var(--wplace-error) !important; + color: var(--wplace-error) !important; + text-shadow: 0 0 8px var(--wplace-error) !important; +} + +.wplace-theme-neon-cyan .wplace-btn-upload { + background: var(--wplace-secondary) !important; + border: 1px dashed var(--wplace-highlight) !important; + color: var(--wplace-highlight) !important; + text-shadow: 0 0 8px var(--wplace-highlight) !important; +} + +.wplace-theme-neon-cyan .wplace-btn-select { + background: var(--wplace-secondary) !important; + border-color: var(--wplace-highlight) !important; + color: var(--wplace-highlight) !important; + text-shadow: 0 0 8px var(--wplace-highlight) !important; +} + +.wplace-theme-neon-cyan .wplace-btn-file { + background: var(--wplace-secondary) !important; + border-color: var(--wplace-warning) !important; + color: var(--wplace-warning) !important; + text-shadow: 0 0 8px var(--wplace-warning) !important; +} + +.wplace-theme-neon-cyan .wplace-progress { + background: rgb(0 0 0 / 80%) !important; + border: 1px solid var(--wplace-text) !important; + border-radius: 0 !important; +} + +.wplace-theme-neon-cyan .wplace-progress-bar { + background: linear-gradient(90deg, var(--wplace-success) 0%, var(--wplace-text) 100%) !important; + box-shadow: 0 0 10px var(--wplace-success) !important; +} + +.wplace-theme-neon-cyan .wplace-stat-value { + color: var(--wplace-text) !important; + text-shadow: 0 0 5px var(--wplace-text) !important; +} + +.wplace-theme-neon-cyan .status-success { + background: rgb(57 255 20 / 10%) !important; + border-color: var(--wplace-success) !important; + color: var(--wplace-success) !important; + box-shadow: 0 0 15px var(--wplace-success) !important; + text-shadow: 0 0 8px var(--wplace-success) !important; +} + +.wplace-theme-neon-cyan .status-error { + background: rgb(255 7 58 / 10%) !important; + border-color: var(--wplace-error) !important; + color: var(--wplace-error) !important; + box-shadow: 0 0 15px var(--wplace-error) !important; + text-shadow: 0 0 8px var(--wplace-error) !important; +} + +.wplace-theme-neon-cyan .status-default { + background: rgb(234 156 0 / 10%) !important; + border-color: var(--wplace-text) !important; + color: var(--wplace-text) !important; + text-shadow: 0 0 5px var(--wplace-text) !important; +} + +/* Settings dialog neon styling */ +.wplace-theme-neon-cyan #wplace-settings-container { + background: var(--wplace-primary) !important; + border: 2px solid var(--wplace-text) !important; + border-radius: 0 !important; + box-shadow: 0 0 30px var(--wplace-text), inset 0 0 30px rgb(234 156 0 / 10%) !important; + font-family: var(--wplace-font) !important; +} + +.wplace-theme-neon-cyan .wplace-settings-header { + background: var(--wplace-secondary) !important; + border-bottom: 1px solid var(--wplace-text) !important; +} + +.wplace-theme-neon-cyan .wplace-settings-header h3 { + color: var(--wplace-text) !important; + text-shadow: 0 0 10px var(--wplace-text) !important; + font-family: var(--wplace-font) !important; + font-size: 16px !important; + text-transform: uppercase !important; +} + +.wplace-theme-neon-cyan .wplace-settings-close-btn { + background: var(--wplace-secondary) !important; + border: 1px solid var(--wplace-text) !important; + border-radius: 0 !important; + color: var(--wplace-text) !important; +} + +.wplace-theme-neon-cyan .wplace-settings-close-btn:hover { + background: var(--wplace-error) !important; + box-shadow: 0 0 15px var(--wplace-error) !important; +} + +.wplace-theme-neon-cyan .wplace-settings-section-wrapper { + background: rgb(22 33 62 / 30%) !important; + border: 1px solid var(--wplace-text) !important; + border-radius: 0 !important; +} + +.wplace-theme-neon-cyan .wplace-settings-select { + background: var(--wplace-secondary) !important; + border: 1px solid var(--wplace-text) !important; + border-radius: 0 !important; + color: var(--wplace-text) !important; + font-family: var(--wplace-font) !important; + font-size: 11px !important; +} + +.wplace-theme-neon-cyan .wplace-settings-section-label { + color: var(--wplace-text) !important; + text-shadow: 0 0 8px var(--wplace-text) !important; + font-family: var(--wplace-font) !important; + font-size: 12px !important; + text-transform: uppercase !important; +} + +.wplace-theme-neon-cyan .wplace-speed-value { + background: var(--wplace-secondary) !important; + border: 1px solid var(--wplace-text) !important; + border-radius: 0 !important; + color: var(--wplace-text) !important; + text-shadow: 0 0 8px var(--wplace-text) !important; + font-family: var(--wplace-font) !important; + box-shadow: 0 0 10px rgb(234 156 0 / 30%) !important; +} + +.wplace-theme-neon-cyan .wplace-overlay-opacity-value { + background: var(--wplace-secondary) !important; + border: 1px solid var(--wplace-text) !important; + border-radius: 0 !important; + color: var(--wplace-text) !important; + text-shadow: 0 0 5px var(--wplace-text) !important; +} + +/* Neon slider styling */ +.wplace-theme-neon-cyan .wplace-slider, +.wplace-theme-neon-cyan .wplace-speed-slider, +.wplace-theme-neon-cyan .wplace-overlay-opacity-slider { + background: var(--wplace-secondary) !important; + border: 1px solid var(--wplace-text) !important; + border-radius: 0 !important; + box-shadow: 0 0 10px rgb(234 156 0 / 30%) !important; +} + +.wplace-theme-neon-cyan .wplace-slider::-webkit-slider-thumb, +.wplace-theme-neon-cyan .wplace-speed-slider::-webkit-slider-thumb, +.wplace-theme-neon-cyan .wplace-overlay-opacity-slider::-webkit-slider-thumb { + -webkit-appearance: none; + width: 16px; + height: 16px; + border-radius: 0 !important; + background: var(--wplace-slider-thumb-bg) !important; + border: 2px solid var(--wplace-text) !important; + cursor: pointer; + transition: all 0.2s ease; + box-shadow: 0 0 8px rgb(234 156 0 / 50%) !important; +} + +.wplace-theme-neon-cyan .wplace-slider::-webkit-slider-thumb:hover, +.wplace-theme-neon-cyan .wplace-speed-slider::-webkit-slider-thumb:hover, +.wplace-theme-neon-cyan .wplace-overlay-opacity-slider::-webkit-slider-thumb:hover { + transform: scale(1.1); + box-shadow: 0 0 15px var(--wplace-text) !important; +} + +/* Scanline animation for neon theme */ +.wplace-theme-neon-cyan #wplace-image-bot-container::before { + content: ''; + position: absolute; + top: 0; + left: 0; + right: 0; + height: 2px; + background: linear-gradient(90deg, transparent, var(--wplace-text), transparent); + z-index: 1; + pointer-events: none; + animation: scanline 3s linear infinite; + opacity: 0.7; +} + + +.wplace-theme-neon-cyan #wplace-stats-container::before { + content: ''; + position: absolute; + top: 0; + left: 0; + right: 0; + height: 2px; + background: linear-gradient(90deg, transparent, var(--wplace-text), transparent); + z-index: 1; + pointer-events: none; + animation: scanline-stats 4s linear infinite; + opacity: 0.7; +} + + +/* Text glow animations */ +@keyframes neon-glow { + 0%, 100% { + text-shadow: 0 0 5px currentcolor, 0 0 10px currentcolor, 0 0 15px currentcolor; + } + + 50% { + text-shadow: 0 0 2px currentcolor, 0 0 5px currentcolor, 0 0 8px currentcolor; + } +} + +@keyframes pixel-blink { + 0%, 50% { + opacity: 1; + } + + 51%, 100% { + opacity: 0.7; + } +} + +@keyframes scanline { + 0% { + transform: translateY(-100%); + } + + 100% { + transform: translateY(400px); + } +} + +@keyframes scanline-stats { + 0% { + transform: translateY(-100%); + } + + 100% { + transform: translateY(300px); + } +} + +/* Dual control layout styling for neon theme */ +.wplace-theme-neon-cyan .wplace-dual-control-compact { + display: flex; + flex-direction: row; + align-items: center; + gap: 8px; + margin: 6px 0; + flex-wrap: wrap; +} + +.wplace-theme-neon-cyan .wplace-slider-container-compact { + display: flex; + align-items: center; + gap: 6px; + flex: 1; + min-width: 160px; +} + +.wplace-theme-neon-cyan .wplace-speed-slider-container-compact { + display: flex; + align-items: center; + gap: 6px; + flex: 1; + min-width: 160px; +} + +.wplace-theme-neon-cyan .wplace-input-group-compact { + display: flex; + align-items: center; + gap: 3px; + flex-shrink: 0; +} + +.wplace-theme-neon-cyan .wplace-input-btn-compact { + background: var(--wplace-secondary) !important; + border: 1px solid var(--wplace-text) !important; + border-radius: 0 !important; + color: var(--wplace-text) !important; + width: 18px; + height: 18px; + cursor: pointer; + font-weight: bold; + font-size: 10px; + display: flex; + align-items: center; + justify-content: center; + transition: all 0.2s ease; + text-shadow: 0 0 5px var(--wplace-text) !important; + font-family: var(--wplace-font) !important; +} + +.wplace-theme-neon-cyan .wplace-input-btn-compact:hover { + box-shadow: 0 0 10px var(--wplace-text) !important; + animation: pixel-blink 0.5s infinite !important; +} + +.wplace-theme-neon-cyan .wplace-number-input-compact { + background: var(--wplace-secondary) !important; + border: 1px solid var(--wplace-text) !important; + border-radius: 0 !important; + color: var(--wplace-text) !important; + padding: 2px 4px; + font-size: 10px; + width: 65px; + text-align: center; + transition: all 0.2s ease; + height: 18px; + font-family: var(--wplace-font) !important; + text-shadow: 0 0 5px var(--wplace-text) !important; +} + +.wplace-theme-neon-cyan .wplace-number-input-compact:focus { + outline: none; + box-shadow: 0 0 10px var(--wplace-text) !important; + background: rgb(234 156 0 / 15%) !important; +} + +.wplace-theme-neon-cyan .wplace-input-label-compact { + color: var(--wplace-text) !important; + font-size: 9px; + margin-left: 3px; + white-space: nowrap; + text-shadow: 0 0 5px var(--wplace-text) !important; + font-family: var(--wplace-font) !important; + text-transform: uppercase; +} + +.wplace-theme-neon-cyan .wplace-batch-size-header { + margin-bottom: 6px; +} + +.wplace-theme-neon-cyan .wplace-batch-size-label { + color: var(--wplace-text) !important; + font-weight: 500; + font-size: 11px; + text-shadow: 0 0 8px var(--wplace-text) !important; + font-family: var(--wplace-font) !important; + text-transform: uppercase; +} + +/* Cooldown control styling for neon theme */ +.wplace-theme-neon-cyan .wplace-cooldown-control { + margin: 8px 0; +} + +.wplace-theme-neon-cyan .wplace-cooldown-control label { + color: var(--wplace-text) !important; + font-weight: 500; + font-size: 11px; + text-shadow: 0 0 8px var(--wplace-text) !important; + font-family: var(--wplace-font) !important; + text-transform: uppercase; + margin-bottom: 6px; + display: block; +} + +/* Resize dialog styling for neon theme */ +.wplace-theme-neon-cyan .resize-container { + background-color: #1959A1 !important; /* Solid neon blue */ + opacity: 1 !important; /* Fully opaque */ + backdrop-filter: none !important; /* Remove blur effects */ + box-shadow: none !important; /* Optional: remove glows */ + font-family: 'Press Start 2P', monospace !important; +} + +/* Account switching UI styles */ +.wplace-theme-neon-cyan .wplace-section { + margin: 15px 0; + border-radius: 0; + background: rgba(0, 255, 255, 0.05); + padding: 15px; + border: 2px solid #00ffff; + box-shadow: 0 0 20px rgba(0, 255, 255, 0.3); + font-family: var(--wplace-font); +} + +.wplace-theme-neon-cyan .wplace-section-title { + display: flex; + align-items: center; + gap: 8px; + font-weight: 600; + color: var(--wplace-text); + margin-bottom: 10px; + font-size: 12px; + text-transform: uppercase; + text-shadow: 0 0 8px var(--wplace-text); + font-family: var(--wplace-font); +} + +/* Auto-swap toggle switch */ +.wplace-theme-neon-cyan .wplace-switch { + position: relative; + display: inline-block; + width: 50px; + height: 24px; +} + +.wplace-theme-neon-cyan .wplace-switch input { + opacity: 0; + width: 0; + height: 0; +} + +.wplace-theme-neon-cyan .wplace-slider-round { + position: absolute; + cursor: pointer; + top: 0; + left: 0; + right: 0; + bottom: 0; + background-color: #1a1a2e; + transition: 0.4s; + border-radius: 0; + border: 2px solid #333; + box-shadow: 0 0 10px rgba(0, 255, 255, 0.2); +} + +.wplace-theme-neon-cyan .wplace-slider-round:before { + position: absolute; + content: ""; + height: 14px; + width: 14px; + left: 3px; + bottom: 3px; + background-color: #fff; + transition: 0.4s; + border-radius: 0; + box-shadow: 0 0 8px rgba(255, 255, 255, 0.5); +} + +.wplace-theme-neon-cyan .wplace-switch input:checked + .wplace-slider-round { + background-color: #00ffff; + box-shadow: 0 0 20px rgba(0, 255, 255, 0.6); +} + +.wplace-theme-neon-cyan .wplace-switch input:checked + .wplace-slider-round:before { + transform: translateX(26px); + box-shadow: 0 0 12px rgba(0, 255, 255, 0.8); +} + +/* Account list container */ +.wplace-theme-neon-cyan .accounts-list-container { + max-height: 200px; + overflow-y: auto; + margin-top: 10px; + padding: 0 6px; +} + +/* Individual account item */ +.wplace-theme-neon-cyan .wplace-account-item { + display: flex; + justify-content: space-between; + align-items: center; + padding: 6px 8px; + margin: 4px 0; + background: rgba(0, 255, 255, 0.03); + border-radius: 0; + border: 1px solid #00ffff; + transition: all 0.3s ease; + box-shadow: 0 0 5px rgba(0, 255, 255, 0.2); + position: relative; +} + +/* Account ordering number */ +.wplace-theme-neon-cyan .wplace-account-number { + position: absolute; + top: 3px; + left: 3px; + width: 8x; + height: 8px; + background: #00ffff; + color: #1a1a2e; + border-radius: 0; + display: flex; + align-items: center; + justify-content: center; + font-size: 8px; + font-weight: bold; + z-index: 1; + border: 1px solid #00ffff; + box-shadow: 0 0 8px rgba(0, 255, 255, 0.6); + font-family: var(--wplace-font); +} + +.wplace-theme-neon-cyan .wplace-account-item:hover { + background: rgba(0, 255, 255, 0.08); + border-color: #00ffff; + box-shadow: 0 0 15px rgba(0, 255, 255, 0.4); + transform: translateX(2px); +} + +.wplace-theme-neon-cyan .wplace-account-item.is-current { + background: rgba(0, 255, 255, 0.15); + border-color: #00ffff; + box-shadow: 0 0 25px rgba(0, 255, 255, 0.6); + animation: neon-cyan-pulse 2s infinite; +} + +@keyframes neon-cyan-pulse { + 0%, 100% { box-shadow: 0 0 25px rgba(0, 255, 255, 0.6); } + 50% { box-shadow: 0 0 35px rgba(0, 255, 255, 0.8); } +} + +/* Account details */ +.wplace-theme-neon-cyan .wplace-account-details { + flex: 1; + margin-left: 20px; + min-width: 0; +} + +.wplace-theme-neon-cyan .wplace-account-name { + font-weight: 600; + color: var(--wplace-text); + font-size: 8px; + margin-bottom: 1px; + text-transform: uppercase; + text-shadow: 0 0 5px var(--wplace-text); + font-family: var(--wplace-font); + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; +} + +/* Account statistics */ +.wplace-theme-neon-cyan .wplace-account-stats { + display: flex; + gap: 8px; + align-items: center; + font-size: 8px; + color: rgba(0, 255, 255, 0.8); + text-transform: uppercase; + font-family: var(--wplace-font); + flex-shrink: 0; +} + +.wplace-theme-neon-cyan .wplace-account-stats i { + margin-right: 4px; + text-shadow: 0 0 5px currentColor; +} + +.wplace-theme-neon-cyan .wplace-account-stats .fas.fa-bolt { + color: #ffff00; + text-shadow: 0 0 8px #ffff00; +} + +.wplace-theme-neon-cyan .wplace-account-stats .fas.fa-tint { + color: #00ffff; + text-shadow: 0 0 8px #00ffff; +} + +/* Move Artwork Panel Styles for Neon Cyan Theme */ +.wplace-theme-neon-cyan .wplace-move-panel { + position: fixed; + top: 50%; + left: 50%; + transform: translate(-50%, -50%); + background: var(--wplace-primary) !important; + border: 2px solid var(--wplace-text) !important; + border-radius: 0 !important; + padding: 20px; + z-index: 10000; + box-shadow: 0 0 20px var(--wplace-text), inset 0 0 20px rgba(129, 220, 247, 0.1) !important; + min-width: 200px; + text-align: center; + font-family: var(--wplace-font) !important; + color: var(--wplace-text) !important; +} + +.wplace-theme-neon-cyan .wplace-move-panel-title { + color: var(--wplace-text) !important; + margin: 0 0 15px 0; + font-size: 12px !important; + text-transform: uppercase; + text-shadow: 0 0 10px var(--wplace-text) !important; + font-family: var(--wplace-font) !important; + animation: cyan-pulse 2s infinite; +} + +.wplace-theme-neon-cyan .wplace-move-controls { + display: grid; + grid-template-columns: 1fr 1fr 1fr; + gap: 4px; + margin-bottom: 15px; +} + +.wplace-theme-neon-cyan .wplace-move-btn { + padding: 8px !important; + background: var(--wplace-secondary) !important; + color: var(--wplace-text) !important; + border: 1px solid var(--wplace-text) !important; + border-radius: 0 !important; + cursor: pointer; + font-family: var(--wplace-font) !important; + font-size: 10px !important; + text-shadow: 0 0 5px var(--wplace-text) !important; + transition: all 0.2s ease; + text-transform: uppercase; +} + +.wplace-theme-neon-cyan .wplace-move-btn:hover { + background: var(--wplace-accent) !important; + box-shadow: 0 0 15px var(--wplace-text) !important; + animation: cyan-flicker 0.5s infinite; +} + +.wplace-theme-neon-cyan .wplace-move-center { + color: var(--wplace-highlight) !important; + padding: 8px; + font-size: 9px !important; + font-weight: bold; + display: flex; + align-items: center; + justify-content: center; + text-shadow: 0 0 5px var(--wplace-highlight) !important; + text-transform: uppercase; + font-family: var(--wplace-font) !important; +} + +.wplace-theme-neon-cyan .wplace-move-close-btn { + background: var(--wplace-error) !important; + color: white !important; + border: 1px solid var(--wplace-error) !important; + padding: 8px 12px; + border-radius: 0 !important; + cursor: pointer; + font-family: var(--wplace-font) !important; + font-size: 9px !important; + text-transform: uppercase; + text-shadow: 0 0 5px white !important; + transition: all 0.2s ease; +} + +.wplace-theme-neon-cyan .wplace-move-close-btn:hover { + box-shadow: 0 0 15px var(--wplace-error) !important; + animation: error-pulse 0.5s infinite; +} + +.wplace-theme-neon-cyan .wplace-move-overlay { + position: fixed; + top: 0; + left: 0; + width: 100%; + height: 100%; + background: rgba(25, 89, 161, 0.9) !important; + z-index: 9999; +} + +@keyframes cyan-pulse { + 0%, 100% { text-shadow: 0 0 5px var(--wplace-text), 0 0 10px var(--wplace-text); } + 50% { text-shadow: 0 0 10px var(--wplace-text), 0 0 20px var(--wplace-text); } +} + +@keyframes cyan-flicker { + 0%, 50% { opacity: 1; } + 51%, 100% { opacity: 0.8; } +} + +@keyframes error-pulse { + 0%, 100% { box-shadow: 0 0 5px var(--wplace-error); } + 50% { box-shadow: 0 0 15px var(--wplace-error); } +} diff --git a/Extensions/themes/neon-light.css b/Extensions/themes/neon-light.css new file mode 100644 index 0000000..fbc2316 --- /dev/null +++ b/Extensions/themes/neon-light.css @@ -0,0 +1,881 @@ +/* WPlace Auto-Image Bot - Neon Light Theme */ +/* Retro cyberpunk aesthetic with light neon glow effects */ + +.wplace-theme-neon-light { + /* Neon theme colors - matching upstream main neon theme */ + --wplace-primary: #E0E0E1; + --wplace-secondary: #FBFBFB; + --wplace-accent: #203C5D; + --wplace-text: #203C5D; + --wplace-highlight: #203C5D; + --wplace-success: #39ff14; + --wplace-error: #ff073a; + --wplace-warning: #ff0; + + /* Neon UI properties */ + --wplace-radius: 0; + --wplace-border-style: solid; + --wplace-border-width: 2px; + --wplace-border-color: #203C5D; + --wplace-shadow: 0 0 20px rgb(234 156 0 / 40%), inset 0 0 20px rgb(234 156 0 / 10%); + --wplace-backdrop: none; + --wplace-font: 'Press Start 2P', monospace, 'Courier New'; + + /* Z-index layers */ + --wplace-z-overlay: 10000; + --wplace-z-alert: 10002; + --wplace-z-settings: 10002; + + /* Feature toggles */ + --wplace-scanline: 1; + --wplace-pixel-blink: 1; + + /* Icon colors - neon variants */ + --wplace-icon-primary: #203C5D; + --wplace-icon-secondary: #39ff14; + --wplace-icon-palette: #203C5D; + + /* Additional UI colors - neon variants */ + --wplace-danger: #ff073a; + --wplace-danger-dark: #cc0531; + --wplace-muted-text: #00ff4180; + --wplace-highlight-secondary: #fa0; + + /* Text variants - neon style */ + --wplace-text-secondary: #3C74AF; + --wplace-text-muted: #00ff41; + --wplace-text-dim: #00ff41; + --wplace-text-faded: #00ff41cc; + + /* Background variants - neon style */ + --wplace-bg-input: rgb(234 156 0 / 15%); + --wplace-bg-subtle: rgb(234 156 0 / 8%); + --wplace-bg-faint: rgb(234 156 0 / 5%); + --wplace-bg-ghost: rgb(234 156 0 / 3%); + --wplace-bg-whisper: rgb(234 156 0 / 2%); + + /* Border variants - neon style */ + --wplace-border-subtle: rgb(234 156 0 / 40%); + --wplace-border-faint: rgb(234 156 0 / 25%); + --wplace-border-ghost: rgb(234 156 0 / 15%); + --wplace-border-ultra-faint: rgb(234 156 0 / 8%); + + /* Shadow variants - neon style */ + --wplace-shadow-drag: 0 12px 40px rgb(234 156 0 / 60%), 0 0 0 2px rgb(234 156 0 / 80%), 0 0 20px rgb(234 156 0 / 30%); + --wplace-shadow-notification: 0 4px 12px rgb(234 156 0 / 40%), 0 0 15px rgb(234 156 0 / 20%); + --wplace-shadow-slider-thumb: 0 3px 6px rgb(234 156 0 / 50%), 0 0 0 2px var(--wplace-icon-primary), 0 0 10px rgb(234 156 0 / 30%); + --wplace-shadow-slider-hover: 0 4px 8px rgb(234 156 0 / 60%), 0 0 0 3px var(--wplace-icon-primary), 0 0 15px rgb(234 156 0 / 40%); + + /* Animation colors - neon style */ + --wplace-pulse-start: rgb(234 156 0 / 80%); + --wplace-pulse-mid: rgb(234 156 0 / 0%); + --wplace-pulse-end: rgb(234 156 0 / 0%); + + /* Slider colors - neon style */ + --wplace-slider-thumb-bg: #203C5D; +} + +/* Neon-specific styling overrides */ +.wplace-theme-neon-light #wplace-image-bot-container { + background: var(--wplace-primary) !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(234 156 0 / 10%) !important; + font-family: var(--wplace-font) !important; +} + +.wplace-theme-neon-light #wplace-stats-container { + background: var(--wplace-primary) !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(234 156 0 / 10%) !important; + font-family: var(--wplace-font) !important; + position: fixed !important; + overflow: hidden !important; + z-index: 9998 !important; +} + +.wplace-theme-neon-light .wplace-header { + background: var(--wplace-secondary) !important; + border-bottom: 1px solid var(--wplace-text) !important; + color: var(--wplace-text) !important; + text-shadow: 0 0 10px var(--wplace-text) !important; + font-family: var(--wplace-font) !important; +} + +.wplace-theme-neon-light .wplace-section { + background: rgb(22 33 62 / 30%) !important; + border: 1px solid var(--wplace-text) !important; + border-radius: 0 !important; +} + +.wplace-theme-neon-light .wplace-status-section { + background: rgb(22 33 62 / 30%) !important; + border: 1px solid var(--wplace-text) !important; + border-radius: 0 !important; +} + +.wplace-theme-neon-light .wplace-section-title { + color: var(--wplace-text) !important; + text-shadow: 0 0 8px var(--wplace-text) !important; + text-transform: uppercase !important; + font-family: var(--wplace-font) !important; + font-size: 10px !important; +} + +.wplace-theme-neon-light .wplace-btn { + background: var(--wplace-secondary) !important; + border: 1px solid var(--wplace-text) !important; + border-radius: 0 !important; + color: var(--wplace-text) !important; + text-shadow: 0 0 5px var(--wplace-text) !important; + font-family: var(--wplace-font) !important; + font-size: 9px !important; + text-transform: uppercase !important; +} + +/* Pixel blinking and neon glow animation for buttons */ +.wplace-theme-neon-light .wplace-btn:hover:not(:disabled) { + box-shadow: 0 0 15px var(--wplace-text), inset 0 0 15px rgb(234 156 0 / 10%) !important; + animation: pixel-blink 0.5s infinite, neon-glow 1s ease-in-out infinite alternate !important; +} + +.wplace-theme-neon-light .wplace-btn-start { + background: var(--wplace-secondary) !important; + border-color: var(--wplace-success) !important; + color: var(--wplace-success) !important; + text-shadow: 0 0 8px var(--wplace-success) !important; +} + +.wplace-theme-neon-light .wplace-btn-stop { + background: var(--wplace-secondary) !important; + border-color: var(--wplace-error) !important; + color: var(--wplace-error) !important; + text-shadow: 0 0 8px var(--wplace-error) !important; +} + +.wplace-theme-neon-light .wplace-btn-upload { + background: var(--wplace-secondary) !important; + border: 1px dashed var(--wplace-highlight) !important; + color: var(--wplace-highlight) !important; + text-shadow: 0 0 8px var(--wplace-highlight) !important; +} + +.wplace-theme-neon-light .wplace-btn-select { + background: var(--wplace-secondary) !important; + border-color: var(--wplace-highlight) !important; + color: var(--wplace-highlight) !important; + text-shadow: 0 0 8px var(--wplace-highlight) !important; +} + +.wplace-theme-neon-light .wplace-btn-file { + background: var(--wplace-secondary) !important; + border-color: var(--wplace-warning) !important; + color: var(--wplace-warning) !important; + text-shadow: 0 0 8px var(--wplace-warning) !important; +} + +.wplace-theme-neon-light .wplace-progress { + background: rgb(0 0 0 / 80%) !important; + border: 1px solid var(--wplace-text) !important; + border-radius: 0 !important; +} + +.wplace-theme-neon-light .wplace-progress-bar { + background: linear-gradient(90deg, var(--wplace-success) 0%, var(--wplace-text) 100%) !important; + box-shadow: 0 0 10px var(--wplace-success) !important; +} + +.wplace-theme-neon-light .wplace-stat-value { + color: var(--wplace-text) !important; + text-shadow: 0 0 5px var(--wplace-text) !important; +} + +.wplace-theme-neon-light .status-success { + background: rgb(57 255 20 / 10%) !important; + border-color: var(--wplace-success) !important; + color: var(--wplace-success) !important; + box-shadow: 0 0 15px var(--wplace-success) !important; + text-shadow: 0 0 8px var(--wplace-success) !important; +} + +.wplace-theme-neon-light .status-error { + background: rgb(255 7 58 / 10%) !important; + border-color: var(--wplace-error) !important; + color: var(--wplace-error) !important; + box-shadow: 0 0 15px var(--wplace-error) !important; + text-shadow: 0 0 8px var(--wplace-error) !important; +} + +.wplace-theme-neon-light .status-default { + background: rgb(234 156 0 / 10%) !important; + border-color: var(--wplace-text) !important; + color: var(--wplace-text) !important; + text-shadow: 0 0 5px var(--wplace-text) !important; +} + +/* Settings dialog neon styling */ +.wplace-theme-neon-light #wplace-settings-container { + background: var(--wplace-primary) !important; + border: 2px solid var(--wplace-text) !important; + border-radius: 0 !important; + box-shadow: 0 0 30px var(--wplace-text), inset 0 0 30px rgb(234 156 0 / 10%) !important; + font-family: var(--wplace-font) !important; +} + +.wplace-theme-neon-light .wplace-settings-header { + background: var(--wplace-secondary) !important; + border-bottom: 1px solid var(--wplace-text) !important; +} + +.wplace-theme-neon-light .wplace-settings-header h3 { + color: var(--wplace-text) !important; + text-shadow: 0 0 10px var(--wplace-text) !important; + font-family: var(--wplace-font) !important; + font-size: 16px !important; + text-transform: uppercase !important; +} + +.wplace-theme-neon-light .wplace-settings-close-btn { + background: var(--wplace-secondary) !important; + border: 1px solid var(--wplace-text) !important; + border-radius: 0 !important; + color: var(--wplace-text) !important; +} + +.wplace-theme-neon-light .wplace-settings-close-btn:hover { + background: var(--wplace-error) !important; + box-shadow: 0 0 15px var(--wplace-error) !important; +} + +.wplace-theme-neon-light .wplace-settings-section-wrapper { + background: rgb(22 33 62 / 10%) !important; + border: 1px solid var(--wplace-text) !important; + border-radius: 0 !important; +} + +.wplace-theme-neon-light .wplace-settings-select { + background: var(--wplace-secondary) !important; + border: 1px solid var(--wplace-text) !important; + border-radius: 0 !important; + color: var(--wplace-text) !important; + font-family: var(--wplace-font) !important; + font-size: 11px !important; +} + +.wplace-theme-neon-light .wplace-settings-section-label { + color: var(--wplace-text) !important; + text-shadow: 0 0 8px var(--wplace-text) !important; + font-family: var(--wplace-font) !important; + font-size: 12px !important; + text-transform: uppercase !important; +} + +.wplace-theme-neon-light .wplace-speed-value { + background: var(--wplace-secondary) !important; + border: 1px solid var(--wplace-text) !important; + border-radius: 0 !important; + color: var(--wplace-text) !important; + text-shadow: 0 0 8px var(--wplace-text) !important; + font-family: var(--wplace-font) !important; + box-shadow: 0 0 10px rgb(234 156 0 / 30%) !important; +} + +.wplace-theme-neon-light .wplace-overlay-opacity-value { + background: var(--wplace-secondary) !important; + border: 1px solid var(--wplace-text) !important; + border-radius: 0 !important; + color: var(--wplace-text) !important; + text-shadow: 0 0 5px var(--wplace-text) !important; +} + +/* Neon slider styling */ +.wplace-theme-neon-light .wplace-slider, +.wplace-theme-neon-light .wplace-speed-slider, +.wplace-theme-neon-light .wplace-overlay-opacity-slider { + background: var(--wplace-secondary) !important; + border: 1px solid var(--wplace-text) !important; + border-radius: 0 !important; + box-shadow: 0 0 10px rgb(234 156 0 / 30%) !important; +} + +.wplace-theme-neon-light .wplace-slider::-webkit-slider-thumb, +.wplace-theme-neon-light .wplace-speed-slider::-webkit-slider-thumb, +.wplace-theme-neon-light .wplace-overlay-opacity-slider::-webkit-slider-thumb { + -webkit-appearance: none; + width: 16px; + height: 16px; + border-radius: 0 !important; + background: var(--wplace-slider-thumb-bg) !important; + border: 2px solid var(--wplace-text) !important; + cursor: pointer; + transition: all 0.2s ease; + box-shadow: 0 0 8px rgb(234 156 0 / 50%) !important; +} + +.wplace-theme-neon-light .wplace-slider::-webkit-slider-thumb:hover, +.wplace-theme-neon-light .wplace-speed-slider::-webkit-slider-thumb:hover, +.wplace-theme-neon-light .wplace-overlay-opacity-slider::-webkit-slider-thumb:hover { + transform: scale(1.1); + box-shadow: 0 0 15px var(--wplace-text) !important; +} + +/* Scanline animation for neon theme */ +.wplace-theme-neon-light #wplace-image-bot-container::before { + content: ''; + position: absolute; + top: 0; + left: 0; + right: 0; + height: 2px; + background: linear-gradient(90deg, transparent, var(--wplace-text), transparent); + z-index: 1; + pointer-events: none; + animation: scanline 3s linear infinite; + opacity: 0.7; +} + + +.wplace-theme-neon-light #wplace-stats-container::before { + content: ''; + position: absolute; + top: 0; + left: 0; + right: 0; + height: 2px; + background: linear-gradient(90deg, transparent, var(--wplace-text), transparent); + z-index: 1; + pointer-events: none; + animation: scanline-stats 4s linear infinite; + opacity: 0.7; +} + + +/* Text glow animations */ +@keyframes neon-glow { + 0%, 100% { + text-shadow: 0 0 5px currentcolor, 0 0 10px currentcolor, 0 0 15px currentcolor; + } + + 50% { + text-shadow: 0 0 2px currentcolor, 0 0 5px currentcolor, 0 0 8px currentcolor; + } +} + +@keyframes pixel-blink { + 0%, 50% { + opacity: 1; + } + + 51%, 100% { + opacity: 0.7; + } +} + +@keyframes scanline { + 0% { + transform: translateY(-100%); + } + + 100% { + transform: translateY(400px); + } +} + +@keyframes scanline-stats { + 0% { + transform: translateY(-100%); + } + + 100% { + transform: translateY(300px); + } +} + +/* Dual control layout styling for neon theme */ +.wplace-theme-neon-light .wplace-dual-control-compact { + display: flex; + flex-direction: row; + align-items: center; + gap: 8px; + margin: 6px 0; + flex-wrap: wrap; +} + +.wplace-theme-neon-light .wplace-slider-container-compact { + display: flex; + align-items: center; + gap: 6px; + flex: 1; + min-width: 160px; +} + +.wplace-theme-neon-light .wplace-speed-slider-container-compact { + display: flex; + align-items: center; + gap: 6px; + flex: 1; + min-width: 160px; +} + +.wplace-theme-neon-light .wplace-input-group-compact { + display: flex; + align-items: center; + gap: 3px; + flex-shrink: 0; +} + +.wplace-theme-neon-light .wplace-input-btn-compact { + background: var(--wplace-secondary) !important; + border: 1px solid var(--wplace-text) !important; + border-radius: 0 !important; + color: var(--wplace-text) !important; + width: 18px; + height: 18px; + cursor: pointer; + font-weight: bold; + font-size: 10px; + display: flex; + align-items: center; + justify-content: center; + transition: all 0.2s ease; + text-shadow: 0 0 5px var(--wplace-text) !important; + font-family: var(--wplace-font) !important; +} + +.wplace-theme-neon-light .wplace-input-btn-compact:hover { + box-shadow: 0 0 10px var(--wplace-text) !important; + animation: pixel-blink 0.5s infinite !important; +} + +.wplace-theme-neon-light .wplace-number-input-compact { + background: var(--wplace-secondary) !important; + border: 1px solid var(--wplace-text) !important; + border-radius: 0 !important; + color: var(--wplace-text) !important; + padding: 2px 4px; + font-size: 10px; + width: 65px; + text-align: center; + transition: all 0.2s ease; + height: 18px; + font-family: var(--wplace-font) !important; + text-shadow: 0 0 5px var(--wplace-text) !important; +} + +.wplace-theme-neon-light .wplace-number-input-compact:focus { + outline: none; + box-shadow: 0 0 10px var(--wplace-text) !important; + background: rgb(234 156 0 / 15%) !important; +} + +.wplace-theme-neon-light .wplace-input-label-compact { + color: var(--wplace-text) !important; + font-size: 9px; + margin-left: 3px; + white-space: nowrap; + text-shadow: 0 0 5px var(--wplace-text) !important; + font-family: var(--wplace-font) !important; + text-transform: uppercase; +} + +.wplace-theme-neon-light .wplace-batch-size-header { + margin-bottom: 6px; +} + +.wplace-theme-neon-light .wplace-batch-size-label { + color: var(--wplace-text) !important; + font-weight: 500; + font-size: 11px; + text-shadow: 0 0 8px var(--wplace-text) !important; + font-family: var(--wplace-font) !important; + text-transform: uppercase; +} + +/* Cooldown control styling for neon theme */ +.wplace-theme-neon-light .wplace-cooldown-control { + margin: 8px 0; +} + +.wplace-theme-neon-light .wplace-cooldown-control label { + color: var(--wplace-text) !important; + font-weight: 500; + font-size: 11px; + text-shadow: 0 0 8px var(--wplace-text) !important; + font-family: var(--wplace-font) !important; + text-transform: uppercase; + margin-bottom: 6px; + display: block; +} + +/* Resize dialog styling for neon theme */ +.wplace-theme-neon-light .resize-container { + background-color: #E0E0E1 !important; /* Solid neon white */ + border: 3px solid #203C5D !important; + border-radius: 0 !important; + backdrop-filter: none !important; /* Remove blur effects */ + box-shadow: none !important; /* Optional: remove glows */ + font-family: 'Press Start 2P', monospace !important; +} + +/* Account switching UI styles */ +.wplace-theme-neon-light .wplace-section { + margin: 15px 0; + border-radius: 0; + background: rgba(32, 60, 93, 0.05); + padding: 15px; + border: 2px solid #203C5D; + box-shadow: 0 0 20px rgba(32, 60, 93, 0.2); + font-family: var(--wplace-font); +} + +.wplace-theme-neon-light .wplace-section-title { + display: flex; + align-items: center; + gap: 8px; + font-weight: 600; + color: var(--wplace-text); + margin-bottom: 10px; + font-size: 12px; + text-transform: uppercase; + text-shadow: 0 0 5px var(--wplace-text); + font-family: var(--wplace-font); +} + +/* Auto-swap toggle switch */ +.wplace-theme-neon-light .wplace-switch { + position: relative; + display: inline-block; + width: 50px; + height: 24px; +} + +.wplace-theme-neon-light .wplace-switch input { + opacity: 0; + width: 0; + height: 0; +} + +.wplace-theme-neon-light .wplace-slider-round { + position: absolute; + cursor: pointer; + top: 0; + left: 0; + right: 0; + bottom: 0; + background-color: #f0f0f0; + transition: 0.4s; + border-radius: 0; + border: 2px solid #ccc; + box-shadow: 0 0 8px rgba(32, 60, 93, 0.15); +} + +.wplace-theme-neon-light .wplace-slider-round:before { + position: absolute; + content: ""; + height: 14px; + width: 14px; + left: 3px; + bottom: 3px; + background-color: #203C5D; + transition: 0.4s; + border-radius: 0; + box-shadow: 0 0 8px rgba(32, 60, 93, 0.3); +} + +.wplace-theme-neon-light .wplace-switch input:checked + .wplace-slider-round { + background-color: #203C5D; + box-shadow: 0 0 15px rgba(32, 60, 93, 0.4); +} + +.wplace-theme-neon-light .wplace-switch input:checked + .wplace-slider-round:before { + transform: translateX(26px); + background-color: #fff; + box-shadow: 0 0 10px rgba(32, 60, 93, 0.5); +} + +/* Account list container */ +.wplace-theme-neon-light .accounts-list-container { + max-height: 200px; + overflow-y: auto; + margin-top: 10px; + padding: 0 6px; +} + +/* Individual account item */ +.wplace-theme-neon-light .wplace-account-item { + display: flex; + justify-content: space-between; + align-items: center; + padding: 6px 8px; + margin: 4px 0; + background: rgba(32, 60, 93, 0.03); + border-radius: 0; + border: 1px solid #203C5D; + transition: all 0.3s ease; + box-shadow: 0 0 5px rgba(32, 60, 93, 0.1); + position: relative; +} + +/* Account ordering number */ +.wplace-theme-neon-light .wplace-account-number { + position: absolute; + top: 3px; + left: 3px; + width: 8px; + height: 8px; + background: #203C5D; + color: white; + border-radius: 0; + display: flex; + align-items: center; + justify-content: center; + font-size: 8px; + font-weight: bold; + z-index: 1; + border: 1px solid #203C5D; + box-shadow: 0 0 6px rgba(32, 60, 93, 0.4); + font-family: var(--wplace-font); +} + +.wplace-theme-neon-light .wplace-account-item:hover { + background: rgba(32, 60, 93, 0.08); + border-color: #203C5D; + box-shadow: 0 0 12px rgba(32, 60, 93, 0.2); + transform: translateX(2px); +} + +.wplace-theme-neon-light .wplace-account-item.is-current { + background: rgba(32, 60, 93, 0.15); + border-color: #203C5D; + box-shadow: 0 0 20px rgba(32, 60, 93, 0.3); + animation: neon-light-pulse 2s infinite; +} + +@keyframes neon-light-pulse { + 0%, 100% { box-shadow: 0 0 20px rgba(32, 60, 93, 0.3); } + 50% { box-shadow: 0 0 25px rgba(32, 60, 93, 0.4); } +} + +/* Account details */ +.wplace-theme-neon-light .wplace-account-details { + flex: 1; + margin-left: 20px; + min-width: 0; +} + +.wplace-theme-neon-light .wplace-account-name { + font-weight: 600; + color: var(--wplace-text); + font-size: 8px; + margin-bottom: 1px; + text-transform: uppercase; + text-shadow: 0 0 3px var(--wplace-text); + font-family: var(--wplace-font); + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; +} + +/* Account statistics */ +.wplace-theme-neon-light .wplace-account-stats { + display: flex; + gap: 8px; + align-items: center; + font-size: 8px; + color: rgba(32, 60, 93, 0.8); + text-transform: uppercase; + font-family: var(--wplace-font); + flex-shrink: 0; +} + +.wplace-theme-neon-light .wplace-account-stats i { + margin-right: 4px; + text-shadow: 0 0 3px currentColor; +} + +.wplace-theme-neon-light .wplace-account-stats .fas.fa-bolt { + color: #ff6b35; + text-shadow: 0 0 5px #ff6b35; +} + +.wplace-theme-neon-light .wplace-account-stats .fas.fa-tint { + color: #203C5D; + text-shadow: 0 0 5px #203C5D; +} + +/* Move Artwork Panel */ +.wplace-theme-neon-light .wplace-move-overlay { + position: fixed; + top: 0; + left: 0; + width: 100vw; + height: 100vh; + background: rgba(32, 60, 93, 0.3); + backdrop-filter: blur(4px); + z-index: 10000; + display: flex; + align-items: center; + justify-content: center; + animation: light-fade-in 0.3s ease-out; +} + +.wplace-theme-neon-light .wplace-move-panel { + background: var(--wplace-secondary); + border: 2px solid var(--wplace-accent); + border-radius: var(--wplace-radius); + padding: 20px; + box-shadow: var(--wplace-shadow), + 0 0 25px rgba(234, 156, 0, 0.6), + inset 0 0 15px rgba(234, 156, 0, 0.1); + color: var(--wplace-text); + font-family: var(--wplace-font); + text-align: center; + min-width: 200px; + animation: light-panel-pulse 2s ease-in-out infinite alternate; +} + +.wplace-theme-neon-light .wplace-move-panel h3 { + margin: 0 0 15px 0; + color: var(--wplace-accent); + text-shadow: 0 0 8px var(--wplace-accent); + font-size: 14px; + font-weight: bold; + letter-spacing: 1px; + text-transform: uppercase; +} + +.wplace-theme-neon-light .wplace-move-controls { + display: grid; + grid-template-columns: 1fr 1fr 1fr; + grid-template-rows: 1fr 1fr 1fr; + gap: 8px; + width: 150px; + height: 150px; + margin: 0 auto 15px; +} + +.wplace-theme-neon-light .wplace-move-btn { + background: linear-gradient(135deg, var(--wplace-primary) 0%, var(--wplace-secondary) 100%); + border: 2px solid var(--wplace-accent); + border-radius: var(--wplace-radius); + color: var(--wplace-accent); + font-size: 16px; + font-weight: bold; + cursor: pointer; + transition: all 0.2s cubic-bezier(0.4, 0, 0.2, 1); + display: flex; + align-items: center; + justify-content: center; + text-shadow: 0 0 5px currentColor; + box-shadow: 0 0 10px rgba(234, 156, 0, 0.3); + font-family: var(--wplace-font); +} + +.wplace-theme-neon-light .wplace-move-btn:hover { + background: linear-gradient(135deg, var(--wplace-accent) 0%, var(--wplace-highlight) 100%); + color: var(--wplace-secondary); + text-shadow: 0 0 10px currentColor; + box-shadow: 0 0 20px rgba(234, 156, 0, 0.7), + inset 0 0 15px rgba(234, 156, 0, 0.2); + transform: scale(1.05); + animation: light-glow 0.5s ease-in-out; +} + +.wplace-theme-neon-light .wplace-move-btn:active { + transform: scale(0.95); + box-shadow: 0 0 15px rgba(234, 156, 0, 0.9), + inset 0 0 20px rgba(234, 156, 0, 0.3); +} + +.wplace-theme-neon-light .wplace-move-btn[data-direction="up"] { + grid-column: 2; + grid-row: 1; +} + +.wplace-theme-neon-light .wplace-move-btn[data-direction="left"] { + grid-column: 1; + grid-row: 2; +} + +.wplace-theme-neon-light .wplace-move-btn[data-direction="right"] { + grid-column: 3; + grid-row: 2; +} + +.wplace-theme-neon-light .wplace-move-btn[data-direction="down"] { + grid-column: 2; + grid-row: 3; +} + +.wplace-theme-neon-light .wplace-move-close { + background: linear-gradient(135deg, var(--wplace-error) 0%, #ff6b6b 100%); + border: 2px solid var(--wplace-error); + border-radius: var(--wplace-radius); + color: var(--wplace-secondary); + padding: 8px 16px; + font-size: 12px; + font-weight: bold; + cursor: pointer; + transition: all 0.2s ease; + text-shadow: 0 0 5px currentColor; + box-shadow: 0 0 10px rgba(255, 7, 58, 0.3); + font-family: var(--wplace-font); + text-transform: uppercase; + letter-spacing: 0.5px; +} + +.wplace-theme-neon-light .wplace-move-close:hover { + background: linear-gradient(135deg, #ff6b6b 0%, var(--wplace-error) 100%); + box-shadow: 0 0 20px rgba(255, 7, 58, 0.7), + inset 0 0 10px rgba(255, 7, 58, 0.2); + transform: scale(1.05); + animation: light-error-pulse 0.5s ease-in-out; +} + +/* Neon Light Theme Animations */ +@keyframes light-fade-in { + from { + opacity: 0; + backdrop-filter: blur(0px); + } + to { + opacity: 1; + backdrop-filter: blur(4px); + } +} + +@keyframes light-panel-pulse { + 0% { + box-shadow: var(--wplace-shadow), + 0 0 25px rgba(234, 156, 0, 0.6), + inset 0 0 15px rgba(234, 156, 0, 0.1); + } + 100% { + box-shadow: var(--wplace-shadow), + 0 0 35px rgba(234, 156, 0, 0.8), + inset 0 0 20px rgba(234, 156, 0, 0.15); + } +} + +@keyframes light-glow { + 0%, 100% { + box-shadow: 0 0 20px rgba(234, 156, 0, 0.7), + inset 0 0 15px rgba(234, 156, 0, 0.2); + } + 50% { + box-shadow: 0 0 30px rgba(234, 156, 0, 0.9), + inset 0 0 25px rgba(234, 156, 0, 0.3); + } +} + +@keyframes light-error-pulse { + 0%, 100% { + box-shadow: 0 0 20px rgba(255, 7, 58, 0.7), + inset 0 0 10px rgba(255, 7, 58, 0.2); + } + 50% { + box-shadow: 0 0 30px rgba(255, 7, 58, 0.9), + inset 0 0 15px rgba(255, 7, 58, 0.3); + } +} diff --git a/Extensions/themes/neon.css b/Extensions/themes/neon.css new file mode 100644 index 0000000..cad6dea --- /dev/null +++ b/Extensions/themes/neon.css @@ -0,0 +1,929 @@ +/* WPlace Auto-Image Bot - Neon Theme */ +/* Retro cyberpunk aesthetic with green neon glow effects */ + +.wplace-theme-neon { + /* Neon theme colors - matching upstream main neon theme */ + --wplace-primary: #1a1a2e; + --wplace-secondary: #16213e; + --wplace-accent: #0f3460; + --wplace-text: #00ff41; + --wplace-highlight: #ff6b35; + --wplace-success: #39ff14; + --wplace-error: #ff073a; + --wplace-warning: #ff0; + + /* Neon UI properties */ + --wplace-radius: 0; + --wplace-border-style: solid; + --wplace-border-width: 2px; + --wplace-border-color: #00ff41; + --wplace-shadow: 0 0 20px rgb(0 255 65 / 30%), inset 0 0 20px rgb(0 255 65 / 10%); + --wplace-backdrop: none; + --wplace-font: 'Press Start 2P', monospace, 'Courier New'; + + /* Z-index layers */ + --wplace-z-overlay: 10000; + --wplace-z-alert: 10002; + --wplace-z-settings: 10002; + + /* Feature toggles */ + --wplace-scanline: 1; + --wplace-pixel-blink: 1; + + /* Icon colors - neon variants */ + --wplace-icon-primary: #00ff41; + --wplace-icon-secondary: #39ff14; + --wplace-icon-palette: #ff6b35; + + /* Additional UI colors - neon variants */ + --wplace-danger: #ff073a; + --wplace-danger-dark: #cc0531; + --wplace-muted-text: #00ff4180; + --wplace-highlight-secondary: #fa0; + + /* Text variants - neon style */ + --wplace-text-secondary: #00ff41dd; + --wplace-text-muted: #00ff41bb; + --wplace-text-dim: #00ff4199; + --wplace-text-faded: #00ff41cc; + + /* Background variants - neon style */ + --wplace-bg-input: rgb(0 255 65 / 15%); + --wplace-bg-subtle: rgb(0 255 65 / 8%); + --wplace-bg-faint: rgb(0 255 65 / 5%); + --wplace-bg-ghost: rgb(0 255 65 / 3%); + --wplace-bg-whisper: rgb(0 255 65 / 2%); + + /* Border variants - neon style */ + --wplace-border-subtle: rgb(0 255 65 / 40%); + --wplace-border-faint: rgb(0 255 65 / 25%); + --wplace-border-ghost: rgb(0 255 65 / 15%); + --wplace-border-ultra-faint: rgb(0 255 65 / 8%); + + /* Shadow variants - neon style */ + --wplace-shadow-drag: 0 12px 40px rgb(0 255 65 / 60%), 0 0 0 2px rgb(0 255 65 / 80%), 0 0 20px rgb(0 255 65 / 30%); + --wplace-shadow-notification: 0 4px 12px rgb(0 255 65 / 40%), 0 0 15px rgb(0 255 65 / 20%); + --wplace-shadow-slider-thumb: 0 3px 6px rgb(0 255 65 / 50%), 0 0 0 2px var(--wplace-icon-primary), 0 0 10px rgb(0 255 65 / 30%); + --wplace-shadow-slider-hover: 0 4px 8px rgb(0 255 65 / 60%), 0 0 0 3px var(--wplace-icon-primary), 0 0 15px rgb(0 255 65 / 40%); + + /* Animation colors - neon style */ + --wplace-pulse-start: rgb(0 255 65 / 80%); + --wplace-pulse-mid: rgb(0 255 65 / 0%); + --wplace-pulse-end: rgb(0 255 65 / 0%); + + /* Slider colors - neon style */ + --wplace-slider-thumb-bg: #00ff41; +} + +/* Neon-specific styling overrides */ +.wplace-theme-neon #wplace-image-bot-container { + background: var(--wplace-primary) !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; +} + +/* 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; +} + +/* Move Artwork Panel Styles for Neon Theme */ +.wplace-theme-neon .wplace-move-panel { + position: fixed; + top: 50%; + left: 50%; + transform: translate(-50%, -50%); + background: var(--wplace-primary) !important; + border: 2px solid var(--wplace-text) !important; + border-radius: 0 !important; + padding: 20px; + z-index: 10000; + box-shadow: 0 0 20px var(--wplace-text), inset 0 0 20px rgb(0 255 65 / 10%) !important; + min-width: 200px; + text-align: center; + font-family: var(--wplace-font) !important; + color: var(--wplace-text) !important; +} + +.wplace-theme-neon .wplace-move-panel-title { + color: var(--wplace-text) !important; + margin: 0 0 15px 0; + font-size: 12px !important; + text-transform: uppercase; + text-shadow: 0 0 5px var(--wplace-text) !important; + font-family: var(--wplace-font) !important; + animation: pixel-blink 2s infinite; +} + +.wplace-theme-neon .wplace-move-controls { + display: grid; + grid-template-columns: 1fr 1fr 1fr; + gap: 4px; + margin-bottom: 15px; +} + +.wplace-theme-neon .wplace-move-btn { + padding: 8px !important; + background: var(--wplace-secondary) !important; + color: var(--wplace-text) !important; + border: 1px solid var(--wplace-text) !important; + border-radius: 0 !important; + cursor: pointer; + font-family: var(--wplace-font) !important; + font-size: 10px !important; + text-shadow: 0 0 5px var(--wplace-text) !important; + transition: all 0.2s ease; + text-transform: uppercase; +} + +.wplace-theme-neon .wplace-move-btn:hover { + background: var(--wplace-accent) !important; + box-shadow: 0 0 15px var(--wplace-text) !important; + animation: pixel-blink 0.5s infinite; +} + +.wplace-theme-neon .wplace-move-btn:active { + background: var(--wplace-text) !important; + color: var(--wplace-primary) !important; + box-shadow: 0 0 20px var(--wplace-text) !important; +} + +.wplace-theme-neon .wplace-move-center { + color: var(--wplace-highlight) !important; + padding: 8px; + font-size: 9px !important; + font-weight: bold; + display: flex; + align-items: center; + justify-content: center; + text-shadow: 0 0 5px var(--wplace-highlight) !important; + text-transform: uppercase; + font-family: var(--wplace-font) !important; +} + +.wplace-theme-neon .wplace-move-close-btn { + background: var(--wplace-error) !important; + color: white !important; + border: 1px solid var(--wplace-error) !important; + padding: 8px 12px; + border-radius: 0 !important; + cursor: pointer; + font-family: var(--wplace-font) !important; + font-size: 9px !important; + text-transform: uppercase; + text-shadow: 0 0 5px white !important; + transition: all 0.2s ease; +} + +.wplace-theme-neon .wplace-move-close-btn:hover { + box-shadow: 0 0 15px var(--wplace-error) !important; + animation: pixel-blink 0.5s infinite; +} + +.wplace-theme-neon .wplace-move-overlay { + position: fixed; + top: 0; + left: 0; + width: 100%; + height: 100%; + background: rgba(26, 26, 46, 0.9) !important; + z-index: 9999; +} + +/* Neon theme animations */ +@keyframes pixel-blink { + 0%, 50% { opacity: 1; } + 51%, 100% { opacity: 0.7; } +} + +.wplace-theme-neon #wplace-stats-container { + background: var(--wplace-primary) !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; + position: fixed !important; + overflow: hidden !important; + z-index: 9998 !important; +} + +.wplace-theme-neon .wplace-header { + background: var(--wplace-secondary) !important; + border-bottom: 1px solid var(--wplace-text) !important; + color: var(--wplace-text) !important; + text-shadow: 0 0 10px var(--wplace-text) !important; + font-family: var(--wplace-font) !important; +} + +.wplace-theme-neon .wplace-section { + background: rgb(22 33 62 / 50%) !important; + border: 1px solid var(--wplace-text) !important; + border-radius: 0 !important; +} + +.wplace-theme-neon .wplace-status-section { + background: rgb(22 33 62 / 50%) !important; + border: 1px solid var(--wplace-text) !important; + border-radius: 0 !important; +} + +.wplace-theme-neon .wplace-section-title { + color: var(--wplace-text) !important; + text-shadow: 0 0 8px var(--wplace-text) !important; + text-transform: uppercase !important; + font-family: var(--wplace-font) !important; + font-size: 10px !important; +} + +.wplace-theme-neon .wplace-btn { + background: var(--wplace-secondary) !important; + border: 1px solid var(--wplace-text) !important; + border-radius: 0 !important; + color: var(--wplace-text) !important; + text-shadow: 0 0 5px var(--wplace-text) !important; + font-family: var(--wplace-font) !important; + font-size: 9px !important; + text-transform: uppercase !important; +} + +/* Pixel blinking and neon glow animation for buttons */ +.wplace-theme-neon .wplace-btn:hover:not(:disabled) { + box-shadow: 0 0 15px var(--wplace-text), inset 0 0 15px rgb(0 255 65 / 10%) !important; + animation: pixel-blink 0.5s infinite, neon-glow 1s ease-in-out infinite alternate !important; +} + +.wplace-theme-neon .wplace-btn-start { + background: var(--wplace-secondary) !important; + border-color: var(--wplace-success) !important; + color: var(--wplace-success) !important; + text-shadow: 0 0 8px var(--wplace-success) !important; +} + +.wplace-theme-neon .wplace-btn-stop { + background: var(--wplace-secondary) !important; + border-color: var(--wplace-error) !important; + color: var(--wplace-error) !important; + text-shadow: 0 0 8px var(--wplace-error) !important; +} + +.wplace-theme-neon .wplace-btn-upload { + background: var(--wplace-secondary) !important; + border: 1px dashed var(--wplace-highlight) !important; + color: var(--wplace-highlight) !important; + text-shadow: 0 0 8px var(--wplace-highlight) !important; +} + +.wplace-theme-neon .wplace-btn-select { + background: var(--wplace-secondary) !important; + border-color: var(--wplace-highlight) !important; + color: var(--wplace-highlight) !important; + text-shadow: 0 0 8px var(--wplace-highlight) !important; +} + +.wplace-theme-neon .wplace-btn-file { + background: var(--wplace-secondary) !important; + border-color: var(--wplace-warning) !important; + color: var(--wplace-warning) !important; + text-shadow: 0 0 8px var(--wplace-warning) !important; +} + +.wplace-theme-neon .wplace-progress { + background: rgb(0 0 0 / 80%) !important; + border: 1px solid var(--wplace-text) !important; + border-radius: 0 !important; +} + +.wplace-theme-neon .wplace-progress-bar { + background: linear-gradient(90deg, var(--wplace-success) 0%, var(--wplace-text) 100%) !important; + box-shadow: 0 0 10px var(--wplace-success) !important; +} + +.wplace-theme-neon .wplace-stat-value { + color: var(--wplace-text) !important; + text-shadow: 0 0 5px var(--wplace-text) !important; +} + +.wplace-theme-neon .status-success { + background: rgb(57 255 20 / 10%) !important; + border-color: var(--wplace-success) !important; + color: var(--wplace-success) !important; + box-shadow: 0 0 15px var(--wplace-success) !important; + text-shadow: 0 0 8px var(--wplace-success) !important; +} + +.wplace-theme-neon .status-error { + background: rgb(255 7 58 / 10%) !important; + border-color: var(--wplace-error) !important; + color: var(--wplace-error) !important; + box-shadow: 0 0 15px var(--wplace-error) !important; + text-shadow: 0 0 8px var(--wplace-error) !important; +} + +.wplace-theme-neon .status-default { + background: rgb(0 255 65 / 10%) !important; + border-color: var(--wplace-text) !important; + color: var(--wplace-text) !important; + text-shadow: 0 0 5px var(--wplace-text) !important; +} + +/* Settings dialog neon styling */ +.wplace-theme-neon #wplace-settings-container { + background: var(--wplace-primary) !important; + border: 2px solid var(--wplace-text) !important; + border-radius: 0 !important; + box-shadow: 0 0 30px var(--wplace-text), inset 0 0 30px rgb(0 255 65 / 10%) !important; + font-family: var(--wplace-font) !important; +} + +.wplace-theme-neon .wplace-settings-header { + background: var(--wplace-secondary) !important; + border-bottom: 1px solid var(--wplace-text) !important; +} + +.wplace-theme-neon .wplace-settings-header h3 { + color: var(--wplace-text) !important; + text-shadow: 0 0 10px var(--wplace-text) !important; + font-family: var(--wplace-font) !important; + font-size: 16px !important; + text-transform: uppercase !important; +} + +.wplace-theme-neon .wplace-settings-close-btn { + background: var(--wplace-secondary) !important; + border: 1px solid var(--wplace-text) !important; + border-radius: 0 !important; + color: var(--wplace-text) !important; +} + +.wplace-theme-neon .wplace-settings-close-btn:hover { + background: var(--wplace-error) !important; + box-shadow: 0 0 15px var(--wplace-error) !important; +} + +.wplace-theme-neon .wplace-settings-section-wrapper { + background: rgb(22 33 62 / 30%) !important; + border: 1px solid var(--wplace-text) !important; + border-radius: 0 !important; +} + +.wplace-theme-neon .wplace-settings-select { + background: var(--wplace-secondary) !important; + border: 1px solid var(--wplace-text) !important; + border-radius: 0 !important; + color: var(--wplace-text) !important; + font-family: var(--wplace-font) !important; + font-size: 11px !important; +} + +.wplace-theme-neon .wplace-settings-section-label { + color: var(--wplace-text) !important; + text-shadow: 0 0 8px var(--wplace-text) !important; + font-family: var(--wplace-font) !important; + font-size: 12px !important; + text-transform: uppercase !important; +} + +.wplace-theme-neon .wplace-speed-value { + background: var(--wplace-secondary) !important; + border: 1px solid var(--wplace-text) !important; + border-radius: 0 !important; + color: var(--wplace-text) !important; + text-shadow: 0 0 8px var(--wplace-text) !important; + font-family: var(--wplace-font) !important; + box-shadow: 0 0 10px rgb(0 255 65 / 30%) !important; +} + +.wplace-theme-neon .wplace-overlay-opacity-value { + background: var(--wplace-secondary) !important; + border: 1px solid var(--wplace-text) !important; + border-radius: 0 !important; + color: var(--wplace-text) !important; + text-shadow: 0 0 5px var(--wplace-text) !important; +} + +/* Neon slider styling */ +.wplace-theme-neon .wplace-slider, +.wplace-theme-neon .wplace-speed-slider, +.wplace-theme-neon .wplace-overlay-opacity-slider { + background: var(--wplace-secondary) !important; + border: 1px solid var(--wplace-text) !important; + border-radius: 0 !important; + box-shadow: 0 0 10px rgb(0 255 65 / 30%) !important; +} + +.wplace-theme-neon .wplace-slider::-webkit-slider-thumb, +.wplace-theme-neon .wplace-speed-slider::-webkit-slider-thumb, +.wplace-theme-neon .wplace-overlay-opacity-slider::-webkit-slider-thumb { + -webkit-appearance: none; + width: 16px; + height: 16px; + border-radius: 0 !important; + background: var(--wplace-slider-thumb-bg) !important; + border: 2px solid var(--wplace-text) !important; + cursor: pointer; + transition: all 0.2s ease; + box-shadow: 0 0 8px rgb(0 255 65 / 50%) !important; +} + +.wplace-theme-neon .wplace-slider::-webkit-slider-thumb:hover, +.wplace-theme-neon .wplace-speed-slider::-webkit-slider-thumb:hover, +.wplace-theme-neon .wplace-overlay-opacity-slider::-webkit-slider-thumb:hover { + transform: scale(1.1); + box-shadow: 0 0 15px var(--wplace-text) !important; +} + +/* Scanline animation for neon theme */ +.wplace-theme-neon #wplace-image-bot-container::before { + content: ''; + position: absolute; + top: 0; + left: 0; + right: 0; + height: 2px; + background: linear-gradient(90deg, transparent, var(--wplace-text), transparent); + z-index: 1; + pointer-events: none; + animation: scanline 3s linear infinite; + opacity: 0.7; +} + + +.wplace-theme-neon #wplace-stats-container::before { + content: ''; + position: absolute; + top: 0; + left: 0; + right: 0; + height: 2px; + background: linear-gradient(90deg, transparent, var(--wplace-text), transparent); + z-index: 1; + pointer-events: none; + animation: scanline-stats 4s linear infinite; + opacity: 0.7; +} + + +/* Text glow animations */ +@keyframes neon-glow { + 0%, 100% { + text-shadow: 0 0 5px currentcolor, 0 0 10px currentcolor, 0 0 15px currentcolor; + } + + 50% { + text-shadow: 0 0 2px currentcolor, 0 0 5px currentcolor, 0 0 8px currentcolor; + } +} + +@keyframes pixel-blink { + 0%, 50% { + opacity: 1; + } + + 51%, 100% { + opacity: 0.7; + } +} + +@keyframes scanline { + 0% { + transform: translateY(-100%); + } + + 100% { + transform: translateY(400px); + } +} + +@keyframes scanline-stats { + 0% { + transform: translateY(-100%); + } + + 100% { + transform: translateY(300px); + } +} + +/* Dual control layout styling for neon theme */ +.wplace-theme-neon .wplace-dual-control-compact { + display: flex; + flex-direction: row; + align-items: center; + gap: 8px; + margin: 6px 0; + flex-wrap: wrap; +} + +.wplace-theme-neon .wplace-slider-container-compact { + display: flex; + align-items: center; + gap: 6px; + flex: 1; + min-width: 160px; +} + +.wplace-theme-neon .wplace-speed-slider-container-compact { + display: flex; + align-items: center; + gap: 6px; + flex: 1; + min-width: 160px; +} + +.wplace-theme-neon .wplace-input-group-compact { + display: flex; + align-items: center; + gap: 3px; + flex-shrink: 0; +} + +.wplace-theme-neon .wplace-input-btn-compact { + background: var(--wplace-secondary) !important; + border: 1px solid var(--wplace-text) !important; + border-radius: 0 !important; + color: var(--wplace-text) !important; + width: 18px; + height: 18px; + cursor: pointer; + font-weight: bold; + font-size: 10px; + display: flex; + align-items: center; + justify-content: center; + transition: all 0.2s ease; + text-shadow: 0 0 5px var(--wplace-text) !important; + font-family: var(--wplace-font) !important; +} + +.wplace-theme-neon .wplace-input-btn-compact:hover { + box-shadow: 0 0 10px var(--wplace-text) !important; + animation: pixel-blink 0.5s infinite !important; +} + +.wplace-theme-neon .wplace-number-input-compact { + background: var(--wplace-secondary) !important; + border: 1px solid var(--wplace-text) !important; + border-radius: 0 !important; + color: var(--wplace-text) !important; + padding: 2px 4px; + font-size: 10px; + width: 65px; + text-align: center; + transition: all 0.2s ease; + height: 18px; + font-family: var(--wplace-font) !important; + text-shadow: 0 0 5px var(--wplace-text) !important; +} + +.wplace-theme-neon .wplace-number-input-compact:focus { + outline: none; + box-shadow: 0 0 10px var(--wplace-text) !important; + background: rgb(0 255 65 / 15%) !important; +} + +.wplace-theme-neon .wplace-input-label-compact { + color: var(--wplace-text) !important; + font-size: 9px; + margin-left: 3px; + white-space: nowrap; + text-shadow: 0 0 5px var(--wplace-text) !important; + font-family: var(--wplace-font) !important; + text-transform: uppercase; +} + +.wplace-theme-neon .wplace-batch-size-header { + margin-bottom: 6px; +} + +.wplace-theme-neon .wplace-batch-size-label { + color: var(--wplace-text) !important; + font-weight: 500; + font-size: 11px; + text-shadow: 0 0 8px var(--wplace-text) !important; + font-family: var(--wplace-font) !important; + text-transform: uppercase; +} + +/* Cooldown control styling for neon theme */ +.wplace-theme-neon .wplace-cooldown-control { + margin: 8px 0; +} + +.wplace-theme-neon .wplace-cooldown-control label { + color: var(--wplace-text) !important; + font-weight: 500; + font-size: 11px; + text-shadow: 0 0 8px var(--wplace-text) !important; + font-family: var(--wplace-font) !important; + text-transform: uppercase; + margin-bottom: 6px; + display: block; +} + +/* Resize dialog styling for neon theme */ +.wplace-theme-neon .resize-container { + background: #1a1a2e !important; + border: 3px solid #00ff41 !important; + border-radius: 0 !important; + box-shadow: 0 0 30px rgb(0 255 65 / 50%) !important; + font-family: 'Press Start 2P', monospace !important; +} + +/* Account switching UI styles */ +.wplace-theme-neon .wplace-section { + margin: 15px 0; + border-radius: 0; + background: rgba(0, 255, 65, 0.05); + padding: 15px; + border: 2px solid #00ff41; + box-shadow: 0 0 20px rgba(0, 255, 65, 0.3); + font-family: var(--wplace-font); +} + +.wplace-theme-neon .wplace-section-title { + display: flex; + align-items: center; + gap: 8px; + font-weight: 600; + color: var(--wplace-text); + margin-bottom: 10px; + font-size: 12px; + text-transform: uppercase; + text-shadow: 0 0 8px var(--wplace-text); + font-family: var(--wplace-font); +} + +/* Auto-swap toggle switch */ +.wplace-theme-neon .wplace-switch { + position: relative; + display: inline-block; + width: 50px; + height: 24px; +} + +.wplace-theme-neon .wplace-switch input { + opacity: 0; + width: 0; + height: 0; +} + +.wplace-theme-neon .wplace-slider-round { + position: absolute; + cursor: pointer; + top: 0; + left: 0; + right: 0; + bottom: 0; + background-color: #1a1a2e; + transition: 0.4s; + border-radius: 0; + border: 2px solid #333; + box-shadow: 0 0 10px rgba(0, 255, 65, 0.2); +} + +.wplace-theme-neon .wplace-slider-round:before { + position: absolute; + content: ""; + height: 14px; + width: 14px; + left: 3px; + bottom: 3px; + background-color: #fff; + transition: 0.4s; + border-radius: 0; + box-shadow: 0 0 8px rgba(255, 255, 255, 0.5); +} + +.wplace-theme-neon .wplace-switch input:checked + .wplace-slider-round { + background-color: #00ff41; + box-shadow: 0 0 20px rgba(0, 255, 65, 0.6); +} + +.wplace-theme-neon .wplace-switch input:checked + .wplace-slider-round:before { + transform: translateX(26px); + box-shadow: 0 0 12px rgba(0, 255, 65, 0.8); +} + +/* Account list container */ +.wplace-theme-neon .accounts-list-container { + max-height: 200px; + overflow-y: auto; + margin-top: 10px; + padding: 0 6px; +} + +/* Individual account item */ +.wplace-theme-neon .wplace-account-item { + display: flex; + justify-content: space-between; + align-items: center; + padding: 6px 8px; + margin: 4px 0; + background: rgba(0, 255, 65, 0.03); + border-radius: 0; + border: 1px solid #00ff41; + transition: all 0.3s ease; + box-shadow: 0 0 5px rgba(0, 255, 65, 0.2); + position: relative; +} + +/* Account ordering number */ +.wplace-theme-neon .wplace-account-number { + position: absolute; + top: 3px; + left: 3px; + width: 8px; + height: 8px; + background: #00ff41; + color: #1a1a2e; + border-radius: 0; + display: flex; + align-items: center; + justify-content: center; + font-size: 8px; + font-weight: bold; + z-index: 1; + border: 1px solid #00ff41; + box-shadow: 0 0 8px rgba(0, 255, 65, 0.6); + font-family: var(--wplace-font); +} + +.wplace-theme-neon .wplace-account-item:hover { + background: rgba(0, 255, 65, 0.08); + border-color: #00ff41; + box-shadow: 0 0 15px rgba(0, 255, 65, 0.4); + transform: translateX(2px); +} + +.wplace-theme-neon .wplace-account-item.is-current { + background: rgba(0, 255, 65, 0.15); + border-color: #00ff41; + box-shadow: 0 0 25px rgba(0, 255, 65, 0.6); + animation: neon-pulse 2s infinite; +} + +@keyframes neon-pulse { + 0%, 100% { box-shadow: 0 0 25px rgba(0, 255, 65, 0.6); } + 50% { box-shadow: 0 0 35px rgba(0, 255, 65, 0.8); } +} + +/* Account details */ +.wplace-theme-neon .wplace-account-details { + flex: 1; + margin-left: 20px; + min-width: 0; +} + +.wplace-theme-neon .wplace-account-name { + font-weight: 600; + color: var(--wplace-text); + font-size: 8px; + margin-bottom: 1px; + text-transform: uppercase; + text-shadow: 0 0 5px var(--wplace-text); + font-family: var(--wplace-font); + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; +} + +/* Account statistics */ +.wplace-theme-neon .wplace-account-stats { + display: flex; + gap: 8px; + align-items: center; + font-size: 8px; + color: rgba(0, 255, 65, 0.8); + text-transform: uppercase; + font-family: var(--wplace-font); + flex-shrink: 0; +} + +.wplace-theme-neon .wplace-account-stats i { + margin-right: 4px; + text-shadow: 0 0 5px currentColor; +} + +.wplace-theme-neon .wplace-account-stats .fas.fa-bolt { + color: #ffff00; + text-shadow: 0 0 8px #ffff00; +} + +.wplace-theme-neon .wplace-account-stats .fas.fa-tint { + color: #00bfff; + text-shadow: 0 0 8px #00bfff; +} +