${Utils.t('charges')}
@@ -5186,6 +5302,16 @@ function getText(key, params) {
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})`);
+ 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;
@@ -5331,7 +5457,7 @@ function getText(key, params) {
);
}
- function generateCoordinates(width, height, mode, direction, snake, blockWidth, blockHeight) {
+ function generateCoordinates(width, height, mode, direction, snake, blockWidth, blockHeight, startFromX = 0, startFromY = 0) {
const coords = [];
console.log(
'Generating coordinates with \n mode:',
@@ -5343,7 +5469,11 @@ function getText(key, params) {
'\n blockWidth:',
blockWidth,
'\n blockHeight:',
- blockHeight
+ blockHeight,
+ '\n startFromX:',
+ startFromX,
+ '\n startFromY:',
+ startFromY
);
// --------- Standard 4 corners traversal ----------
let xStart, xEnd, xStep;
@@ -5470,6 +5600,30 @@ function getText(key, params) {
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;
}
@@ -5490,15 +5644,19 @@ function getText(key, params) {
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,
};
updateStats();
- updateUI('paintingProgress', 'default', {
- painted: state.paintedPixels,
- total: state.totalPixels,
- });
+ // Update account list with new charges
+ updateCurrentAccountInList();
+ // Progress tracking removed from UI to reduce visual clutter
Utils.performSmartSave();
if (CONFIG.PAINTING_SPEED_ENABLED && state.paintingSpeed > 0 && batchSize > 0) {
@@ -5517,16 +5675,127 @@ function getText(key, params) {
}
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: switch to next account or use cooldown
+ console.log('🔄 Auto-swap enabled: checking account switching options');
+
+ const accounts = JSON.parse(localStorage.getItem("accounts")) || [];
+ 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 {
+ // Check if we're at the last account in the cycle
+ const isLastAccount = state.accountIndex >= accounts.length - 1;
+
+ if (!isLastAccount) {
+ // Switch to next account
+ console.log(`🔄 Switching to next account (${state.accountIndex + 1}/${accounts.length})`);
+ const switchResult = await switchToNextAccount(accounts);
+ if (!switchResult) {
+ console.log('❌ Account switch failed, stopping');
+ state.stopFlag = true;
+ break;
+ }
+ // Continue painting with new account (no cooldown, no token regeneration)
+ continue;
+ } else {
+ // Last account - check cooldown flag
+ if (!state.cooldownUsedThisCycle) {
+ // Use cooldown once per cycle
+ console.log('⏱️ Last account reached, using cooldown period');
+ state.cooldownUsedThisCycle = true;
+
+ const cooldownResult = await executeCooldownPeriod();
+ if (cooldownResult === 'stopped') break;
+
+ const tokenResult = await regenerateTokenForNewSession();
+ if (!tokenResult) {
+ state.stopFlag = true;
+ break;
+ }
+ } else {
+ // Reset cycle - go back to first account
+ console.log('🔁 Cooldown already used, resetting to first account');
+ state.cooldownUsedThisCycle = false;
+ state.accountIndex = 0;
+
+ const switchResult = await switchToSpecificAccount(accounts[0], 0);
+ if (!switchResult) {
+ console.log('❌ Reset to first account failed, stopping');
+ state.stopFlag = true;
+ break;
+ }
+ // 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
- // If availableColors exists and colorsChecked is true, we don't need to wait for tiles
const isRestoredData = state.availableColors && state.availableColors.length > 0 && state.colorsChecked;
if (!isRestoredData) {
- // Only wait for tiles if this is not restored data
// Wait for original tiles to load if needed
const tilesReady = await overlayManager.waitForTiles(
regionX,
@@ -5541,10 +5810,8 @@ function getText(key, params) {
if (!tilesReady) {
updateUI('overlayTilesNotLoaded', 'error');
state.stopFlag = true;
- return;
+ return 'stopped';
}
- } else {
- // Using restored data - skipping tile wait
}
let pixelBatch = null;
@@ -5555,82 +5822,21 @@ function getText(key, params) {
colorUnavailable: 0,
};
- const transparencyThreshold =
- state.customTransparencyThreshold || CONFIG.TRANSPARENCY_THRESHOLD;
+ // 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;
- function checkPixelEligibility(x, y) {
- // 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);
-
- // Template color ID, normalized/mapped to the nearest available color in our palette.
- // Example: template requires "Slate", but we only have "Dark Gray" available
- // → mappedTargetColorId = ID of Dark Gray.
- //
- // If `state.paintUnavailablePixels` is enabled, the painting would stop earlier
- // because "Slate" was not found (null returned).
- //
- // Else, the template "Slate" is mapped to the closest available color (e.g., "Dark Gray"),
- // and we proceed with painting using that mapped color.
- //
- // In this case, if the canvas pixel is already Slate (mapped to available Dark Gray),
- // we skip painting, since template and canvas both resolve to the same available color (Dark Gray).
- const mappedTargetColorId = Utils.resolveColor(
- targetRgb,
- state.availableColors,
- !state.paintUnavailablePixels
- );
-
- // Technically, checking only `!mappedTargetColorId.id` would be enough,
- // but combined with `state.paintUnavailablePixels` it makes the logic explicit:
- // we only skip when the template color cannot be mapped AND strict mode is on.
- 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 };
+ if (state.displayCharges <= 0) {
+ console.log('⚡ No charges available, skipping painting session');
+ return 'charges_depleted';
}
- function skipPixel(reason, id, rgb, x, y) {
- if (reason !== 'transparent') {
- console.log(`Skipped pixel for ${reason} (id: ${id}, (${rgb.join(', ')})) at (${x}, ${y})`);
- }
- skippedPixels[reason]++;
- }
+ 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,
@@ -5639,24 +5845,127 @@ function getText(key, params) {
state.coordinateDirection,
state.coordinateSnake,
state.blockWidth,
- state.blockHeight
+ state.blockHeight,
+ state.lastPosition.x,
+ state.lastPosition.y
);
- outerLoop: for (const [x, y] of coords) {
+ // 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
+ 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
+ if (!Utils.isPixelPainted(x, y)) {
+ 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`
- );
+ console.log(`🎯 Sending last batch before stop with ${pixelBatch.pixels.length} pixels`);
await flushPixelBatch(pixelBatch);
}
state.lastPosition = { x, y };
- updateUI('paintingPaused', 'warning', { x, y });
- // noinspection UnnecessaryLabelOnBreakStatementJS
- break outerLoop;
+ // 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('⚡ 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';
}
- const targetPixelInfo = checkPixelEligibility(x, y);
let absX = startX + x;
let absY = startY + y;
@@ -5665,61 +5974,23 @@ function getText(key, params) {
let pixelX = absX % 1000;
let pixelY = absY % 1000;
- // Template color ID, normalized/mapped to the nearest available color in our palette.
- // Example: template requires "Slate", but we only have "Dark Gray" available
- // → mappedTargetColorId = ID of Dark Gray.
- //
- // If `state.paintUnavailablePixels` is enabled, the painting would stop earlier
- // because "Slate" was not found (null returned).
- //
- // Else, the template "Slate" is mapped to the closest available color (e.g., "Dark Gray"),
- // and we proceed with painting using that mapped color.
- //
- // In this case, if the canvas pixel is already Slate (mapped to available Dark Gray),
- // we skip painting, since template and canvas both resolve to the same available color (Dark Gray).
const targetMappedColorId = targetPixelInfo.mappedColorId;
- if (!targetPixelInfo.eligible) {
- skipPixel(
- targetPixelInfo.reason,
- targetMappedColorId,
- [targetPixelInfo.r, targetPixelInfo.g, targetPixelInfo.b],
- pixelX,
- pixelY
- );
- continue;
- }
-
+ // 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 (switching to region ${regionX + adderX
- },${regionY + adderY})`
- );
+ console.log(`🌍 Sending region-change batch with ${pixelBatch.pixels.length} pixels`);
const success = await flushPixelBatch(pixelBatch);
-
- if (success) {
- if (
- CONFIG.PAINTING_SPEED_ENABLED &&
- state.paintingSpeed > 0 &&
- pixelBatch.pixels.length > 0
- ) {
- const batchDelayFactor = Math.max(1, 100 / state.paintingSpeed);
- const totalDelay = Math.max(100, batchDelayFactor * pixelBatch.pixels.length);
- await Utils.sleep(totalDelay);
- }
- updateStats();
- } else {
+ if (!success) {
console.error(`❌ Batch failed permanently after retries. Stopping painting.`);
state.stopFlag = true;
- updateUI('paintingBatchFailed', 'error');
- // noinspection UnnecessaryLabelOnBreakStatementJS
- break outerLoop;
+ return 'stopped';
}
+ updateStats();
}
pixelBatch = {
@@ -5729,51 +6000,7 @@ function getText(key, params) {
};
}
- try {
- // CRITICAL FIX: Check overlay manager availability
- if (!overlayManager || typeof overlayManager.getTilePixelColor !== 'function') {
- console.error('❌ Overlay manager not available for pixel color check');
- state.log(`❌ Overlay manager unavailable - skipping pixel (${pixelX}, ${pixelY})`);
- continue;
- }
-
- const tileKeyParts = [pixelBatch.regionX, pixelBatch.regionY];
-
- const tilePixelRGBA = await overlayManager.getTilePixelColor(
- tileKeyParts[0],
- tileKeyParts[1],
- pixelX,
- pixelY
- );
-
- if (tilePixelRGBA && Array.isArray(tilePixelRGBA)) {
- // Resolve the actual canvas pixel color to the closest available color.
- // (The raw canvas RGB [er, eg, eb] is mapped into state.availableColors)
- // so that comparison is consistent with targetMappedColorId.
- const mappedCanvasColor = Utils.resolveColor(
- tilePixelRGBA.slice(0, 3),
- state.availableColors
- );
- const isMatch = mappedCanvasColor.id === targetMappedColorId;
- if (isMatch) {
- skipPixel(
- 'alreadyPainted',
- targetMappedColorId,
- [targetPixelInfo.r, targetPixelInfo.g, targetPixelInfo.b],
- pixelX,
- pixelY
- );
- continue;
- }
- }
- } catch (e) {
- console.error(`Error checking existing pixel at (${pixelX}, ${pixelY}):`, e);
- updateUI('paintingPixelCheckFailed', 'error', { x: pixelX, y: pixelY });
- state.stopFlag = true;
- // noinspection UnnecessaryLabelOnBreakStatementJS
- break outerLoop;
- }
-
+ // Add pixel to batch (no need to check again - already pre-filtered)
pixelBatch.pixels.push({
x: pixelX,
y: pixelY,
@@ -5782,118 +6009,136 @@ function getText(key, params) {
localY: y,
});
+ // Send batch if it's full
const maxBatchSize = calculateBatchSize();
if (pixelBatch.pixels.length >= maxBatchSize) {
- const modeText =
- state.batchMode === 'random'
- ? `random (${state.randomBatchMin}-${state.randomBatchMax})`
- : 'normal';
- console.log(
- `📦 Sending batch with ${pixelBatch.pixels.length} pixels (mode: ${modeText}, target: ${maxBatchSize})`
- );
+ 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;
- updateUI('paintingBatchFailed', 'error');
- // noinspection UnnecessaryLabelOnBreakStatementJS
- break outerLoop;
+ return 'stopped';
}
-
pixelBatch.pixels = [];
- }
- if (!CONFIG.autoSwap) {
- if (state.displayCharges < state.cooldownChargeThreshold && !state.stopFlag) {
- await Utils.dynamicSleep(() => {
- if (state.displayCharges >= state.cooldownChargeThreshold) {
- NotificationManager.maybeNotifyChargesReached(true);
- return 0;
- }
- if (state.stopFlag) return 0;
- return getMsToTargetCharges(
- state.preciseCurrentCharges,
- state.cooldownChargeThreshold,
- state.cooldown
- );
- });
- }
- }
- else {
- if (state.displayCharges < state.cooldownChargeThreshold && !state.stopFlag) {
- console.log("⚠️ Charges too low, swapping to next account...");
-
- const accounts = JSON.parse(localStorage.getItem("accounts")) || [];
- if (accounts.length === 0) {
- console.warn("❌ No accounts available, stopping painting.");
- state.stopFlag = true;
- return;
- }
-
- state.accountIndex = (state.accountIndex + 1) % accounts.length;
- console.log("🔄 Switching to account index:", state.accountIndex);
-
- const nextToken = accounts[state.accountIndex];
- console.log("🔑 Next token:", nextToken);
-
- if (!nextToken) {
- console.warn("⚠️ Invalid token, skipping...");
- return;
- }
-
- swapAccountTrigger(nextToken);
-
- let maxRetries = 20;
- let retryCount = 0;
- let swapSuccess = false;
-
- while (!swapSuccess && retryCount < maxRetries) {
- console.log(`⏳ Waiting for account swap... (Attempt ${retryCount + 1}/${maxRetries})`);
-
- // Wait for a short period before checking.
- await new Promise(resolve => setTimeout(resolve, 1000));
-
- try {
- await fetchAccount();
-
- console.log("✅ Account swap confirmed.");
- swapSuccess = true;
- } catch (error) {
- console.warn("❌ Account swap not yet successful. Retrying...", error);
- retryCount++;
- }
- }
-
- if (swapSuccess) {
-
- const { charges, cooldown } = await WPlaceService.getCharges();
- state.displayCharges = Math.floor(charges);
- state.cooldown = cooldown;
- Utils.performSmartSave();
- updateStats();
- } else {
- console.error("❌ Failed to swap account after multiple retries. Stopping loop.");
- state.stopFlag = true;
- }
- }
- }
-
- if (state.stopFlag) {
- // noinspection UnnecessaryLabelOnBreakStatementJS
- break outerLoop;
+ 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 after all retries.`
- );
+ 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 {
- if (window._chargesInterval) clearInterval(window._chargesInterval);
+ // 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; // Limit 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);
+ 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);
+
+ // Fail-safe: Don't exceed max checks
+ 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;
}
@@ -5903,8 +6148,6 @@ function getText(key, params) {
} else {
updateUI('paintingComplete', 'success', { count: state.paintedPixels });
state.lastPosition = { x: 0, y: 0 };
- // Keep painted map until user starts new project
- // state.paintedMap = null // Commented out to preserve data
Utils.saveProgress(); // Save final complete state
overlayManager.clear();
const toggleOverlayBtn = document.getElementById('toggleOverlayBtn');
@@ -5913,27 +6156,71 @@ function getText(key, params) {
toggleOverlayBtn.disabled = true;
}
}
-
- // Log skip statistics
- console.log(`📊 Pixel Statistics:`);
- console.log(` Painted: ${state.paintedPixels}`);
- console.log(` Skipped - Transparent: ${skippedPixels.transparent}`);
- console.log(` Skipped - White (disabled): ${skippedPixels.white}`);
- console.log(` Skipped - Already painted: ${skippedPixels.alreadyPainted}`);
- console.log(` Skipped - Color Unavailable: ${skippedPixels.colorUnavailable}`);
- console.log(
- ` Total processed: ${state.paintedPixels +
- skippedPixels.transparent +
- skippedPixels.white +
- skippedPixels.alreadyPainted +
- skippedPixels.colorUnavailable
- }`
- );
-
- updateStats();
}
- // Helper function to calculate batch size based on mode
+ // 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;
@@ -5970,15 +6257,31 @@ function getText(key, params) {
console.log(`✅ Batch succeeded on attempt ${attempt}`);
return true;
} else if (result === 'token_error') {
- console.log(`🔑 Token error on attempt ${attempt}, regenerating...`);
+ 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();
+ 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 on attempt ${attempt}:`, 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);
@@ -6006,17 +6309,10 @@ function getText(key, params) {
async function sendPixelBatch(pixelBatch, regionX, regionY) {
let token = getTurnstileToken();
- // Generate new token if we don't have one
+ // Don't auto-generate tokens during processing - return error if no token available
if (!token) {
- try {
- console.log('🔑 Generating Turnstile token for pixel batch...');
- token = await handleCaptcha();
- setTurnstileToken(token); // Store for potential reuse
- } catch (error) {
- console.error('❌ Failed to generate Turnstile token:', error);
- createTokenPromise();
- return 'token_error';
- }
+ console.warn('⚠️ No token available and auto-generation disabled during processing');
+ return 'token_error';
}
const coords = new Array(pixelBatch.length * 2);
@@ -6043,40 +6339,21 @@ function getText(key, params) {
try {
data = await res.json();
} catch (_) { }
- console.error('❌ 403 Forbidden. Turnstile token might be invalid or expired.');
-
- // Try to generate a new token and retry once
- try {
- console.log('🔄 Regenerating Turnstile token after 403...');
- token = await handleCaptcha();
- setTurnstileToken(token);
-
- // Retry the request with new token
- const retryPayload = { coords, colors, t: token, fp: fpStr32 };
- var wasmtoken = await createWasmToken(regionX, regionY, retryPayload);
- const retryRes = await fetch(
- `https://backend.wplace.live/s0/pixel/${regionX}/${regionY}`,
- {
- method: 'POST',
- headers: { 'Content-Type': 'text/plain;charset=UTF-8', "x-pawtect-token": wasmtoken },
- credentials: 'include',
- body: JSON.stringify(retryPayload),
- }
- );
-
- if (retryRes.status === 403) {
- setTurnstileToken(null);
- createTokenPromise();
- return 'token_error';
- }
-
- const retryData = await retryRes.json();
- return retryData?.painted === pixelBatch.length;
- } catch (retryError) {
- console.error('❌ Token regeneration failed:', retryError);
- setTurnstileToken(null);
- createTokenPromise();
- return 'token_error';
+ 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';
}
}
@@ -6663,6 +6940,211 @@ function getText(key, params) {
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 = `
Initializing...
`;
+ }
+
+ let originalToken = null;
+
+ try {
+ await getAccounts();
+ const accountsTokens = JSON.parse(localStorage.getItem("accounts")) || [];
+ if (accountsTokens.length === 0) {
+ if (accountsListArea) accountsListArea.innerHTML = `
No accounts found.
`;
+ return;
+ }
+
+ const { id: originalId } = await WPlaceService.getCharges();
+ state.allAccountsInfo = [];
+ renderAccountsList();
+
+ for (let i = 0; i < accountsTokens.length; i++) {
+ const token = accountsTokens[i];
+ swapAccountTrigger(token);
+
+ let retries = 0;
+ let swapped = false;
+ let fetchedInfo = null;
+ while (retries < 5 && !swapped) {
+ await Utils.sleep(1000);
+ try {
+ fetchedInfo = await WPlaceService.fetchCheck();
+ if (fetchedInfo.ID) swapped = true;
+ } catch (e) { retries++; }
+ }
+
+ if (swapped) {
+ await fetchAccount();
+ const actualName = fetchedInfo.Username || `User${fetchedInfo.ID}` || `Account ${i + 1}`;
+ if (fetchedInfo.ID === originalId) originalToken = token;
+ state.allAccountsInfo.push({ ...fetchedInfo, token, displayName: actualName, isCurrent: fetchedInfo.ID === originalId });
+ } else {
+ const fallbackName = `Account ${i + 1}`;
+ state.allAccountsInfo.push({ token, ID: `...${token.slice(-4)}`, displayName: fallbackName, error: 'Failed to fetch' });
+ }
+ renderAccountsList();
+ }
+ } catch (error) {
+ console.error("Error fetching all account details:", error);
+ if (accountsListArea) accountsListArea.innerHTML = `
Error loading accounts.
`;
+ } finally {
+ if (originalToken) swapAccountTrigger(originalToken);
+ await Utils.sleep(1000);
+
+ // After switching back, update stats and sync the list
+ const meData = await WPlaceService.getCharges();
+ state.currentCharges = Math.floor(meData.charges);
+ state.cooldown = meData.cooldown;
+ state.maxCharges = Math.floor(meData.max) > 1 ? Math.floor(meData.max) : state.maxCharges;
+ const currentAccountInList = state.allAccountsInfo.find(acc => acc.ID === meData.id);
+ if (currentAccountInList) {
+ currentAccountInList.Charges = state.currentCharges;
+ currentAccountInList.Max = state.maxCharges;
+ currentAccountInList.Droplets = meData.droplets;
+ }
+ await updateStats();
+ renderAccountsList();
+
+ state.isFetchingAllAccounts = false;
+ if (refreshBtn) {
+ refreshBtn.innerHTML = '
';
+ refreshBtn.disabled = false;
+ }
+ }
+ }
+
+ // Function to update current account charges in the account list
+ 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) {
+ currentAccountInList.Charges = Math.floor(state.displayCharges || state.preciseCurrentCharges || 0);
+ currentAccountInList.Max = state.maxCharges;
+ // Re-render the account list to show updated charges
+ renderAccountsList();
+ }
+ }
+
+ 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;
+ }
+
+ state.allAccountsInfo.forEach((info, index) => {
+ const item = document.createElement('div');
+ item.className = `wplace-account-item ${info.isCurrent ? 'is-current' : ''}`;
+
+ // Create ordering number element
+ const orderNumber = document.createElement('div');
+ orderNumber.className = 'wplace-account-number';
+ orderNumber.textContent = index + 1;
+
+ 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) {
+ state.accountIndex = (state.accountIndex + 1) % accounts.length;
+ console.log(`🔄 Switching to account index: ${state.accountIndex}`);
+
+ const nextToken = accounts[state.accountIndex];
+ console.log(`🔑 Next token: ${nextToken}`);
+
+ if (!nextToken) {
+ console.warn('⚠️ Invalid token, skipping...');
+ return false;
+ }
+
+ return await switchToSpecificAccount(nextToken, state.accountIndex);
+ }
+
+ async function switchToSpecificAccount(token, accountIndex) {
+ swapAccountTrigger(token);
+
+ let maxRetries = 20;
+ let retryCount = 0;
+ let swapSuccess = false;
+
+ while (!swapSuccess && retryCount < maxRetries) {
+ console.log(`⏳ Waiting for account swap... (Attempt ${retryCount + 1}/${maxRetries})`);
+
+ await new Promise(resolve => setTimeout(resolve, 1000));
+
+ try {
+ await fetchAccount();
+ console.log('✅ Account swap confirmed.');
+ swapSuccess = true;
+ } catch (error) {
+ console.warn('❌ Account swap not yet successful. Retrying...', error);
+ retryCount++;
+ }
+ }
+
+ if (swapSuccess) {
+ const { charges, cooldown } = await WPlaceService.getCharges();
+ state.displayCharges = Math.floor(charges);
+ state.preciseCurrentCharges = charges;
+ state.cooldown = cooldown;
+ state.accountIndex = accountIndex;
+ Utils.performSmartSave();
+ updateStats();
+ return true;
+ } else {
+ console.error('❌ Failed to swap account after multiple retries.');
+ return false;
+ }
+ }
+
// Wait for dependencies before initializing UI
async function waitForDependenciesAndInitialize() {
// Wait for all global managers to be available
diff --git a/Extension/scripts/Auto-Repair.js b/Extension/scripts/Auto-Repair.js
index cde5232..a4ef4ee 100644
--- a/Extension/scripts/Auto-Repair.js
+++ b/Extension/scripts/Auto-Repair.js
@@ -1773,16 +1773,21 @@
const targetColor = Utils.resolveColor([originalR, originalG, originalB], state.availableColors);
const currentColor = Utils.resolveColor(currentPixel.slice(0, 3), state.availableColors);
- if (targetColor.id !== currentColor.id) {
- damagedPixels.push({
- x,
- y,
- originalColor: targetColor,
- currentColor: currentColor,
- originalRgb: [originalR, originalG, originalB],
- currentRgb: currentPixel.slice(0, 3),
- isDamagedTransparent: false
- });
+ 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) {
diff --git a/Extension/scripts/image-processor.js b/Extension/scripts/image-processor.js
index f25e22f..cc8bff6 100644
--- a/Extension/scripts/image-processor.js
+++ b/Extension/scripts/image-processor.js
@@ -10,7 +10,7 @@
/**
* ImageProcessor - Handles image loading, processing, color conversion, and dithering for WPlace AutoBot
- * Extracted from Auto-Image.js for better modularity and reusability
+ * Extracted from Auto-Image.js for better modularity and reusabilit
*/
class ImageProcessor {
constructor(imageSrc = null) {
diff --git a/Extension/scripts/overlay-manager.js b/Extension/scripts/overlay-manager.js
index c7f3fa5..b3290ef 100644
--- a/Extension/scripts/overlay-manager.js
+++ b/Extension/scripts/overlay-manager.js
@@ -10,7 +10,7 @@
/**
* OverlayManager - Handles overlay processing, tile chunking, and canvas operations for WPlace AutoBot
- * Extracted from Auto-Image.js for better modularity and reusability
+ * Extracted from Auto-Image.js for better modularity and reusabilityc
*/
class OverlayManager {
constructor() {
diff --git a/Extension/scripts/token-manager.js b/Extension/scripts/token-manager.js
index 6e07a7f..874bfe4 100644
--- a/Extension/scripts/token-manager.js
+++ b/Extension/scripts/token-manager.js
@@ -5,12 +5,12 @@
// @description Turnstile token management for WPlace AutoBot
// @author Wbot
// @match https://wplace.live/*
-// @grant none
+// @grant nones
// ==/UserScript==
/**
* TokenManager - Handles Turnstile token generation, caching, and validation for WPlace AutoBot
- * Extracted from Auto-Image.js for better modularity and reusability
+ * Extracted from Auto-Image.js for better modularity and reusabilitys
*/
class TokenManager {
constructor() {
diff --git a/Extension/scripts/utils-manager.js b/Extension/scripts/utils-manager.js
index c1bf6e4..e0f458a 100644
--- a/Extension/scripts/utils-manager.js
+++ b/Extension/scripts/utils-manager.js
@@ -1,6 +1,6 @@
/**
* WPlace AutoBOT - Utils Manager
- * Centralized utility functions for the WPlace automation system
+ * Centralized utility functions for the WPlace automation systemds
*/
class WPlaceUtilsManager {
@@ -408,6 +408,16 @@ class WPlaceUtilsManager {
}
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);
@@ -423,15 +433,34 @@ class WPlaceUtilsManager {
}
calculateEstimatedTime(remainingPixels, charges, cooldown) {
- if (remainingPixels <= 0) return 0;
+ // 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 = window.state.paintingSpeed > 0 ? 1000 / window.state.paintingSpeed : 1000;
+ const paintingSpeedDelay = 1000 / paintingSpeed;
const timeFromSpeed = remainingPixels * paintingSpeedDelay;
const cyclesNeeded = Math.ceil(remainingPixels / Math.max(charges, 1));
const timeFromCharges = cyclesNeeded * cooldown;
- return timeFromSpeed + timeFromCharges;
+ 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
@@ -472,6 +501,11 @@ class WPlaceUtilsManager {
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();
diff --git a/Extension/themes/acrylic.css b/Extension/themes/acrylic.css
index 10995a5..c096e97 100644
--- a/Extension/themes/acrylic.css
+++ b/Extension/themes/acrylic.css
@@ -627,3 +627,156 @@
border-radius: var(--wplace-radius);
color: var(--wplace-text);
}
+
+/* Account switching UI styles */
+.wplace-theme-acrylic .wplace-section {
+ margin: 15px 0;
+ border-radius: var(--wplace-radius);
+ background: rgba(255, 255, 255, 0.08);
+ padding: 15px;
+ 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-bottom: 10px;
+ 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: 200px;
+ 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;
+}
diff --git a/Extension/themes/classic-light.css b/Extension/themes/classic-light.css
index 09fd3ae..742b9d9 100644
--- a/Extension/themes/classic-light.css
+++ b/Extension/themes/classic-light.css
@@ -556,7 +556,7 @@
color: var(--wplace-text);
padding: 4px 8px;
font-size: 12px;
- width: 50px;
+ width: 65px;
text-align: center;
transition: all 0.2s ease;
height: 22px;
@@ -617,6 +617,164 @@
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;
+}
+
diff --git a/Extension/themes/classic.css b/Extension/themes/classic.css
index 6db93a4..b20fd21 100644
--- a/Extension/themes/classic.css
+++ b/Extension/themes/classic.css
@@ -689,7 +689,7 @@
color: white;
padding: 4px 8px;
font-size: 12px;
- width: 50px;
+ width: 65px;
text-align: center;
transition: all 0.2s ease;
height: 22px;
@@ -747,3 +747,177 @@
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;
+}
+
diff --git a/Extension/themes/neon-cyan.css b/Extension/themes/neon-cyan.css
index f41140d..33f4858 100644
--- a/Extension/themes/neon-cyan.css
+++ b/Extension/themes/neon-cyan.css
@@ -455,7 +455,7 @@
color: var(--wplace-text) !important;
padding: 2px 4px;
font-size: 10px;
- width: 40px;
+ width: 65px;
text-align: center;
transition: all 0.2s ease;
height: 18px;
@@ -516,3 +516,188 @@
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;
+}
diff --git a/Extension/themes/neon-light.css b/Extension/themes/neon-light.css
index e7c662c..f5debd0 100644
--- a/Extension/themes/neon-light.css
+++ b/Extension/themes/neon-light.css
@@ -455,7 +455,7 @@
color: var(--wplace-text) !important;
padding: 2px 4px;
font-size: 10px;
- width: 40px;
+ width: 65px;
text-align: center;
transition: all 0.2s ease;
height: 18px;
@@ -517,3 +517,189 @@
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;
+}
diff --git a/Extension/themes/neon.css b/Extension/themes/neon.css
index 184cb64..a5f0e4c 100644
--- a/Extension/themes/neon.css
+++ b/Extension/themes/neon.css
@@ -571,7 +571,7 @@
color: var(--wplace-text) !important;
padding: 2px 4px;
font-size: 10px;
- width: 40px;
+ width: 65px;
text-align: center;
transition: all 0.2s ease;
height: 18px;
@@ -633,3 +633,188 @@
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;
+}
+