} true if tiles are ready
+ */
+ async waitForTiles(
+ startRegionX,
+ startRegionY,
+ pixelWidth,
+ pixelHeight,
+ startPixelX = 0,
+ startPixelY = 0,
+ timeoutMs = 10000
+ ) {
+ 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;
+
+ const startTime = Date.now();
+
+ while (Date.now() - startTime < timeoutMs) {
+ if (state.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 Utils.sleep(100);
+ }
+
+ console.warn(`❌ Timeout waiting for tiles: ${requiredTiles.length} required,
+ ${requiredTiles.filter((k) => this.originalTiles.has(k)).length} loaded`);
+ return false;
+ }
}
const overlayManager = new OverlayManager();
// Optimized Turnstile token handling with improved caching and retry logic
- 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 // Maximum attempts for batch sending
- const TOKEN_LIFETIME = 240000 // 4 minutes (tokens typically last 5 min, use 4 for safety)
+ 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; // Maximum attempts for batch sending
+ const TOKEN_LIFETIME = 240000; // 4 minutes (tokens typically last 5 min, use 4 for safety)
function setTurnstileToken(token) {
if (_resolveToken) {
- _resolveToken(token)
- _resolveToken = null
+ _resolveToken(token);
+ _resolveToken = null;
}
- turnstileToken = token
- tokenExpiryTime = Date.now() + TOKEN_LIFETIME
- console.log("✅ Turnstile token set successfully")
+ turnstileToken = token;
+ tokenExpiryTime = Date.now() + TOKEN_LIFETIME;
+ console.log('✅ Turnstile token set successfully');
}
function isTokenValid() {
- return turnstileToken && Date.now() < tokenExpiryTime
+ return turnstileToken && Date.now() < tokenExpiryTime;
}
function invalidateToken() {
- turnstileToken = null
- tokenExpiryTime = 0
- console.log("🗑️ Token invalidated, will force fresh generation")
+ turnstileToken = null;
+ tokenExpiryTime = 0;
+ console.log('🗑️ Token invalidated, will force fresh generation');
}
async function ensureToken(forceRefresh = false) {
@@ -913,31 +1091,31 @@ function applyTheme() {
// Avoid multiple simultaneous token generations
if (tokenGenerationInProgress) {
- console.log("🔄 Token generation already in progress, waiting...");
+ 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...");
+ 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");
+ console.log('✅ Token captured and cached successfully');
return token;
}
- console.log("⚠️ Invisible Turnstile failed, forcing browser automation...");
+ 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");
+ console.log('✅ Fallback token captured successfully');
return fallbackToken;
}
- console.log("❌ All token generation methods failed");
+ console.log('❌ All token generation methods failed');
return null;
} finally {
tokenGenerationInProgress = false;
@@ -945,26 +1123,56 @@ function applyTheme() {
}
async function handleCaptchaWithRetry() {
- const startTime = Date.now();
- try {
- const sitekey = Utils.detectSitekey();
- console.log("🔑 Generating Turnstile token for sitekey:", sitekey);
+ const startTime = performance.now();
- if (typeof window !== "undefined" && window.navigator) {
- console.log("🧭 UA:", window.navigator.userAgent, "Platform:", window.navigator.platform);
+ try {
+ const { sitekey, token: preGeneratedToken } = await Utils.obtainSitekeyAndToken();
+
+ if (!sitekey) {
+ throw new Error('No valid sitekey found');
}
- const token = await Utils.generatePaintToken(sitekey);
- if (token && token.length > 20) {
- const elapsed = Math.round(Date.now() - startTime);
+ 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");
+ throw new Error(`Invalid or empty token received - Length: ${token?.length || 0}`);
}
} catch (error) {
- const elapsed = Math.round(Date.now() - startTime);
- console.log(`❌ Turnstile token generation failed after ${elapsed}ms:`, error);
+ const elapsed = Math.round(performance.now() - startTime);
+ console.error(`❌ Turnstile token generation failed after ${elapsed}ms:`, error);
throw error;
}
}
@@ -972,7 +1180,7 @@ function applyTheme() {
async function handleCaptchaFallback() {
// Implementation for fallback token generation would go here
// This is a placeholder for browser automation fallback
- console.log("🔄 Attempting fallback token generation...");
+ console.log('🔄 Attempting fallback token generation...');
return null;
}
@@ -1000,17 +1208,30 @@ function applyTheme() {
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];
+ const url = args[0] instanceof Request ? args[0].url : args[0];
- if (typeof url === "string") {
- if (url.includes("https://backend.wplace.live/s0/pixel/")) {
+ if (typeof url === 'string') {
+ if (url.includes('https://backend.wplace.live/s0/pixel/')) {
try {
const payload = JSON.parse(args[1].body);
if (payload.t) {
- console.log("✅ Turnstile Token Captured:", payload.t);
+ // 📊 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 */ }
+ } catch (_) {
+ /* ignore */
+ }
}
const contentType = response.headers.get('content-type') || '';
@@ -1021,19 +1242,24 @@ function applyTheme() {
const originalBlob = await cloned.blob();
fetchedBlobQueue.set(blobUUID, (processedBlob) => {
- resolve(new Response(processedBlob, {
- headers: cloned.headers,
- status: cloned.status,
- statusText: cloned.statusText
- }));
+ 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,
- }, '*');
+ window.postMessage(
+ {
+ source: 'auto-image-tile',
+ endpoint: url,
+ blobID: blobUUID,
+ blobData: originalBlob,
+ },
+ '*'
+ );
});
}
}
@@ -1051,22 +1277,22 @@ function applyTheme() {
if (source === 'turnstile-capture' && token) {
setTurnstileToken(token);
- if (document.querySelector("#statusText")?.textContent.includes("CAPTCHA")) {
- Utils.showAlert(Utils.t("tokenCapturedSuccess"), "success");
- updateUI("colorsFound", "success", { count: state.availableColors.length });
+ if (document.querySelector('#statusText')?.textContent.includes('CAPTCHA')) {
+ Utils.showAlert(Utils.t('tokenCapturedSuccess'), 'success');
+ updateUI('colorsFound', 'success', { count: state.availableColors.length });
}
}
});
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"
+ 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"
+ state.language = navigator.language.startsWith('pt') ? 'pt' : 'en';
}
}
@@ -1074,6 +1300,15 @@ function applyTheme() {
const Utils = {
sleep: (ms) => new Promise((r) => setTimeout(r, ms)),
+ 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());
+ }
+ },
+
waitForSelector: async (selector, interval = 200, timeout = 5000) => {
const start = Date.now();
while (Date.now() - start < timeout) {
@@ -1084,7 +1319,47 @@ function applyTheme() {
return null;
},
- // Turnstile Generator Integration - Optimized with widget reuse and proper cleanup
+ 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`;
+ },
+
+ /**
+ * Calculate the range of tile coordinates (in region space) that cover a given image area.
+ * @param {number} startRegionX - Base region X
+ * @param {number} startRegionY - Base region Y
+ * @param {number} startPixelX - Starting pixel X within the region grid
+ * @param {number} startPixelY - Starting pixel Y within the region grid
+ * @param {number} width - Image width in pixels
+ * @param {number} height - Image height in pixels
+ * @param {number} tileSize - Size of a 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),
+ };
+ }, // Turnstile Generator Integration - Optimized with widget reuse and proper cleanup
turnstileLoaded: false,
_turnstileContainer: null,
_turnstileOverlay: null,
@@ -1097,10 +1372,14 @@ function applyTheme() {
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"]')) {
+ if (
+ document.querySelector(
+ 'script[src^="https://challenges.cloudflare.com/turnstile/v0/api.js"]'
+ )
+ ) {
const checkReady = () => {
if (window.turnstile) {
this.turnstileLoaded = true;
@@ -1111,32 +1390,32 @@ function applyTheme() {
};
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");
+ console.log('✅ Turnstile script loaded successfully');
resolve();
};
script.onerror = () => {
- console.error("❌ Failed to load Turnstile script");
+ 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
+ // Create or reuse the turnstile container - completely hidden for token generation
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');
@@ -1158,7 +1437,7 @@ function applyTheme() {
const title = document.createElement('div');
title.textContent = Utils.t('turnstileInstructions');
- title.style.cssText = 'font: 600 12px/1.3 "Segoe UI",sans-serif; margin-bottom: 8px; opacity: 0.9;';
+ title.className = 'wplace-turnstile-title';
const host = document.createElement('div');
host.id = 'turnstile-overlay-host';
@@ -1184,17 +1463,19 @@ function applyTheme() {
// Try reusing existing widget first if sitekey matches
if (this._turnstileWidgetId && this._lastSitekey === sitekey && window.turnstile?.execute) {
try {
- console.log("🔄 Reusing existing Turnstile widget...");
+ 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))
+ new Promise((_, reject) =>
+ setTimeout(() => reject(new Error('Execute timeout')), 15000)
+ ),
]);
if (token && token.length > 20) {
- console.log("✅ Token generated via widget reuse");
+ console.log('✅ Token generated via widget reuse');
return token;
}
} catch (error) {
- console.log("� Widget reuse failed, will create a fresh widget:", error.message);
+ console.log('� Widget reuse failed, will create a fresh widget:', error.message);
}
}
@@ -1204,7 +1485,7 @@ function applyTheme() {
return invisibleToken;
}
- console.log("� Falling back to interactive Turnstile (visible).");
+ console.log('� Falling back to interactive Turnstile (visible).');
return await this.createTurnstileWidgetInteractive(sitekey, action);
},
@@ -1213,24 +1494,24 @@ function applyTheme() {
try {
// Force cleanup of any existing widget
if (this._turnstileWidgetId && window.turnstile?.remove) {
- try {
- window.turnstile.remove(this._turnstileWidgetId);
+ 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,
@@ -1243,12 +1524,12 @@ function applyTheme() {
resolve(token);
},
'error-callback': () => resolve(null),
- 'timeout-callback': () => resolve(null)
+ 'timeout-callback': () => resolve(null),
});
-
+
this._turnstileWidgetId = widgetId;
this._lastSitekey = sitekey;
-
+
if (!widgetId) {
return resolve(null);
}
@@ -1256,8 +1537,12 @@ function applyTheme() {
// 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));
+ 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);
@@ -1268,25 +1553,25 @@ function applyTheme() {
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);
+ 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');
@@ -1294,7 +1579,7 @@ function applyTheme() {
overlay.style.display = 'none';
resolve(null);
}, 60000); // 60 seconds for user interaction
-
+
const widgetId = window.turnstile.render(host, {
sitekey,
action,
@@ -1303,9 +1588,9 @@ function applyTheme() {
callback: (token) => {
clearTimeout(timeout);
overlay.classList.add('wplace-overlay-hidden');
- overlay.style.display = 'none';
+ overlay.style.display = 'none';
console.log('✅ Interactive Turnstile completed successfully');
-
+
if (typeof token === 'string' && token.length > 20) {
resolve(token);
} else {
@@ -1316,15 +1601,15 @@ function applyTheme() {
'error-callback': (error) => {
clearTimeout(timeout);
overlay.classList.add('wplace-overlay-hidden');
- overlay.style.display = 'none';
+ 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');
@@ -1341,10 +1626,6 @@ function applyTheme() {
});
},
- async generatePaintToken(sitekey) {
- return this.executeTurnstile(sitekey, 'paint');
- },
-
// Cleanup method for when the script is disabled/reloaded
cleanupTurnstile() {
if (this._turnstileWidgetId && window.turnstile?.remove) {
@@ -1354,26 +1635,32 @@ function applyTheme() {
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;
},
- detectSitekey(fallback = '0x4AAAAAABpqJe8FO0N84q0F') {
+ async obtainSitekeyAndToken(fallback = '0x4AAAAAABpqJe8FO0N84q0F') {
// Cache sitekey to avoid repeated DOM queries
if (this._cachedSitekey) {
- console.log("🔍 Using cached sitekey:", this._cachedSitekey);
- return 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
@@ -1382,112 +1669,141 @@ function applyTheme() {
'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 {
- // Try to find sitekey in data attributes
+ // 1️⃣ data-sitekey attribute
const sitekeySel = document.querySelector('[data-sitekey]');
if (sitekeySel) {
const sitekey = sitekeySel.getAttribute('data-sitekey');
- if (sitekey && sitekey.length > 10) {
- this._cachedSitekey = sitekey;
- console.log("🔍 Sitekey detected from data attribute:", sitekey);
- return sitekey;
+ const result = await trySitekey(sitekey, 'data attribute');
+ if (result) {
+ return result;
}
}
- // Try turnstile element
+ // 2️⃣ Turnstile element
const turnstileEl = document.querySelector('.cf-turnstile');
- if (turnstileEl?.dataset?.sitekey && turnstileEl.dataset.sitekey.length > 10) {
- this._cachedSitekey = turnstileEl.dataset.sitekey;
- console.log("🔍 Sitekey detected from turnstile element:", this._cachedSitekey);
- return this._cachedSitekey;
+ if (turnstileEl?.dataset?.sitekey) {
+ const sitekey = turnstileEl.dataset.sitekey;
+ const result = await trySitekey(sitekey, 'turnstile element');
+ if (result) {
+ return result;
+ }
}
- // Try to find sitekey in meta tags
- const metaTags = document.querySelectorAll('meta[name*="turnstile"], meta[property*="turnstile"]');
+ // 3️⃣ Meta tags
+ const metaTags = document.querySelectorAll(
+ 'meta[name*="turnstile"], meta[property*="turnstile"]'
+ );
for (const meta of metaTags) {
const content = meta.getAttribute('content');
- if (content && content.length > 10) {
- this._cachedSitekey = content;
- console.log("🔍 Sitekey detected from meta tag:", this._cachedSitekey);
- return this._cachedSitekey;
+ const result = await trySitekey(content, 'meta tag');
+ if (result) {
+ return result;
}
}
- // Try global variable
- if (typeof window !== 'undefined' && window.__TURNSTILE_SITEKEY && window.__TURNSTILE_SITEKEY.length > 10) {
- this._cachedSitekey = window.__TURNSTILE_SITEKEY;
- console.log("🔍 Sitekey detected from global variable:", this._cachedSitekey);
- return this._cachedSitekey;
+ // 4️⃣ Global variable
+ if (window.__TURNSTILE_SITEKEY) {
+ const result = await trySitekey(window.__TURNSTILE_SITEKEY, 'global variable');
+ if (result) {
+ return result;
+ }
}
- // Try script tags for inline sitekey
+ // 5️⃣ Script tags
const scripts = document.querySelectorAll('script');
for (const script of scripts) {
const content = script.textContent || script.innerHTML;
- const sitekeyMatch = content.match(/sitekey['":\s]+(['"0-9a-zA-X_-]{20,})/i);
- if (sitekeyMatch && sitekeyMatch[1] && sitekeyMatch[1].length > 10) {
- this._cachedSitekey = sitekeyMatch[1].replace(/['"]/g, '');
- console.log("🔍 Sitekey detected from script content:", this._cachedSitekey);
- return this._cachedSitekey;
+ 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;
+ }
}
}
- // If no sitekey found through detection, try the known working sitekeys
- console.log("🔍 No sitekey detected, trying known working sitekeys...");
+ // 6️⃣ Known potential sitekeys
+ console.log('🔍 Testing known potential sitekeys...');
for (const testSitekey of potentialSitekeys) {
- console.log("🔍 Trying sitekey:", testSitekey);
- this._cachedSitekey = testSitekey;
- return testSitekey;
+ const result = await trySitekey(testSitekey, 'known list');
+ if (result) {
+ return result;
+ }
}
-
} catch (error) {
- console.warn('Error detecting sitekey:', error);
+ console.warn('⚠️ Error during sitekey detection:', error);
}
-
- console.log("🔍 Using fallback sitekey:", fallback);
- this._cachedSitekey = fallback;
- return fallback;
+
+ // 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 };
},
createElement: (tag, props = {}, children = []) => {
- const element = document.createElement(tag)
+ const element = document.createElement(tag);
Object.entries(props).forEach(([key, value]) => {
if (key === 'style' && typeof value === 'object') {
- Object.assign(element.style, value)
+ Object.assign(element.style, value);
} else if (key === 'className') {
- element.className = value
+ element.className = value;
} else if (key === 'innerHTML') {
- element.innerHTML = value
+ element.innerHTML = value;
} else {
- element.setAttribute(key, value)
+ element.setAttribute(key, value);
}
- })
+ });
if (typeof children === 'string') {
- element.textContent = children
+ element.textContent = children;
} else if (Array.isArray(children)) {
- children.forEach(child => {
+ children.forEach((child) => {
if (typeof child === 'string') {
- element.appendChild(document.createTextNode(child))
+ element.appendChild(document.createTextNode(child));
} else {
- element.appendChild(child)
+ element.appendChild(child);
}
- })
+ });
}
- return element
+ return element;
},
createButton: (id, text, icon, onClick, style = CONFIG.CSS_CLASSES.BUTTON_PRIMARY) => {
const button = Utils.createElement('button', {
id: id,
style: style,
- innerHTML: `${icon ? `` : ''}${text}`
- })
- if (onClick) button.addEventListener('click', onClick)
- return button
+ innerHTML: `${icon ? `` : ''}${text}`,
+ });
+ if (onClick) button.addEventListener('click', onClick);
+ return button;
},
// Synchronous translation function for UI rendering
@@ -1527,31 +1843,32 @@ function applyTheme() {
Object.keys(params).forEach((param) => {
text = text.replace(new RegExp(`\\{${param}\\}`, 'g'), params[param]);
});
-
+
// Log missing translations for debugging
if (text === key && key !== 'undefined') {
console.warn(`⚠️ Missing translation for key: ${key} (language: ${state.language})`);
}
-
+
return text;
},
- showAlert: (message, type = "info") => {
- const alertDiv = document.createElement("div")
- alertDiv.className = `wplace-alert-base wplace-alert-${type}`
+ showAlert: (message, type = 'info') => {
+ const alertDiv = document.createElement('div');
+ alertDiv.className = `wplace-alert-base wplace-alert-${type}`;
- alertDiv.textContent = message
- document.body.appendChild(alertDiv)
+ alertDiv.textContent = message;
+ document.body.appendChild(alertDiv);
setTimeout(() => {
- alertDiv.style.animation = "slideDown 0.3s ease-out reverse"
+ alertDiv.style.animation = 'slide-down 0.3s ease-out reverse';
setTimeout(() => {
- document.body.removeChild(alertDiv)
- }, 300)
- }, 4000)
+ document.body.removeChild(alertDiv);
+ }, 300);
+ }, 4000);
},
- 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)),
+ 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)),
_labCache: new Map(), // key: (r<<16)|(g<<8)|b value: [L,a,b]
_rgbToLab: (r, g, b) => {
// sRGB -> linear
@@ -1566,10 +1883,12 @@ function applyTheme() {
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.00000;
+ 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 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);
@@ -1588,8 +1907,8 @@ function applyTheme() {
// Use provided palette or derive from COLOR_MAP
if (!palette || palette.length === 0) {
palette = Object.values(CONFIG.COLOR_MAP)
- .filter(c => c.rgb)
- .map(c => [c.rgb.r, c.rgb.g, c.rgb.b]);
+ .filter((c) => c.rgb)
+ .map((c) => [c.rgb.r, c.rgb.g, c.rgb.b]);
}
if (state.colorMatchingAlgorithm === 'legacy') {
let menorDist = Infinity;
@@ -1600,7 +1919,11 @@ function applyTheme() {
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));
+ 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];
@@ -1641,129 +1964,257 @@ function applyTheme() {
return r >= wt && g >= wt && b >= wt;
},
+ resolveColor(targetRgb, availableColors, exactMatch = false) {
+ if (!availableColors || availableColors.length === 0) {
+ return {
+ id: null,
+ rgb: targetRgb,
+ };
+ }
+
+ const cacheKey = `${targetRgb[0]},${targetRgb[1]},${targetRgb[2]}|${state.colorMatchingAlgorithm}|${
+ state.enableChromaPenalty ? 'c' : 'nc'
+ }|${state.chromaPenaltyWeight}|${exactMatch ? 'exact' : 'closest'}`;
+
+ if (colorCache.has(cacheKey)) return colorCache.get(cacheKey);
+
+ // Check for an exact color match in availableColors.
+ // If found, return the matched color with its ID.
+ // If not found, return the target color with null ID.
+ // Cache the result for future lookups.
+ 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 };
+ colorCache.set(cacheKey, result);
+ return result;
+ }
+
+ // check for white using threshold
+ const whiteThreshold = state.customWhiteThreshold || CONFIG.WHITE_THRESHOLD;
+ 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] };
+ colorCache.set(cacheKey, result);
+ return result;
+ }
+ }
+
+ // find nearest color
+ let bestId = availableColors[0].id;
+ let bestRgb = [...availableColors[0].rgb];
+ let bestScore = Infinity;
+
+ if (state.colorMatchingAlgorithm === '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] = Utils._lab(targetRgb[0], targetRgb[1], targetRgb[2]);
+ const targetChroma = Math.sqrt(at * at + bt * bt);
+ const penaltyWeight = state.enableChromaPenalty ? state.chromaPenaltyWeight || 0.15 : 0;
+
+ for (let i = 0; i < availableColors.length; i++) {
+ const c = availableColors[i];
+ const [r, g, b] = c.rgb;
+ const [L2, a2, b2] = Utils._lab(r, g, b);
+ const dL = Lt - L2,
+ da = at - a2,
+ 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 };
+ colorCache.set(cacheKey, result);
+
+ // limit the size of the cache
+ if (colorCache.size > 15000) {
+ const firstKey = colorCache.keys().next().value;
+ colorCache.delete(firstKey);
+ }
+
+ return result;
+ },
+
createImageUploader: () =>
new Promise((resolve) => {
- const input = document.createElement("input")
- input.type = "file"
- input.accept = "image/png,image/jpeg"
+ const input = document.createElement('input');
+ input.type = 'file';
+ input.accept = 'image/png,image/jpeg';
input.onchange = () => {
- const fr = new FileReader()
- fr.onload = () => resolve(fr.result)
- fr.readAsDataURL(input.files[0])
- }
- input.click()
+ const fr = new FileReader();
+ fr.onload = () => resolve(fr.result);
+ fr.readAsDataURL(input.files[0]);
+ };
+ 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)
+ 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: () =>
new Promise((resolve, reject) => {
- const input = document.createElement("input")
- input.type = "file"
- input.accept = ".json"
+ const input = document.createElement('input');
+ input.type = 'file';
+ input.accept = '.json';
input.onchange = (e) => {
- const file = e.target.files[0]
+ const file = e.target.files[0];
if (file) {
- const reader = new FileReader()
+ const reader = new FileReader();
reader.onload = () => {
try {
- const data = JSON.parse(reader.result)
- resolve(data)
+ const data = JSON.parse(reader.result);
+ resolve(data);
} catch (error) {
- reject(new Error("Invalid JSON file"))
+ reject(new Error('Invalid JSON file'));
}
- }
- reader.onerror = () => reject(new Error("File reading error"))
- reader.readAsText(file)
+ };
+ reader.onerror = () => reject(new Error('File reading error'));
+ reader.readAsText(file);
} else {
- reject(new Error("No file selected"))
+ reject(new Error('No file selected'));
}
- }
- input.click()
+ };
+ input.click();
}),
extractAvailableColors: () => {
- const colorElements = document.querySelectorAll('[id^="color-"]')
-
+ const colorElements = document.querySelectorAll('.tooltip button[id^="color-"]');
+ if (colorElements.length === 0) {
+ console.log('❌ No color elements found on page');
+ return null;
+ }
// Separate available and unavailable colors
- const availableColors = []
- const unavailableColors = []
+ 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 id = Number.parseInt(el.id.replace('color-', ''));
+ if (id === 0) return; // Skip transparent color
- const rgbStr = el.style.backgroundColor.match(/\d+/g)
- const rgb = rgbStr ? rgbStr.map(Number) : [0, 0, 0]
+ 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
- const colorInfo = Object.values(CONFIG.COLOR_MAP).find(color => color.id === id)
- const name = colorInfo ? colorInfo.name : `Unknown Color ${id}`
+ const colorInfo = Object.values(CONFIG.COLOR_MAP).find((color) => color.id === id);
+ const name = colorInfo ? colorInfo.name : `Unknown Color ${id}`;
- const colorData = { id, name, rgb }
+ const colorData = { id, name, rgb };
// Check if color is available (no SVG overlay means available)
- if (!el.querySelector("svg")) {
- availableColors.push(colorData)
+ if (!el.querySelector('svg')) {
+ availableColors.push(colorData);
} else {
- unavailableColors.push(colorData)
+ 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}`)
+ 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 ---")
+ 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]})`)
- })
+ 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 ---")
+ 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(
+ `${
+ index + 1
+ }. ID: ${color.id}, Name: "${color.name}", RGB: (${color.rgb[0]}, ${color.rgb[1]}, ${color.rgb[2]}) [LOCKED]`
+ );
+ });
}
- console.log("=== END COLOR STATUS ===")
+ console.log('=== END COLOR STATUS ===');
- return availableColors
+ return availableColors;
},
formatTime: (ms) => {
- 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))
+ 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`
+ 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
+ return result;
},
calculateEstimatedTime: (remainingPixels, charges, cooldown) => {
if (remainingPixels <= 0) return 0;
- const paintingSpeedDelay = state.paintingSpeed > 0 ? (1000 / state.paintingSpeed) : 1000;
+ const paintingSpeedDelay = state.paintingSpeed > 0 ? 1000 / state.paintingSpeed : 1000;
const timeFromSpeed = remainingPixels * paintingSpeedDelay;
const cyclesNeeded = Math.ceil(remainingPixels / Math.max(charges, 1));
@@ -1775,7 +2226,9 @@ function applyTheme() {
// --- Painted pixel tracking helpers ---
initializePaintedMap: (width, height) => {
if (!state.paintedMap || state.paintedMap.length !== height) {
- state.paintedMap = Array(height).fill().map(() => Array(width).fill(false));
+ state.paintedMap = Array(height)
+ .fill()
+ .map(() => Array(width).fill(false));
console.log(`📋 Initialized painted map: ${width}x${height}`);
}
},
@@ -1783,9 +2236,13 @@ function applyTheme() {
markPixelPainted: (x, y, regionX = 0, regionY = 0) => {
const actualX = x + regionX;
const actualY = y + regionY;
-
- if (state.paintedMap && state.paintedMap[actualY] &&
- actualX >= 0 && actualX < state.paintedMap[actualY].length) {
+
+ if (
+ state.paintedMap &&
+ state.paintedMap[actualY] &&
+ actualX >= 0 &&
+ actualX < state.paintedMap[actualY].length
+ ) {
state.paintedMap[actualY][actualX] = true;
}
},
@@ -1793,9 +2250,13 @@ function applyTheme() {
isPixelPainted: (x, y, regionX = 0, regionY = 0) => {
const actualX = x + regionX;
const actualY = y + regionY;
-
- if (state.paintedMap && state.paintedMap[actualY] &&
- actualX >= 0 && actualX < state.paintedMap[actualY].length) {
+
+ if (
+ state.paintedMap &&
+ state.paintedMap[actualY] &&
+ actualX >= 0 &&
+ actualX < state.paintedMap[actualY].length
+ ) {
return state.paintedMap[actualY][actualX];
}
return false;
@@ -1806,28 +2267,26 @@ function applyTheme() {
const now = Date.now();
const pixelsSinceLastSave = state.paintedPixels - state._lastSavePixelCount;
const timeSinceLastSave = now - state._lastSaveTime;
-
+
// Save conditions:
// 1. Every 25 pixels (reduced from 50 for more frequent saves)
// 2. At least 30 seconds since last save (prevent spam)
// 3. Not already saving
- return !state._saveInProgress &&
- pixelsSinceLastSave >= 25 &&
- timeSinceLastSave >= 30000;
+ return !state._saveInProgress && pixelsSinceLastSave >= 25 && timeSinceLastSave >= 30000;
},
performSmartSave: () => {
if (!Utils.shouldAutoSave()) return false;
-
+
state._saveInProgress = true;
const success = Utils.saveProgress();
-
+
if (success) {
state._lastSavePixelCount = state.paintedPixels;
state._lastSaveTime = Date.now();
console.log(`💾 Auto-saved at ${state.paintedPixels} pixels`);
}
-
+
state._saveInProgress = false;
return success;
},
@@ -1846,15 +2305,18 @@ function applyTheme() {
for (let x = 0; x < width; x++) {
const bit = row && row[x] ? 1 : 0;
const b = bitIndex >> 3; // byte index
- const o = bitIndex & 7; // bit offset
- if (bit) bytes[b] |= (1 << o);
+ const o = bitIndex & 7; // bit offset
+ if (bit) bytes[b] |= 1 << o;
bitIndex++;
}
}
- let binary = "";
+ 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)));
+ binary += String.fromCharCode.apply(
+ null,
+ bytes.subarray(i, Math.min(i + chunk, bytes.length))
+ );
}
return btoa(binary);
},
@@ -1864,7 +2326,9 @@ function applyTheme() {
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));
+ 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++) {
@@ -1880,8 +2344,12 @@ function applyTheme() {
// Migration helpers for backward compatibility
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;
+ const isV1 =
+ !saved.version ||
+ saved.version === '1' ||
+ saved.version === '1.0' ||
+ saved.version === '1.1';
+ if (!isV1) return saved;
try {
const migrated = { ...saved };
@@ -1904,7 +2372,11 @@ function applyTheme() {
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';
+ const isV1 =
+ !saved.version ||
+ saved.version === '1' ||
+ saved.version === '1.0' ||
+ saved.version === '1.1';
if (!isV2 && !isV1) return saved; // save this for future
try {
const migrated = { ...saved };
@@ -1926,112 +2398,201 @@ function applyTheme() {
}
},
- saveProgress: () => {
+ migrateProgressToV22: (data) => {
try {
- // Pack painted map if available
- let paintedMapPacked = null;
- if (state.paintedMap && state.imageData) {
- const data = Utils.packPaintedMapToBase64(state.paintedMap, state.imageData.width, state.imageData.height);
- if (data) {
- paintedMapPacked = {
- width: state.imageData.width,
- height: state.imageData.height,
- data: data
- };
- }
+ const migrated = { ...data };
+ migrated.version = '2.2';
+
+ // Add new fields with default values
+ if (!migrated.state.coordinateMode) {
+ migrated.state.coordinateMode = CONFIG.COORDINATE_MODE;
+ }
+ if (!migrated.state.coordinateDirection) {
+ migrated.state.coordinateDirection = CONFIG.COORDINATE_DIRECTION;
+ }
+ if (!migrated.state.coordinateSnake) {
+ migrated.state.coordinateSnake = CONFIG.COORDINATE_SNAKE;
+ }
+ if (!migrated.state.blockWidth) {
+ migrated.state.blockWidth = CONFIG.COORDINATE_BLOCK_WIDTH;
+ }
+ if (!migrated.state.blockHeight) {
+ migrated.state.blockHeight = CONFIG.COORDINATE_BLOCK_HEIGHT;
}
- const progressData = {
- timestamp: Date.now(),
- version: "2.1",
- state: {
- totalPixels: state.totalPixels,
- paintedPixels: state.paintedPixels,
- lastPosition: state.lastPosition,
- startPosition: state.startPosition,
- region: state.region,
- imageLoaded: state.imageLoaded,
- colorsChecked: state.colorsChecked,
- availableColors: state.availableColors,
- },
- imageData: state.imageData
- ? {
+ return migrated;
+ } catch (e) {
+ console.warn('Migration to v2.2 failed, using original data:', e);
+ return data;
+ }
+ },
+
+ buildPaintedMapPacked() {
+ if (state.paintedMap && state.imageData) {
+ const data = Utils.packPaintedMapToBase64(
+ state.paintedMap,
+ state.imageData.width,
+ state.imageData.height
+ );
+ if (data) {
+ return {
+ width: state.imageData.width,
+ height: state.imageData.height,
+ data: data,
+ };
+ }
+ }
+ return null;
+ },
+
+ buildProgressData() {
+ return {
+ timestamp: Date.now(),
+ version: '2.2',
+ state: {
+ totalPixels: state.totalPixels,
+ paintedPixels: state.paintedPixels,
+ lastPosition: state.lastPosition,
+ startPosition: state.startPosition,
+ region: state.region,
+ imageLoaded: state.imageLoaded,
+ colorsChecked: state.colorsChecked,
+ coordinateMode: state.coordinateMode,
+ coordinateDirection: state.coordinateDirection,
+ coordinateSnake: state.coordinateSnake,
+ blockWidth: state.blockWidth,
+ blockHeight: state.blockHeight,
+ availableColors: state.availableColors,
+ },
+ imageData: state.imageData
+ ? {
width: state.imageData.width,
height: state.imageData.height,
pixels: Array.from(state.imageData.pixels),
totalPixels: state.imageData.totalPixels,
}
- : null,
- paintedMapPacked: paintedMapPacked,
- }
+ : null,
+ paintedMapPacked: Utils.buildPaintedMapPacked(),
+ };
+ },
- localStorage.setItem("wplace-bot-progress", JSON.stringify(progressData))
- return true
+ migrateProgress(saved) {
+ if (!saved) return null;
+
+ let data = saved;
+ const ver = data.version;
+
+ // If version is missing or ≤ 1.x → first migrate to v2
+ if (!ver || ver === '1' || ver === '1.0' || ver === '1.1') {
+ data = Utils.migrateProgressToV2(data);
+ }
+
+ // If still older than v2.1 → migrate to 2.1
+ if (data.version === '2' || data.version === '2.0') {
+ data = Utils.migrateProgressToV21(data);
+ }
+
+ // If still older than v2.2 → migrate to 2.2
+ if (data.version === '2.1') {
+ data = Utils.migrateProgressToV22(data);
+ }
+
+ // Now data is guaranteed to be the latest version
+ return data;
+ },
+
+ saveProgress: () => {
+ try {
+ const progressData = Utils.buildProgressData(state);
+
+ localStorage.setItem('wplace-bot-progress', JSON.stringify(progressData));
+ return true;
} catch (error) {
- console.error("Error saving progress:", error)
- return false
+ console.error('Error saving progress:', error);
+ return false;
}
},
loadProgress: () => {
try {
- const saved = localStorage.getItem("wplace-bot-progress")
+ const saved = localStorage.getItem('wplace-bot-progress');
if (!saved) return null;
let data = JSON.parse(saved);
- const ver = data.version;
- let migrated = data;
- if (ver === '2.1') {
- // already latest
- } else if (ver === '2' || ver === '2.0') {
- migrated = Utils.migrateProgressToV21(data);
- } else {
- migrated = Utils.migrateProgressToV21(data);
- }
+ const migrated = Utils.migrateProgress(data);
+
if (migrated && migrated !== data) {
- try { localStorage.setItem("wplace-bot-progress", JSON.stringify(migrated)); } catch {}
- data = migrated;
+ try {
+ localStorage.setItem('wplace-bot-progress', JSON.stringify(migrated));
+ } catch {}
}
- return data;
+ return migrated;
} catch (error) {
- console.error("Error loading progress:", error)
- return null
+ console.error('Error loading progress:', error);
+ return null;
}
},
clearProgress: () => {
try {
- localStorage.removeItem("wplace-bot-progress")
+ localStorage.removeItem('wplace-bot-progress');
// Also clear painted map from memory
state.paintedMap = null;
state._lastSavePixelCount = 0;
state._lastSaveTime = 0;
- console.log("📋 Progress and painted map cleared");
- return true
+ // Reset coordinate generation settings to their default values
+ state.coordinateMode = CONFIG.COORDINATE_MODE;
+ state.coordinateDirection = CONFIG.COORDINATE_DIRECTION;
+ state.coordinateSnake = CONFIG.COORDINATE_SNAKE;
+ state.blockWidth = CONFIG.COORDINATE_BLOCK_WIDTH;
+ state.blockHeight = CONFIG.COORDINATE_BLOCK_HEIGHT;
+ console.log('📋 Progress and painted map cleared');
+ return true;
} catch (error) {
- console.error("Error clearing progress:", error)
- return false
+ console.error('Error clearing progress:', error);
+ return false;
}
},
restoreProgress: (savedData) => {
try {
- Object.assign(state, savedData.state)
+ Object.assign(state, savedData.state);
+
+ // Restore coordinate generation settings
+ if (savedData.state.coordinateMode) {
+ state.coordinateMode = savedData.state.coordinateMode;
+ }
+ if (savedData.state.coordinateDirection) {
+ state.coordinateDirection = savedData.state.coordinateDirection;
+ }
+ if (savedData.state.coordinateSnake !== undefined) {
+ state.coordinateSnake = savedData.state.coordinateSnake;
+ }
+ if (savedData.state.blockWidth) {
+ state.blockWidth = savedData.state.blockWidth;
+ }
+ if (savedData.state.blockHeight) {
+ state.blockHeight = savedData.state.blockHeight;
+ }
if (savedData.imageData) {
state.imageData = {
...savedData.imageData,
pixels: new Uint8ClampedArray(savedData.imageData.pixels),
- }
+ };
try {
const canvas = document.createElement('canvas');
canvas.width = state.imageData.width;
canvas.height = state.imageData.height;
const ctx = canvas.getContext('2d');
- const imageData = new ImageData(state.imageData.pixels, state.imageData.width, state.imageData.height);
+ const imageData = new ImageData(
+ state.imageData.pixels,
+ state.imageData.width,
+ state.imageData.height
+ );
ctx.putImageData(imageData, 0, 0);
const proc = new ImageProcessor('');
- proc.img = canvas;
+ proc.img = canvas;
proc.canvas = canvas;
proc.ctx = ctx;
state.imageData.processor = proc;
@@ -2045,83 +2606,44 @@ function applyTheme() {
const { width, height, data } = savedData.paintedMapPacked;
state.paintedMap = Utils.unpackPaintedMapFromBase64(data, width, height);
} else if (savedData.paintedMap) {
- state.paintedMap = savedData.paintedMap.map((row) => Array.from(row))
+ state.paintedMap = savedData.paintedMap.map((row) => Array.from(row));
}
- return true
+ return true;
} catch (error) {
- console.error("Error restoring progress:", error)
- return false
+ console.error('Error restoring progress:', error);
+ return false;
}
},
- saveProgressToFile: () => {
+ saveProgressToFile: () => {
try {
- // Pack painted map if available
- let paintedMapPacked = null;
- if (state.paintedMap && state.imageData) {
- const data = Utils.packPaintedMapToBase64(state.paintedMap, state.imageData.width, state.imageData.height);
- if (data) {
- paintedMapPacked = {
- width: state.imageData.width,
- height: state.imageData.height,
- data: data
- };
- }
- }
-
- const progressData = {
- timestamp: Date.now(),
- version: "2.1",
- state: {
- totalPixels: state.totalPixels,
- paintedPixels: state.paintedPixels,
- lastPosition: state.lastPosition,
- startPosition: state.startPosition,
- region: state.region,
- imageLoaded: state.imageLoaded,
- colorsChecked: state.colorsChecked,
- availableColors: state.availableColors,
- },
- imageData: state.imageData
- ? {
- width: state.imageData.width,
- height: state.imageData.height,
- pixels: Array.from(state.imageData.pixels),
- totalPixels: state.imageData.totalPixels,
- }
- : null,
- paintedMapPacked: paintedMapPacked,
- }
-
- const filename = `wplace-bot-progress-${new Date().toISOString().slice(0, 19).replace(/:/g, "-")}.json`
- Utils.createFileDownloader(JSON.stringify(progressData, null, 2), filename)
- return true
+ const progressData = Utils.buildProgressData();
+ 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
+ console.error('Error saving to file:', error);
+ return false;
}
},
- loadProgressFromFile: async () => {
+ loadProgressFromFile: async () => {
try {
- const data = await Utils.createFileUploader()
+ const data = await Utils.createFileUploader();
if (!data || !data.state) {
- throw new Error("Invalid file format")
+ throw new Error('Invalid file format');
}
- const ver = data.version;
- let migrated = data;
- if (ver === '2.1') {
- } else if (ver === '2' || ver === '2.0') {
- migrated = Utils.migrateProgressToV21(data) || data;
- } else {
- migrated = Utils.migrateProgressToV21(data) || data;
- }
- const success = Utils.restoreProgress(migrated)
- return success
+ const migrated = Utils.migrateProgress(data);
+
+ const success = Utils.restoreProgress(migrated);
+ return success;
} catch (error) {
- console.error("Error loading from file:", error)
- throw error
+ console.error('Error loading from file:', error);
+ throw error;
}
},
@@ -2163,74 +2685,83 @@ function applyTheme() {
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';
+ },
+ };
// IMAGE PROCESSOR CLASS
class ImageProcessor {
constructor(imageSrc) {
- this.imageSrc = imageSrc
- this.img = null
- this.canvas = null
- this.ctx = null
+ this.imageSrc = imageSrc;
+ this.img = null;
+ this.canvas = null;
+ this.ctx = null;
}
async load() {
return new Promise((resolve, reject) => {
- this.img = new Image()
- this.img.crossOrigin = "anonymous"
+ 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
- })
+ 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;
+ });
}
getDimensions() {
return {
width: this.canvas.width,
height: this.canvas.height,
- }
+ };
}
getPixelData() {
- return this.ctx.getImageData(0, 0, this.canvas.width, this.canvas.height).data
+ return this.ctx.getImageData(0, 0, this.canvas.width, this.canvas.height).data;
}
resize(newWidth, newHeight) {
- const tempCanvas = document.createElement("canvas")
- const tempCtx = tempCanvas.getContext("2d")
+ const tempCanvas = document.createElement('canvas');
+ const tempCtx = tempCanvas.getContext('2d');
- tempCanvas.width = newWidth
- tempCanvas.height = newHeight
+ tempCanvas.width = newWidth;
+ tempCanvas.height = newHeight;
- tempCtx.imageSmoothingEnabled = false
- tempCtx.drawImage(this.canvas, 0, 0, newWidth, 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)
+ 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
+ return this.ctx.getImageData(0, 0, newWidth, newHeight).data;
}
generatePreview(width, height) {
- const previewCanvas = document.createElement("canvas")
- const previewCtx = previewCanvas.getContext("2d")
+ const previewCanvas = document.createElement('canvas');
+ const previewCtx = previewCanvas.getContext('2d');
- previewCanvas.width = width
- previewCanvas.height = height
+ previewCanvas.width = width;
+ previewCanvas.height = height;
- previewCtx.imageSmoothingEnabled = false
- previewCtx.drawImage(this.img, 0, 0, width, height)
+ previewCtx.imageSmoothingEnabled = false;
+ previewCtx.drawImage(this.img, 0, 0, width, height);
- return previewCanvas.toDataURL()
+ return previewCanvas.toDataURL();
}
}
@@ -2238,50 +2769,65 @@ function applyTheme() {
const WPlaceService = {
async paintPixelInRegion(regionX, regionY, pixelX, pixelY, color) {
try {
- await ensureToken()
- if (!turnstileToken) return "token_error"
- const payload = { coords: [pixelX, pixelY], colors: [color], t: turnstileToken }
+ await ensureToken();
+ if (!turnstileToken) return 'token_error';
+ const payload = {
+ coords: [pixelX, pixelY],
+ colors: [color],
+ t: turnstileToken,
+ };
const res = await fetch(`https://backend.wplace.live/s0/pixel/${regionX}/${regionY}`, {
- method: "POST",
- headers: { "Content-Type": "text/plain;charset=UTF-8" },
- credentials: "include",
+ method: 'POST',
+ headers: { 'Content-Type': 'text/plain;charset=UTF-8' },
+ credentials: 'include',
body: JSON.stringify(payload),
- })
+ });
if (res.status === 403) {
- console.error("❌ 403 Forbidden. Turnstile token might be invalid or expired.")
- turnstileToken = null
- tokenPromise = new Promise((resolve) => { _resolveToken = resolve })
- return "token_error"
+ console.error('❌ 403 Forbidden. Turnstile token might be invalid or expired.');
+ turnstileToken = null;
+ tokenPromise = new Promise((resolve) => {
+ _resolveToken = resolve;
+ });
+ return 'token_error';
}
- const data = await res.json()
- return data?.painted === 1
+ const data = await res.json();
+ return data?.painted === 1;
} catch (e) {
- console.error("Paint request failed:", e)
- return false
+ console.error('Paint request failed:', e);
+ return false;
}
},
async getCharges() {
+ const defaultResult = {
+ charges: 0,
+ max: 1,
+ cooldown: CONFIG.COOLDOWN_DEFAULT,
+ };
+
try {
- const res = await fetch("https://backend.wplace.live/me", {
- credentials: "include",
- })
- const data = await res.json()
- return {
- charges: data.charges?.count || 0,
- max: data.charges?.max || 1,
- cooldown: data.charges?.next || CONFIG.COOLDOWN_DEFAULT,
+ const res = await fetch('https://backend.wplace.live/me', {
+ credentials: 'include',
+ });
+
+ if (!res.ok) {
+ console.error(`Failed to get charges: HTTP ${res.status}`);
+ return defaultResult;
}
+
+ const data = await res.json();
+
+ return {
+ charges: data.charges?.count ?? 0,
+ max: data.charges?.max ?? 1,
+ cooldown: data.charges?.cooldownMs ?? CONFIG.COOLDOWN_DEFAULT,
+ };
} catch (e) {
- console.error("Failed to get charges:", e)
- return {
- charges: 0,
- max: 1,
- cooldown: CONFIG.COOLDOWN_DEFAULT,
- }
+ console.error('Failed to get charges:', e);
+ return defaultResult;
}
},
- }
+ };
// Desktop Notification Manager
const NotificationManager = {
@@ -2289,14 +2835,14 @@ function applyTheme() {
pollIntervalMs: 60_000,
icon() {
const link = document.querySelector("link[rel~='icon']");
- return link?.href || (location.origin + "/favicon.ico");
+ return link?.href || location.origin + '/favicon.ico';
},
async requestPermission() {
- if (!("Notification" in window)) {
- Utils.showAlert(Utils.t("notificationsNotSupported"), "warning");
- return "denied";
+ if (!('Notification' in window)) {
+ Utils.showAlert(Utils.t('notificationsNotSupported'), 'warning');
+ return 'denied';
}
- if (Notification.permission === "granted") return "granted";
+ if (Notification.permission === 'granted') return 'granted';
try {
const perm = await Notification.requestPermission();
return perm;
@@ -2305,11 +2851,13 @@ function applyTheme() {
}
},
canNotify() {
- return state.notificationsEnabled &&
- typeof Notification !== "undefined" &&
- Notification.permission === "granted";
+ return (
+ state.notificationsEnabled &&
+ typeof Notification !== 'undefined' &&
+ Notification.permission === 'granted'
+ );
},
- notify(title, body, tag = "wplace-charges", force = false) {
+ notify(title, body, tag = 'wplace-charges', force = false) {
if (!this.canNotify()) return false;
if (!force && state.notifyOnlyWhenUnfocused && document.hasFocus()) return false;
try {
@@ -2324,25 +2872,29 @@ function applyTheme() {
return true;
} catch {
// Graceful fallback
- Utils.showAlert(body, "info");
+ Utils.showAlert(body, 'info');
return false;
}
},
resetEdgeTracking() {
- state._lastChargesBelow = state.currentCharges < state.cooldownChargeThreshold;
+ state._lastChargesBelow = state.displayCharges < state.cooldownChargeThreshold;
state._lastChargesNotifyAt = 0;
},
maybeNotifyChargesReached(force = false) {
if (!state.notificationsEnabled || !state.notifyOnChargesReached) return;
- const reached = state.currentCharges >= state.cooldownChargeThreshold;
+ 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: Math.floor(state.currentCharges), max: state.maxCharges, threshold: state.cooldownChargeThreshold });
- this.notify(Utils.t("chargesReadyNotification"), msg, "wplace-notify-charges");
+ 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;
@@ -2357,11 +2909,13 @@ function applyTheme() {
this.pollTimer = setInterval(async () => {
try {
const { charges, cooldown, max } = await WPlaceService.getCharges();
- state.currentCharges = Math.floor(charges);
+ state.displayCharges = Math.floor(charges);
state.cooldown = cooldown;
state.maxCharges = Math.max(1, Math.floor(max));
this.maybeNotifyChargesReached();
- } catch { /* ignore */ }
+ } catch {
+ /* ignore */
+ }
}, this.pollIntervalMs);
},
stopPolling() {
@@ -2378,69 +2932,18 @@ function applyTheme() {
};
// COLOR MATCHING FUNCTION - Optimized with caching
- const colorCache = new Map()
-
- function findClosestColor(targetRgb, availableColors) {
- if (!availableColors || availableColors.length === 0) return 1
- const cacheKey = `${targetRgb[0]},${targetRgb[1]},${targetRgb[2]}|${state.colorMatchingAlgorithm}|${state.enableChromaPenalty?'c':'nc'}|${state.chromaPenaltyWeight}`
- if (colorCache.has(cacheKey)) return colorCache.get(cacheKey)
-
- const whiteThreshold = state.customWhiteThreshold || CONFIG.WHITE_THRESHOLD
- 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) { colorCache.set(cacheKey, whiteEntry.id); return whiteEntry.id }
- }
-
- let bestId = availableColors[0].id
- let bestScore = Infinity
-
- if (state.colorMatchingAlgorithm === '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; if (dist === 0) break }
- }
- } else { // lab
- const [Lt, at, bt] = Utils._lab(targetRgb[0], targetRgb[1], targetRgb[2])
- const targetChroma = Math.sqrt(at * at + bt * bt)
- const penaltyWeight = state.enableChromaPenalty ? (state.chromaPenaltyWeight || 0.15) : 0
- for (let i = 0; i < availableColors.length; i++) {
- const c = availableColors[i]
- const [r, g, b] = c.rgb
- const [L2, a2, b2] = Utils._lab(r, g, b)
- const dL = Lt - L2, da = at - a2, 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; if (dist === 0) break }
- }
- }
-
- colorCache.set(cacheKey, bestId)
- if (colorCache.size > 15000) { const firstKey = colorCache.keys().next().value; colorCache.delete(firstKey) }
- return bestId
- }
+ const colorCache = new Map();
// UI UPDATE FUNCTIONS (declared early to avoid reference errors)
- let updateUI = () => { }
- let updateStats = () => { }
- let updateDataButtons = () => { }
+ let updateUI = () => {};
+ let updateStats = (isManualRefresh) => {};
+ let updateDataButtons = () => {};
function updateActiveColorPalette() {
state.activeColorPalette = [];
const activeSwatches = document.querySelectorAll('.wplace-color-swatch.active');
if (activeSwatches) {
- activeSwatches.forEach(swatch => {
+ activeSwatches.forEach((swatch) => {
const rgbStr = swatch.getAttribute('data-rgb');
if (rgbStr) {
const rgb = rgbStr.split(',').map(Number);
@@ -2456,7 +2959,7 @@ function applyTheme() {
function toggleAllColors(select, showingUnavailable = false) {
const swatches = document.querySelectorAll('.wplace-color-swatch');
if (swatches) {
- swatches.forEach(swatch => {
+ 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) {
@@ -2473,16 +2976,16 @@ function applyTheme() {
function unselectAllPaidColors() {
const swatches = document.querySelectorAll('.wplace-color-swatch');
if (swatches) {
- swatches.forEach(swatch => {
+ swatches.forEach((swatch) => {
const colorId = parseInt(swatch.getAttribute('data-color-id'), 10);
- if (!isNaN(colorId) && colorId>= 32) {
+ 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');
@@ -2492,7 +2995,9 @@ function applyTheme() {
// 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')}
`;
+ colorsContainer.innerHTML = `${Utils.t(
+ 'uploadImageFirst'
+ )}
`;
return;
}
@@ -2502,16 +3007,16 @@ function applyTheme() {
let totalCount = 0;
// Convert COLOR_MAP to array and filter out transparent
- const allColors = Object.values(CONFIG.COLOR_MAP).filter(color => color.rgb !== null);
+ const allColors = Object.values(CONFIG.COLOR_MAP).filter((color) => color.rgb !== null);
- allColors.forEach(colorData => {
+ allColors.forEach((colorData) => {
const { id, name, rgb } = colorData;
const rgbKey = `${rgb.r},${rgb.g},${rgb.b}`;
totalCount++;
// Check if this color is available in the captured colors
- const isAvailable = state.availableColors.some(c =>
- c.rgb[0] === rgb.r && c.rgb[1] === rgb.g && c.rgb[2] === rgb.b
+ const 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
@@ -2521,7 +3026,9 @@ function applyTheme() {
if (isAvailable) availableCount++;
- const colorItem = Utils.createElement('div', { className: 'wplace-color-item' });
+ 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)' : ''}`,
@@ -2540,10 +3047,14 @@ function applyTheme() {
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)' : ''));
+ 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) {
@@ -2571,49 +3082,107 @@ function applyTheme() {
});
}
- container.querySelector('#selectAllBtn')?.addEventListener('click', () => toggleAllColors(true, showAllToggle?.checked));
- container.querySelector('#unselectAllBtn')?.addEventListener('click', () => toggleAllColors(false, showAllToggle?.checked));
- container.querySelector('#unselectPaidBtn')?.addEventListener('click', () => unselectAllPaidColors());
+ 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() {
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 (state.tokenSource === 'manual') {
+ console.log('🎯 Manual token source selected - using pixel placement automation');
return await handleCaptchaFallback();
}
-
+
// Generator mode (pure) or Hybrid mode - try generator first
try {
// Use optimized token generation with automatic sitekey detection
- const sitekey = Utils.detectSitekey();
- console.log("🔑 Generating Turnstile token for sitekey:", sitekey);
- console.log('🧭 UA:', navigator.userAgent.substring(0, 50) + '...', 'Platform:', navigator.platform);
-
+ 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();
}
-
- const token = await Utils.generatePaintToken(sitekey);
-
- 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}`);
-
+
+ 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 = turnstileToken;
+ }
+ // ✅ Otherwise generate a new one
+ else {
+ console.log('🔐 No valid pre-generated or cached token, creating new one...');
+ token = await Utils.executeTurnstile(sitekey, 'paint');
+ if (token) {
+ setTurnstileToken(token);
+ }
+ }
+
+ // 📊 Debug log
+ 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}`);
+ 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...");
+ if (state.tokenSource === 'hybrid') {
+ console.log(
+ '🔄 Hybrid mode: Generator failed, automatically switching to manual pixel placement...'
+ );
const fbToken = await handleCaptchaFallback();
return fbToken;
} else {
@@ -2629,23 +3198,31 @@ function applyTheme() {
try {
// Ensure we have a fresh promise to await for a new token capture
if (!_resolveToken) {
- tokenPromise = new Promise((res) => { _resolveToken = res; });
+ tokenPromise = new Promise((res) => {
+ _resolveToken = res;
+ });
}
- const timeoutPromise = Utils.sleep(20000).then(() => reject(new Error("Auto-CAPTCHA timed out.")));
+ 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.");
+ 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.");
+ 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.");
+ if (!canvas) throw new Error('Could not find the canvas element.');
canvas.setAttribute('tabindex', '0');
canvas.focus();
@@ -2653,10 +3230,28 @@ function applyTheme() {
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 }));
+ 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 }));
+ canvas.dispatchEvent(
+ new KeyboardEvent('keyup', {
+ key: ' ',
+ code: 'Space',
+ bubbles: true,
+ })
+ );
await Utils.sleep(500);
// 800ms delay before sending confirmation
@@ -2665,7 +3260,9 @@ function applyTheme() {
// 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');
+ 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;
@@ -2685,65 +3282,76 @@ function applyTheme() {
})();
await Promise.race([solvePromise, timeoutPromise]);
-
} catch (error) {
- console.error("Auto-CAPTCHA process failed:", error);
+ console.error('Auto-CAPTCHA process failed:', error);
reject(error);
}
});
}
-
async function createUI() {
- await detectLanguage()
+ 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")
+ 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()
+ if (existingContainer) existingContainer.remove();
+ if (existingStats) existingStats.remove();
+ if (existingSettings) existingSettings.remove();
+ if (existingResizeContainer) existingResizeContainer.remove();
+ if (existingResizeOverlay) existingResizeOverlay.remove();
- loadThemePreference()
- await initializeTranslations()
+ loadThemePreference();
+ await initializeTranslations();
- const theme = getCurrentTheme()
- applyTheme() // <- new: set CSS vars and theme class before building UI
+ const theme = getCurrentTheme();
+ applyTheme(); // <- new: set CSS vars and theme class before building UI
- 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)
+ 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;
- if (theme.fontFamily.includes("Press Start 2P")) {
- const googleFonts = document.createElement("link")
- googleFonts.rel = "stylesheet"
- googleFonts.href = "https://fonts.googleapis.com/css2?family=Press+Start+2P&display=swap"
- document.head.appendChild(googleFonts)
+ // 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);
}
- // Link external CSS files
- const cssLink = document.createElement('link');
- cssLink.rel = 'stylesheet';
- cssLink.href = 'https://staninna.github.io/WPlace-AutoBOT/decoupled-css/auto-image-styles.css'; // TODO: Before merge change to https://raw.githubusercontent.com/Wplace-AutoBot/WPlace-AutoBOT/refs/heads/main/auto-image-styles.css
- cssLink.setAttribute('data-wplace-theme', 'true');
- document.head.appendChild(cssLink);
+ appendLinkOnce('https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css');
- const container = document.createElement("div")
- container.id = "wplace-image-bot-container"
+ if (theme.fontFamily.includes('Press Start 2P')) {
+ appendLinkOnce('https://fonts.googleapis.com/css2?family=Press+Start+2P&display=swap');
+ }
+
+ appendLinkOnce(
+ 'https://skalsech.github.io/WPlace-AutoBOT/feat-external-lang/auto-image-styles.css',
+ { 'data-wplace-theme': 'true' }
+ );
+
+ const container = document.createElement('div');
+ container.id = 'wplace-image-bot-container';
container.innerHTML = `