您需要先安装一个扩展,例如 篡改猴、Greasemonkey 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 Userscripts ,之后才能安装此脚本。
您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。
您需要先安装用户脚本管理器扩展后才能安装此脚本。
ETA bubble with draggable UI
// ==UserScript== // @name Wplace charge regen ETA bubble // @namespace Zex2 // @version 3.0.7 // @description ETA bubble with draggable UI // @match https://wplace.live/* // @author Zex2 // @grant GM_registerMenuCommand // @grant GM_getValue // @grant GM_setValue // @run-at document-idle // @license MIT // ==/UserScript== (function () { 'use strict'; // --- Storage keys const K = { cur: 'eta_cur', max: 'eta_max', debugOn: 'eta_debug_on', bubblePos: 'eta_bubble_pos', panelPos: 'eta_panel_pos' }; // --- State let current = GM_getValue(K.cur, 148); let max = GM_getValue(K.max, 208); let lastTimer = null; // seconds until next tick (parsed from (m:ss)) let lastTickCheck = Date.now(); // ms clock for AFK recovery let lastAutoIncAt = 0; // ms guard to avoid double-increment on regen detection let debugEnabled = GM_getValue(K.debugOn, true); // --- UI elements let bubble, panel, panelBody; // --- Menu GM_registerMenuCommand('Set current charges', () => { const val = prompt('Current charges:', String(current)); if (val === null) return; current = clampInt(parseInt(val, 10), 0, 9999); GM_setValue(K.cur, current); log(`Manual set: current=${current}`); render(); }); GM_registerMenuCommand('Set max charges', () => { const val = prompt('Max charges:', String(max)); if (val === null) return; max = clampInt(parseInt(val, 10), 1, 9999); GM_setValue(K.max, max); log(`Manual set: max=${max}`); render(); }); GM_registerMenuCommand('Toggle debug overlay', () => { debugEnabled = !debugEnabled; GM_setValue(K.debugOn, debugEnabled); if (panel) panel.style.display = debugEnabled ? 'block' : 'none'; log(`Debug overlay ${debugEnabled ? 'ENABLED' : 'DISABLED'}`); }); GM_registerMenuCommand('Reset positions', () => { GM_setValue(K.bubblePos, null); GM_setValue(K.panelPos, null); place(bubble, { x: null, y: null }, { bottom: 20, right: 20 }); place(panel, { x: null, y: null }, { top: 20, left: 20 }); log('Positions reset'); }); // --- Ensure body exists then init if (document.readyState === 'loading') { document.addEventListener('DOMContentLoaded', init); } else { init(); } function init() { createBubble(); createPanel(); makeDraggable(bubble, K.bubblePos, { bottom: 20, right: 20 }); makeDraggable(panel, K.panelPos, { top: 20, left: 20 }); // Initial render render(); // Ticker setInterval(tick, 500); } // --- Core loop function tick() { const timerEl = findTimerEl(); if (!timerEl) { setBubble('Waiting for timer…', `${current}/${max}`); return; } const untilNext = parseTimer(timerEl.textContent); if (untilNext == null) { setBubble('Invalid timer', `${current}/${max}`); return; } // Auto-increment on cycle reset (e.g., 0:01 -> 0:30) if (lastTimer != null) { const jumpedUp = untilNext > lastTimer + 10; // robust jump threshold const enoughSinceLastInc = (Date.now() - lastAutoIncAt) > 15000; // guard if (jumpedUp && current < max && enoughSinceLastInc) { current += 1; lastAutoIncAt = Date.now(); GM_setValue(K.cur, current); log(`Regen: timer reset detected (+1) → current=${current}/${max}`); } } lastTimer = untilNext; // AFK recovery (award charges for elapsed time) const now = Date.now(); const elapsed = Math.floor((now - lastTickCheck) / 1000); if (elapsed > 40) { const gained = Math.floor(elapsed / 30); if (gained > 0 && current < max) { const before = current; current = Math.min(max, current + gained); GM_setValue(K.cur, current); log(`AFK recovery: +${current - before} (elapsed ${elapsed}s) → current=${current}/${max}`); } } lastTickCheck = now; // Clamp and render if (current >= max) { current = max; setBubble('Fully charged', `${current}/${max}`); return; } const etaSec = Math.max(0, (max - current - 1) * 30 + untilNext); setBubble(formatHMS(etaSec) + ' to full', `${current}/${max}`); } // --- Helpers function findTimerEl() { // Scan common inline text containers for a string like "(m:ss)" const candidates = document.querySelectorAll('time, span, div, p'); for (const el of candidates) { const t = el.textContent || ''; if (/\(\d+:\d{2}\)/.test(t)) return el; } return null; } function parseTimer(s) { const m = /\((\d+):(\d{2})\)/.exec(s || ''); if (!m) return null; const mins = parseInt(m[1], 10); const secs = parseInt(m[2], 10); if (isNaN(mins) || isNaN(secs)) return null; return mins * 60 + secs; } function formatHMS(totalSec) { const s = Math.max(0, Math.ceil(totalSec)); const h = Math.floor(s / 3600); const m = Math.floor((s % 3600) / 60); const sec = s % 60; if (h > 0) return `${h}h ${String(m).padStart(2,'0')}m ${String(sec).padStart(2,'0')}s`; return `${m}m ${String(sec).padStart(2,'0')}s`; } function clampInt(n, lo, hi) { if (!Number.isFinite(n)) return lo; return Math.min(hi, Math.max(lo, n | 0)); } // --- Bubble renderer (adds edit button; listeners are delegated) function setBubble(line1, counts) { if (!bubble) return; bubble.innerHTML = ` <div style="font-weight:600">${line1}</div> <div style="display:flex; justify-content:center; align-items:center; gap:6px; opacity:.9"> <span class="charge-counts">${counts}</span> <button class="bubble-edit-btn" title="Edit current/max" style=" background:none; border:1px solid #8a8f98; border-radius:4px; color:#cbd5e1; font-size:11px; padding:0 6px; cursor:pointer; line-height:1.6;">⚙️</button> </div> `; } function log(msg) { if (!panelBody) return; const ts = new Date(); const t = ts.toLocaleTimeString([], { hour12: false }); const row = document.createElement('div'); row.textContent = `[${t}] ${msg}`; panelBody.appendChild(row); // cap to last 120 lines while (panelBody.childNodes.length > 120) { panelBody.removeChild(panelBody.firstChild); } if (debugEnabled && panel) panel.style.display = 'block'; } // --- UI builders function createBubble() { bubble = document.createElement('div'); Object.assign(bubble.style, { position: 'fixed', bottom: '20px', right: '20px', padding: '8px 10px', background: 'rgba(16,18,22,0.9)', color: '#e6edf3', border: '1px solid #2b3138', borderRadius: '10px', font: '12px/1.4 system-ui, Segoe UI, Roboto, Arial, sans-serif', zIndex: 2147483647, boxShadow: '0 6px 18px rgba(0,0,0,0.25)', cursor: 'move', minWidth: '160px' }); // Delegate edit button click (no duplicate listeners on re-render) bubble.addEventListener('click', (e) => { const editBtn = e.target.closest('.bubble-edit-btn'); if (!editBtn) return; e.stopPropagation(); const inVal = prompt('Enter current/max charges:', `${current}/${max}`); if (!inVal) return; const m = inVal.match(/^\s*(\d+)\s*\/\s*(\d+)\s*$/); if (!m) { alert('Format must be: number/number (e.g., 7/10)'); return; } const newCur = clampInt(parseInt(m[1], 10), 0, 9999); const newMax = clampInt(parseInt(m[2], 10), 1, 9999); current = Math.min(newCur, newMax); max = newMax; GM_setValue(K.cur, current); GM_setValue(K.max, max); log(`Inline edit via button: current=${current}, max=${max}`); render(); }); // Prevent drag start when pressing the edit button bubble.addEventListener('mousedown', (e) => { if (e.target.closest('.bubble-edit-btn')) e.stopPropagation(); }, true); document.body.appendChild(bubble); place(bubble, GM_getValue(K.bubblePos, null), { bottom: 20, right: 20 }); } function createPanel() { panel = document.createElement('div'); Object.assign(panel.style, { position: 'fixed', top: '20px', left: '20px', width: '360px', maxHeight: '40vh', overflow: 'auto', background: 'rgba(10,12,16,0.95)', color: '#cbd5e1', border: '1px solid #2b3138', borderRadius: '8px', font: '12px/1.4 ui-monospace, SFMono-Regular, Menlo, Consolas, monospace', zIndex: 2147483647, boxShadow: '0 8px 22px rgba(0,0,0,0.35)', display: debugEnabled ? 'block' : 'none', cursor: 'move' }); const header = document.createElement('div'); header.textContent = 'Charge ETA — Debug'; Object.assign(header.style, { padding: '6px 8px', fontWeight: '700', borderBottom: '1px solid #2b3138', background: 'rgba(255,255,255,0.03)' }); const controls = document.createElement('div'); Object.assign(controls.style, { display: 'flex', gap: '8px', padding: '6px 8px', borderBottom: '1px solid #2b3138', flexWrap: 'wrap' }); const btnHide = document.createElement('button'); btnHide.textContent = 'Do not show again'; styleBtn(btnHide); btnHide.onclick = () => { debugEnabled = false; GM_setValue(K.debugOn, false); panel.style.display = 'none'; }; const btnClear = document.createElement('button'); btnClear.textContent = 'Clear'; styleBtn(btnClear); btnClear.onclick = () => { panelBody.innerHTML = ''; log('Log cleared'); }; const btnSync = document.createElement('button'); btnSync.textContent = 'Sync +1'; styleBtn(btnSync); btnSync.onclick = () => { if (current < max) { current += 1; GM_setValue(K.cur, current); log(`Manual sync: +1 → current=${current}/${max}`); render(); } }; controls.append(btnHide, btnClear, btnSync); panelBody = document.createElement('div'); Object.assign(panelBody.style, { padding: '6px 8px', whiteSpace: 'pre-wrap' }); panel.append(header, controls, panelBody); document.body.appendChild(panel); place(panel, GM_getValue(K.panelPos, null), { top: 20, left: 20 }); // initial line shows state log(`Startup: current=${current}/${max}`); } function styleBtn(b) { Object.assign(b.style, { background: '#0b63ff', color: 'white', border: '1px solid #1547b0', padding: '2px 6px', borderRadius: '6px', cursor: 'pointer', font: '600 11px system-ui, sans-serif' }); } // --- Positioning + drag with persistence function makeDraggable(el, storageKey, fallback) { if (!el) return; let start = null; el.addEventListener('mousedown', (e) => { if (e.target.closest('button, input, textarea, select, a')) return; start = { x: e.clientX, y: e.clientY, left: el.offsetLeft, top: el.offsetTop }; e.preventDefault(); }); window.addEventListener('mousemove', (e) => { if (!start) return; const dx = e.clientX - start.x; const dy = e.clientY - start.y; el.style.left = (start.left + dx) + 'px'; el.style.top = (start.top + dy) + 'px'; el.style.right = 'auto'; el.style.bottom = 'auto'; }); window.addEventListener('mouseup', () => { if (!start) return; start = null; const rect = el.getBoundingClientRect(); GM_setValue(storageKey, { x: rect.left + window.scrollX, y: rect.top + window.scrollY }); }); // Touch el.addEventListener('touchstart', (e) => { const t = e.touches[0]; start = { x: t.clientX, y: t.clientY, left: el.offsetLeft, top: el.offsetTop }; }, { passive: true }); window.addEventListener('touchmove', (e) => { if (!start) return; const t = e.touches[0]; const dx = t.clientX - start.x; const dy = t.clientY - start.y; el.style.left = (start.left + dx) + 'px'; el.style.top = (start.top + dy) + 'px'; el.style.right = 'auto'; el.style.bottom = 'auto'; }, { passive: true }); window.addEventListener('touchend', () => { if (!start) return; start = null; const rect = el.getBoundingClientRect(); GM_setValue(storageKey, { x: rect.left + window.scrollX, y: rect.top + window.scrollY }); }); // Initial placement place(el, GM_getValue(storageKey, null), fallback); } function place(el, saved, fallback) { if (!el) return; if (saved && typeof saved.x === 'number' && typeof saved.y === 'number') { el.style.left = saved.x + 'px'; el.style.top = saved.y + 'px'; el.style.right = 'auto'; el.style.bottom = 'auto'; } else { if (fallback.left != null) el.style.left = fallback.left + 'px'; if (fallback.top != null) el.style.top = fallback.top + 'px'; if (fallback.right != null) el.style.right = fallback.right + 'px'; if (fallback.bottom != null) el.style.bottom = fallback.bottom + 'px'; } } function render() { const timerEl = findTimerEl(); const untilNext = timerEl ? parseTimer(timerEl.textContent) : null; if (current >= max) { current = max; setBubble('Fully charged', `${current}/${max}`); return; } if (untilNext == null) { setBubble('Waiting for timer…', `${current}/${max}`); return; } const etaSec = Math.max(0, (max - current - 1) * 30 + untilNext); setBubble(formatHMS(etaSec) + ' to full', `${current}/${max}`); } })();