diff --git a/Extension/background.js b/Extension/background.js
index fe15f65..0217179 100644
--- a/Extension/background.js
+++ b/Extension/background.js
@@ -13,29 +13,6 @@ chrome.runtime.onInstalled.addListener(async () => {
console.log('📦 Installation resource test:', resources);
});
-chrome.runtime.onMessage.addListener((request, sender, sendResponse) => {
- if (request.action === 'executeScript') {
- // Get tabId from sender or requests
- const tabId = request.tabId || sender.tab?.id;
-
- if (!tabId) {
- sendResponse({ success: false, error: 'Could not determine target tab' });
- return;
- }
-
- // Use IIFE for async handlings
- (async () => {
- try {
- await executeLocalScript(request.scriptName, tabId);
- sendResponse({ success: true });
- } catch (error) {
- sendResponse({ success: false, error: error.message });
- }
- })();
-
- return true; // Important: indicates async response
- }
-});
async function executeLocalScript(scriptName, tabId) {
try {
diff --git a/Extension/scripts/Auto-Image.js b/Extension/scripts/Auto-Image.js
index 8df37c5..93d5040 100644
--- a/Extension/scripts/Auto-Image.js
+++ b/Extension/scripts/Auto-Image.js
@@ -10,36 +10,6 @@
// ==/UserScript==
localStorage.removeItem("lp");
-// Fallback translation function for when utils manager isn't loaded
-function getText(key, params) {
- // Try to get translation from loadedTranslations
- try {
- if (window.loadedTranslations && window.loadedTranslations[key]) {
- let text = window.loadedTranslations[key];
- if (params) {
- Object.keys(params).forEach(paramKey => {
- text = text.replace(new RegExp(`{{${paramKey}}}`, 'g'), params[paramKey]);
- });
- }
- return text;
- }
-
- // Try with state.language if available
- if (window.state && window.state.language && window.loadedTranslations && window.loadedTranslations[window.state.language] && window.loadedTranslations[window.state.language][key]) {
- let text = window.loadedTranslations[window.state.language][key];
- if (params) {
- Object.keys(params).forEach(paramKey => {
- text = text.replace(new RegExp(`{{${paramKey}}}`, 'g'), params[paramKey]);
- });
- }
- return text;
- }
- } catch (error) {
- console.warn('Error in getText fallback:', error);
- }
-
- return key; // Fallback to key if no translation found
-}
; (async () => {
// Prevent multiple instances of this script from running
@@ -952,6 +922,94 @@ function getText(key, params) {
// Create global account manager instance
const accountManager = new AccountManager();
+ // Local Charge Model to minimize API calls and drive account switching
+ const ChargeModel = (() => {
+ class Model {
+ constructor() {
+ this.map = new Map(); // token -> {charges,max,lastTickAt,lastSyncAt}
+ this.tickIntervalMs = 30_000; // +1 charge per 30s
+ this.timer = null;
+ this.startedAt = Date.now();
+ }
+ seedFromAccounts(accounts) {
+ const now = Date.now();
+ (accounts || []).forEach(acc => {
+ if (!acc || !acc.token) return;
+ const existing = this.map.get(acc.token) || {};
+ const charges = Number.isFinite(acc.Charges) ? Math.floor(acc.Charges) : (existing.charges || 0);
+ const max = Number.isFinite(acc.Max) ? Math.floor(acc.Max) : (existing.max || 1);
+ this.map.set(acc.token, {
+ charges: Math.max(0, Math.min(charges, max)),
+ max: Math.max(1, max),
+ lastTickAt: existing.lastTickAt || now,
+ lastSyncAt: existing.lastSyncAt || now,
+ });
+ });
+ }
+ ensureToken(token) {
+ if (!token) return null;
+ if (!this.map.has(token)) {
+ this.map.set(token, { charges: 0, max: Math.max(1, state.maxCharges || 1), lastTickAt: Date.now(), lastSyncAt: 0 });
+ }
+ return this.map.get(token);
+ }
+ get(token) { return this.ensureToken(token); }
+ getForCurrent() { return this.get(accountManager.getCurrentAccount()?.token); }
+ setFromServer(token, charges, max) {
+ const node = this.ensureToken(token);
+ if (!node) return;
+ node.charges = Math.max(0, Math.min(Math.floor(charges || 0), Math.max(1, Math.floor(max || node.max || 1))));
+ node.max = Math.max(1, Math.floor(max || node.max || 1));
+ node.lastSyncAt = Date.now();
+ }
+ decrement(token, amount) {
+ const node = this.ensureToken(token);
+ if (!node) return 0;
+ const n = Math.max(0, Math.floor(amount || 0));
+ node.charges = Math.max(0, node.charges - n);
+ return node.charges;
+ }
+ incrementTickAll() {
+ const now = Date.now();
+ accountManager.getAllAccounts().forEach(acc => {
+ if (!acc?.token) return;
+ const node = this.ensureToken(acc.token);
+ if (!node) return;
+ // catch-up ticks if tab was inactive
+ const elapsed = now - (node.lastTickAt || now);
+ const ticks = Math.floor(elapsed / this.tickIntervalMs);
+ if (ticks > 0) {
+ node.charges = Math.min(node.max, node.charges + ticks);
+ node.lastTickAt = (node.lastTickAt || now) + ticks * this.tickIntervalMs;
+ }
+ });
+ // Mirror values into AccountManager and UI state
+ this.syncToAccountManager();
+ }
+ start() {
+ if (this.timer) return;
+ this.timer = setInterval(() => this.incrementTickAll(), this.tickIntervalMs);
+ }
+ stop() { if (this.timer) { clearInterval(this.timer); this.timer = null; } }
+ predictTimeToReach(token, target) {
+ const node = this.get(token);
+ if (!node) return Infinity;
+ if (node.charges >= target) return 0;
+ return (target - node.charges) * this.tickIntervalMs;
+ }
+ syncToAccountManager() {
+ const list = accountManager.getAllAccounts();
+ list.forEach(acc => {
+ const node = this.map.get(acc.token);
+ if (!node) return;
+ accountManager.updateAccountData(acc.token, { Charges: node.charges, Max: node.max });
+ });
+ renderAccountsList();
+ }
+ }
+ return new Model();
+ })();
+
// GLOBAL STATE
const state = {
running: false,
@@ -1017,6 +1075,10 @@ function getText(key, params) {
notificationIntervalMinutes: CONFIG.NOTIFICATIONS.REPEAT_MINUTES,
_lastChargesNotifyAt: 0,
_lastChargesBelow: true,
+ // Switch debouncing state
+ lastSwitchAt: 0,
+ paintedSinceSwitch: 0,
+ minMsBetweenSwitches: 3000,
// Smart save tracking
_lastSavePixelCount: 0,
_lastSaveTime: 0,
@@ -4348,7 +4410,7 @@ function getText(key, params) {
const timeText = Utils.msToTimeText(remainingMs);
if (currentChargesEl) {
- currentChargesEl.innerHTML = `${state.displayCharges} / ${state.maxCharges}`;
+ currentChargesEl.innerHTML = `${state.displayCharges} / ${max}`;
}
if (
@@ -4546,7 +4608,7 @@ function getText(key, params) {
totalMaxCharges = accounts.reduce((sum, acc) => {
if (currentAccount && acc.token === currentAccount.token) {
// Use real-time max charges for current account
- return sum + Math.floor(state.maxCharges || acc.Max || 0);
+ return sum + Math.floor((state.fullChargeData?.max ?? state.maxCharges ?? acc.Max ?? 0));
} else {
// Use stored data for other accounts
return sum + Math.floor(acc.Max || 0);
@@ -4571,7 +4633,7 @@ function getText(key, params) {
${Utils.t('charges')}
- ${state.displayCharges} / ${state.maxCharges}
+ ${state.displayCharges} / ${state.fullChargeData?.max ?? state.maxCharges}
@@ -8503,9 +8565,20 @@ function getText(key, params) {
state.lastPaintedPosition = { x: lastPixel.localX, y: lastPixel.localY };
}
+ // Track painted pixels since last account switch to avoid flip-flop
+ state.paintedSinceSwitch = (state.paintedSinceSwitch || 0) + actuallyPaintedCount;
+
// 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);
+ // Also update the global local charge model per account with bonus logic
+ try {
+ const tok = accountManager.getCurrentAccount()?.token;
+ const after = ChargeModel.decrement(tok, batchSize);
+ state.displayCharges = Math.floor(after);
+ state.preciseCurrentCharges = after;
+ if (tok) accountManager.updateAccountData(tok, { Charges: state.displayCharges });
+ } catch {}
state.fullChargeData = {
...state.fullChargeData,
@@ -8671,13 +8744,23 @@ function getText(key, params) {
// 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;
+ // Use local charge model to avoid extra API calls
+ const currentToken = accountManager.getCurrentAccount()?.token;
+ let node = ChargeModel.get(currentToken);
+ // One-time sync for current account if never synced (safety)
+ if (node && !node.lastSyncAt) {
+ try {
+ const sync = await WPlaceService.getCharges();
+ ChargeModel.setFromServer(currentToken, sync.charges, sync.max);
+ node = ChargeModel.get(currentToken);
+ } catch {}
+ }
+ state.displayCharges = Math.floor(node?.charges || 0);
+ state.preciseCurrentCharges = node?.charges || 0;
+ state.cooldown = CONFIG.COOLDOWN_DEFAULT;
await updateStats();
if (state.displayCharges <= 0) {
- console.log('⚡ No charges available, skipping painting session');
+ console.log('⚡ No charges available (local), skipping painting session');
return 'charges_depleted';
}
@@ -9030,55 +9113,38 @@ function getText(key, params) {
// const maxChargeChecks = 10; // REMOVED: No limit on API calls during cooldown
while (!state.stopFlag) {
- chargeCheckCount++;
+ const threshold = Math.max(1, state.cooldownChargeThreshold || 1);
+ const accounts = accountManager.getAllAccounts();
+ let anyReady = false;
+ let bestMs = Infinity;
+ let currentCharges = ChargeModel.getForCurrent()?.charges || 0;
- const { charges, cooldown } = await WPlaceService.getCharges();
- state.displayCharges = Math.floor(charges);
- state.preciseCurrentCharges = charges;
- state.cooldown = cooldown;
+ for (const acc of accounts) {
+ const node = ChargeModel.get(acc.token);
+ if (!node) continue;
+ if (node.charges >= threshold) {
+ anyReady = true;
+ break;
+ }
+ const ms = ChargeModel.predictTimeToReach(acc.token, threshold);
+ if (ms < bestMs) bestMs = ms;
+ }
- if (state.displayCharges >= state.cooldownChargeThreshold) {
- console.log(`✅ Cooldown target reached: ${state.displayCharges}/${state.cooldownChargeThreshold}`);
+ if (anyReady) {
+ console.log(`✅ Cooldown target reached locally (≥${threshold})`);
NotificationManager.maybeNotifyChargesReached(true);
await updateStats();
return 'target_reached';
}
+ const waitMs = Number.isFinite(bestMs) ? Math.max(1000, Math.min(bestMs, 10000)) : 10000;
updateUI('noChargesThreshold', 'warning', {
- time: Utils.msToTimeText(state.cooldown),
- threshold: state.cooldownChargeThreshold,
- current: state.displayCharges,
+ time: Utils.msToTimeText(waitMs),
+ threshold,
+ current: currentCharges,
});
await updateStats();
-
- // Smart delay calculation to reduce API calls
- const chargesNeeded = state.cooldownChargeThreshold - state.displayCharges;
- const estimatedWaitTime = chargesNeeded * state.cooldown;
-
- // Use longer delays during cooldown to prevent rate limiting
- let delayTime;
- if (chargeCheckCount < 3) {
- // First few checks - shorter delay
- delayTime = Math.max(3000, state.cooldown); // 3 seconds minimum
- } else if (estimatedWaitTime > 60000) {
- // Long wait expected - check every 15 seconds
- delayTime = 10000;
- } else if (estimatedWaitTime > 30000) {
- // Medium wait - check every 15 seconds
- delayTime = 15000;
- } else {
- // Close to target - check every 5 seconds
- delayTime = 5000;
- }
-
- console.log(`⏱️ Cooldown check ${chargeCheckCount}: ${state.displayCharges}/${state.cooldownChargeThreshold} charges, waiting 10s before next check`);
- await Utils.sleep(10000);
-
- // REMOVED: No limit on charge checks - bot will wait infinitely until charges are available
- // if (chargeCheckCount >= maxChargeChecks) {
- // console.warn('⚠️ Max charge checks reached during cooldown, continuing anyway');
- // break;
- // }
+ await Utils.sleep(waitMs);
}
return 'stopped';
@@ -9847,9 +9913,10 @@ function getText(key, params) {
console.log('✅ Found wasm Module...');
return url.split('/').pop();
}
- } catch { }
+ } catch (e) { /* ignore individual fetch errors */ }
}
- console.error(`❌ Could not find Pawtect chunk: `, error);
+ console.error('❌ Could not find Pawtect chunk among preloaded modules');
+ return null;
}
async function purchase(type) {
@@ -9993,12 +10060,6 @@ function getText(key, params) {
});
}
- async function fetchAccount() {
- const { ID, Charges, Max, Droplets } = await WPlaceService.fetchCheck();
- console.log("User's ID :", ID);
- console.log("User's Charges :", Charges, "/", Max);
- console.log("User's Droplets :", Droplets);
- }
async function fetchAllAccountDetails() {
if (state.isFetchingAllAccounts) {
@@ -10109,6 +10170,7 @@ function getText(key, params) {
}
console.log(`🎯 [FETCH] Completed fetching fresh data for all accounts`);
+ try { ChargeModel.seedFromAccounts(accountManager.getAllAccounts()); } catch {}
}
// Render the accounts list with fresh data
@@ -10131,34 +10193,18 @@ function getText(key, params) {
// Function to update current account charges in the account list
async function updateCurrentAccountInList() {
if (accountManager.getAccountCount() === 0) return;
-
try {
- // Always trust backend /me and then map by ID to avoid mixing data between tokens
- const currentAccountData = await WPlaceService.getCharges();
- state.displayCharges = Math.floor(currentAccountData.charges);
- state.preciseCurrentCharges = currentAccountData.charges;
+ const current = accountManager.getCurrentAccount();
+ const node = ChargeModel.get(current?.token);
+ if (!current || !node) return;
+ state.displayCharges = Math.floor(node.charges || 0);
+ state.preciseCurrentCharges = node.charges || 0;
await updateStats();
-
- const accounts = accountManager.getAllAccounts();
- const idx = accounts.findIndex(acc => acc.ID === currentAccountData.id);
- const targetToken = idx !== -1 ? accounts[idx].token : accountManager.getCurrentAccount()?.token;
-
- if (targetToken) {
- accountManager.updateAccountData(targetToken, {
- Charges: Math.floor(state.displayCharges || 0),
- Max: Math.floor(currentAccountData.max || state.maxCharges || 0),
- Droplets: Math.floor(currentAccountData.droplets)
- });
-
- // Keep manager index in sync with reality when possible
- if (idx !== -1 && typeof accountManager.setCurrentIndex === 'function') {
- accountManager.setCurrentIndex(idx);
- state.accountIndex = idx;
- }
-
- // Re-render the account list to show updated charges
- renderAccountsList();
- }
+ accountManager.updateAccountData(current.token, {
+ Charges: Math.floor(node.charges || 0),
+ Max: Math.floor(node.max || current.Max || 0)
+ });
+ renderAccountsList();
} catch (e) {
console.warn('⚠️ updateCurrentAccountInList failed:', e);
}
@@ -10321,6 +10367,18 @@ function getText(key, params) {
async function switchToNextAccount(accounts) {
console.log(`🔄 [SWITCH] Starting account switch`);
+ // Debounce rapid consecutive switches when no painting happened
+ try {
+ const now = Date.now();
+ const last = state.lastSwitchAt || 0;
+ const minGap = state.minMsBetweenSwitches || 3000;
+ const painted = state.paintedSinceSwitch || 0;
+ if (now - last < minGap && painted === 0) {
+ console.log(`⏳ [SWITCH] Debounced rapid switch (Δ${now - last}ms < ${minGap}ms and paintedSinceSwitch=${painted}).`);
+ return false;
+ }
+ } catch {}
+
// Validate we have accounts
if (accountManager.getAccountCount() === 0) {
console.error('❌ No accounts available for switching');
@@ -10401,6 +10459,10 @@ function getText(key, params) {
// Update the account status and UI after successful switch
await updateCurrentAccountInList();
+ // Record switch time and reset painted counter to prevent rapid bouncing
+ state.lastSwitchAt = Date.now();
+ state.paintedSinceSwitch = 0;
+
return true;
} catch (error) {
console.error('❌ [SWITCH] Account switch failed:', error);
@@ -10411,6 +10473,19 @@ function getText(key, params) {
// SIMPLIFIED helper function for specific account switching
async function switchToSpecificAccount(token, accountName) {
console.log(`🔄 [SPECIFIC SWITCH] Attempting to switch to account: ${accountName}`);
+
+ // Debounce rapid consecutive switches when no painting happened
+ try {
+ const now = Date.now();
+ const last = state.lastSwitchAt || 0;
+ const minGap = state.minMsBetweenSwitches || 3000;
+ const painted = state.paintedSinceSwitch || 0;
+ if (now - last < minGap && painted === 0) {
+ console.log(`⏳ [SPECIFIC SWITCH] Debounced rapid switch (Δ${now - last}ms < ${minGap}ms and paintedSinceSwitch=${painted}).`);
+ return false;
+ }
+ } catch {}
+
if (!token) {
console.error('❌ [SPECIFIC SWITCH] Missing token');
return false;
@@ -10471,6 +10546,7 @@ function getText(key, params) {
// Fetch fresh stats for UI/state
const { charges, cooldown, droplets, max } = await WPlaceService.getCharges();
+ try { ChargeModel.setFromServer(token, charges, max); } catch {}
state.displayCharges = Math.floor(charges);
state.preciseCurrentCharges = charges;
state.cooldown = cooldown;
@@ -10530,42 +10606,43 @@ function getText(key, params) {
try {
const total = accountManager.getAccountCount();
if (total <= 1) return false;
+ const threshold = Math.max(1, minRequired || 1);
const startIdx = accountManager.currentIndex;
- let best = { cooldown: Infinity, token: null, name: null, idx: -1 };
+ let candidate = null; // {token,name,idx}
+ let bestWait = Infinity; // ms to reach threshold
for (let step = 1; step <= total - 1; step++) {
const idx = (startIdx + step) % total;
const acc = accountManager.getAccountByIndex(idx);
if (!acc || !acc.token) continue;
+ const node = ChargeModel.get(acc.token);
+ const localCharges = Math.floor(node?.charges || 0);
- console.log(`🔄 [SEARCH] Switching temporarily to ${acc.displayName} (${idx + 1}/${total}) to check charges...`);
- const ok = await switchToSpecificAccount(acc.token, acc.displayName);
- if (!ok) {
- console.warn(`⚠️ [SEARCH] Failed to switch to ${acc.displayName}, trying next...`);
- continue;
- }
-
- const currentAccountData = await WPlaceService.getCharges();
- const charges = Math.floor(currentAccountData?.charges || 0);
- const cooldown = Math.max(0, Number(currentAccountData?.cooldown || 0));
- if (charges >= minRequired) {
- console.log(`✅ [SEARCH] Found account with sufficient charges: ${acc.displayName} (⚡${charges})`);
- return true;
- }
-
- console.log(`⏳ [SEARCH] ${acc.displayName} has no charges (⚡${charges}), cooldown ${cooldown}ms`);
- if (cooldown < best.cooldown) {
- best = { cooldown, token: acc.token, name: acc.displayName, idx };
+ console.log(`🔍 [SEARCH] Checking locally ${acc.displayName}: ⚡${localCharges}/${node?.max ?? 0}`);
+ if (localCharges >= threshold) {
+ candidate = { token: acc.token, name: acc.displayName, idx };
+ break;
+ } else {
+ const eta = ChargeModel.predictTimeToReach(acc.token, threshold);
+ if (eta < bestWait) {
+ bestWait = eta;
+ candidate = { token: acc.token, name: acc.displayName, idx };
+ }
}
}
- // None had enough charges: switch to the one with the soonest recharge and return false
- if (best.token && best.idx !== accountManager.currentIndex) {
- console.log(`🎯 [SEARCH] Switching to account with soonest recharge: ${best.name} (~${Utils.msToTimeText(best.cooldown)})`);
- await switchToSpecificAccount(best.token, best.name);
+ if (candidate && ChargeModel.get(candidate.token)?.charges >= threshold) {
+ console.log(`✅ [SEARCH] Local model found eligible account: ${candidate.name}`);
+ const ok = await switchToSpecificAccount(candidate.token, candidate.name);
+ return !!ok;
+ }
+
+ // None eligible yet – do not switch now. Caller may enter cooldown.
+ if (candidate) {
+ console.log(`🕒 [SEARCH] No accounts meet threshold. Best candidate: ${candidate.name} in ~${Utils.msToTimeText(bestWait)}`);
} else {
- console.log('🎯 [SEARCH] Staying on current account for cooldown.');
+ console.log('🕒 [SEARCH] No candidate accounts available.');
}
return false;
} catch (e) {
@@ -10583,6 +10660,14 @@ function getText(key, params) {
console.log('🔄 Initial account load from cache...');
try {
await accountManager.loadAccounts();
+ // Seed and start local charge model regardless of count
+ try {
+ state.chargeModel = ChargeModel;
+ ChargeModel.seedFromAccounts(accountManager.getAllAccounts());
+ ChargeModel.start();
+ console.log('⚡ Local ChargeModel started (tick +1 per 30s for all accounts)');
+ } catch (e) { console.warn('ChargeModel init failed:', e); }
+
if (accountManager.getAccountCount() > 0) {
console.log(`✅ Loaded ${accountManager.getAccountCount()} cached accounts`);
renderAccountsList();
diff --git a/Extension/scripts/Auto-Repair.js b/Extension/scripts/Auto-Repair.js
index 4f6abed..3942791 100644
--- a/Extension/scripts/Auto-Repair.js
+++ b/Extension/scripts/Auto-Repair.js
@@ -595,9 +595,10 @@
console.log('✅ Found wasm Module...');
return url.split('/').pop();
}
- } catch { }
+ } catch (e) { /* ignore individual fetch errors */ }
}
- console.error(`❌ Could not find Pawtect chunk with string: ${str}`);
+ console.error('❌ Could not find Pawtect chunk among preloaded modules');
+ return null;
}
// Audio notification system