您需要先安装一个扩展,例如 篡改猴、Greasemonkey 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 Userscripts ,之后才能安装此脚本。
您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。
您需要先安装用户脚本管理器扩展后才能安装此脚本。
Track Keno numbers with multi-round support. Allows adding/overwriting on import. Pick counts can now be sorted by frequency or number.
当前为
// ==UserScript== // @name Enhanced Keno Tracker // @namespace http://zachwozn.com/ // @version 2.5 // @description Track Keno numbers with multi-round support. Allows adding/overwriting on import. Pick counts can now be sorted by frequency or number. // @author zachwozn // @match https://www.torn.com/page.php?sid=keno // @grant none // ==/UserScript== (function() { 'use strict'; // Constants for localStorage keys const LS_KEY_POSITION = 'kenoTrackerPosition'; const LS_KEY_PICK_COUNTS = 'kenoNumberPickCounts'; const LS_KEY_SORT_ORDER = 'kenoCountSortOrder'; // --- UI Creation Function --- function createUI() { const container = document.createElement('div'); container.id = 'kenoUI'; Object.assign(container.style, { position: 'fixed', top: `${getPosition().top}px`, left: `${getPosition().left}px`, backgroundColor: '#1e293b', color: '#f9fafb', padding: '20px', borderRadius: '10px', zIndex: '9999', fontFamily: 'Arial, sans-serif', boxShadow: '0 4px 10px rgba(0, 0, 0, 0.2)', maxWidth: '350px', overflowY: 'auto', textAlign: 'center', fontSize: '14px', resize: 'both', overflow: 'auto', minWidth: '250px', minHeight: '200px', cursor: 'grab' }); const style = document.createElement('style'); style.textContent = ` #kenoUI *, #kenoUI *:focus, #kenoUI *:active, #kenoUI *:hover { outline: none !important; box-shadow: none !important; -webkit-tap-highlight-color: transparent !important; } #kenoUI h3, #kenoUI h4 { border: none !important; background: none !important; padding: 0 !important; margin: 0 0 5px 0 !important; } #kenoUI #winningNumbers, #kenoUI #lostNumbers { margin-top: 0 !important; display: flex !important; flex-wrap: wrap !important; gap: 5px !important; justify-content: center; } #kenoUI #numberCountDisplay { margin-top: 0 !important; } #kenoUI #winningNumbers li, #kenoUI #lostNumbers li { display: inline-block !important; margin: 0 !important; padding: 2px 5px; border-radius: 3px; background-color: #2d3748; } #kenoUI #winningNumbersSection, #kenoUI #lostNumbersSection { margin-bottom: 15px !important; } /* --- Custom Scrollbar Styling (New Addition) --- */ /* For Firefox */ #kenoUI, #winningNumbers, #lostNumbers, #numberCountDisplay { scrollbar-width: thin; scrollbar-color: #718096 #1e293b; } /* For WebKit Browsers (Chrome, Safari, Edge, etc.) */ #kenoUI::-webkit-scrollbar { width: 8px; height: 8px; } #kenoUI::-webkit-scrollbar-track { background: transparent; } #kenoUI::-webkit-scrollbar-thumb { background-color: #4a5568; border-radius: 10px; border: 2px solid #1e293b; } #kenoUI::-webkit-scrollbar-thumb:hover { background-color: #718096; } `; document.head.appendChild(style); const title = document.createElement('h3'); title.textContent = 'Keno Tracker'; Object.assign(title.style, { fontSize: '18px', fontWeight: 'bold', cursor: 'grab' }); container.appendChild(title); const winningSection = document.createElement('div'); winningSection.id = 'winningNumbersSection'; const winningTitle = document.createElement('h4'); winningTitle.textContent = 'System Winning Numbers:'; winningSection.appendChild(winningTitle); const winningList = document.createElement('ul'); winningList.id = 'winningNumbers'; Object.assign(winningList.style, { listStyleType: 'none', padding: '5px', maxHeight: '100px', overflowY: 'auto', border: '1px solid #4a5568', borderRadius: '5px' }); winningSection.appendChild(winningList); container.appendChild(winningSection); const lostSection = document.createElement('div'); lostSection.id = 'lostNumbersSection'; const lostTitle = document.createElement('h4'); lostTitle.textContent = 'System Lost Numbers:'; lostSection.appendChild(lostTitle); const lostList = document.createElement('ul'); lostList.id = 'lostNumbers'; Object.assign(lostList.style, { listStyleType: 'none', padding: '5px', maxHeight: '100px', overflowY: 'auto', border: '1px solid #4a5568', borderRadius: '5px' }); lostSection.appendChild(lostList); container.appendChild(lostSection); const countSection = document.createElement('div'); countSection.id = 'countSection'; const countHeader = document.createElement('div'); Object.assign(countHeader.style, { display: 'flex', justifyContent: 'space-between', alignItems: 'center', marginBottom: '5px' }); const countTitle = document.createElement('h4'); countTitle.textContent = 'System Pick Counts:'; countHeader.appendChild(countTitle); const sortButton = document.createElement('button'); sortButton.id = 'countSortToggle'; Object.assign(sortButton.style, { fontSize: '11px', padding: '2px 6px', cursor: 'pointer', backgroundColor: '#4a5568', color: 'white', border: '1px solid #718096', borderRadius: '4px' }); sortButton.addEventListener('click', () => { countSortOrder = (countSortOrder === 'byPick') ? 'byNumber' : 'byPick'; localStorage.setItem(LS_KEY_SORT_ORDER, countSortOrder); updateDisplay(); }); countHeader.appendChild(sortButton); countSection.appendChild(countHeader); const numberCount = document.createElement('div'); numberCount.id = 'numberCountDisplay'; Object.assign(numberCount.style, { maxHeight: '150px', overflowY: 'auto', padding: '10px', backgroundColor: '#2d3748', borderRadius: '5px', marginBottom: '20px', textAlign: 'left', display: 'grid', gridTemplateColumns: 'repeat(auto-fill, minmax(60px, 1fr))', gap: '5px' }); countSection.appendChild(numberCount); container.appendChild(countSection); const clearButton = document.createElement('button'); clearButton.textContent = 'Clear All Data'; Object.assign(clearButton.style, { backgroundColor: '#ef4444', color: '#fff', padding: '10px 15px', border: 'none', borderRadius: '5px', cursor: 'pointer', transition: 'background-color 0.2s ease', width: '100%', marginBottom: '10px' }); clearButton.onmouseover = () => clearButton.style.backgroundColor = '#dc2626'; clearButton.onmouseout = () => clearButton.style.backgroundColor = '#ef4444'; clearButton.addEventListener('click', clearData); container.appendChild(clearButton); const ioContainer = document.createElement('div'); Object.assign(ioContainer.style, { display: 'flex', gap: '10px', justifyContent: 'center' }); const importButton = document.createElement('button'); importButton.textContent = 'Import Counts'; Object.assign(importButton.style, { backgroundColor: '#2563eb', color: '#fff', padding: '10px 15px', border: 'none', borderRadius: '5px', cursor: 'pointer', transition: 'background-color 0.2s ease', flex: '1' }); importButton.onmouseover = () => importButton.style.backgroundColor = '#1d4ed8'; importButton.onmouseout = () => importButton.style.backgroundColor = '#2563eb'; importButton.addEventListener('click', importPickCounts); const exportButton = document.createElement('button'); exportButton.textContent = 'Export Counts'; Object.assign(exportButton.style, { backgroundColor: '#16a34a', color: '#fff', padding: '10px 15px', border: 'none', borderRadius: '5px', cursor: 'pointer', transition: 'background-color 0.2s ease', flex: '1' }); exportButton.onmouseover = () => exportButton.style.backgroundColor = '#15803d'; exportButton.onmouseout = () => exportButton.style.backgroundColor = '#16a34a'; exportButton.addEventListener('click', exportPickCounts); ioContainer.appendChild(importButton); ioContainer.appendChild(exportButton); container.appendChild(ioContainer); document.body.appendChild(container); setupDrag(container); } function setupDrag(element) { let isDragging = false, offsetX, offsetY; element.addEventListener('mousedown', (e) => { if (e.button !== 0 || e.target.closest('button, ul, #numberCountDisplay')) return; const isDraggableArea = e.target === element || e.target.closest('h3, h4'); if (isDraggableArea) { isDragging = true; const rect = element.getBoundingClientRect(); offsetX = e.clientX - rect.left; offsetY = e.clientY - rect.top; element.style.cursor = 'grabbing'; } }); document.addEventListener('mousemove', (e) => { if (isDragging) { e.preventDefault(); element.style.left = `${e.clientX - offsetX}px`; element.style.top = `${e.clientY - offsetY}px`; } }); document.addEventListener('mouseup', () => { if (isDragging) { isDragging = false; element.style.cursor = 'grab'; savePosition(element.style.left, element.style.top); } }); } let numberPickCounts; let roundsToPlay = 0; let roundsProcessedInSession = 0; let countSortOrder; // --- Helper Functions --- function savePosition(left, top) { localStorage.setItem(LS_KEY_POSITION, JSON.stringify({ top: parseInt(top), left: parseInt(left) })); } function getPosition() { return JSON.parse(localStorage.getItem(LS_KEY_POSITION)) || { top: 20, left: 20 }; } function clearData() { if (confirm('Are you sure you want to clear ALL tracker data? This action cannot be undone.')) { localStorage.removeItem(LS_KEY_PICK_COUNTS); numberPickCounts = {}; roundsToPlay = 0; roundsProcessedInSession = 0; updateDisplay(); alert('All Keno tracker data has been cleared.'); } } function exportPickCounts() { const data = localStorage.getItem(LS_KEY_PICK_COUNTS); if (!data || data === '{}') { alert('No pick count data to export.'); return; } navigator.clipboard.writeText(data).then(() => { alert('System pick counts have been copied to your clipboard.'); }).catch(err => { console.error('Failed to copy data automatically: ', err); prompt('Could not automatically copy. Please copy the data manually from here:', data); }); } function importPickCounts() { const rawData = prompt('Paste your exported Keno pick count data here:'); if (!rawData) { return; } let parsedData; try { parsedData = JSON.parse(rawData); if (typeof parsedData !== 'object' || parsedData === null || Array.isArray(parsedData)) throw new Error('Data is not a valid JSON object.'); } catch (error) { alert('Import failed. The data provided was not in a valid format.'); return; } const importMethod = prompt("How would you like to import?\n\nType 'overwrite' to replace all existing data.\nType 'add' to sum the imported counts with your existing data.", "add"); if (!importMethod) { return; } const method = importMethod.toLowerCase(); if (method === 'overwrite') { if (confirm('This will completely REPLACE your current pick counts. Are you sure?')) { localStorage.setItem(LS_KEY_PICK_COUNTS, JSON.stringify(parsedData)); numberPickCounts = parsedData; updateDisplay(); alert('Pick counts overwritten successfully!'); } } else if (method === 'add') { if (confirm('This will ADD the imported counts to your current data, summing the totals. Are you sure?')) { let currentCounts = JSON.parse(localStorage.getItem(LS_KEY_PICK_COUNTS)) || {}; for (const number in parsedData) { if (Object.hasOwnProperty.call(parsedData, number)) { currentCounts[number] = (parseInt(currentCounts[number]) || 0) + (parseInt(parsedData[number]) || 0); } } localStorage.setItem(LS_KEY_PICK_COUNTS, JSON.stringify(currentCounts)); numberPickCounts = currentCounts; updateDisplay(); alert('Pick counts added successfully!'); } } else { alert('Invalid import method. Action cancelled.'); } } function updateDisplay() { const winningList = document.getElementById('winningNumbers'); const lostList = document.getElementById('lostNumbers'); const numberCountDisplay = document.getElementById('numberCountDisplay'); const sortButton = document.getElementById('countSortToggle'); if (!winningList || !lostList || !numberCountDisplay) return; winningList.innerHTML = ''; lostList.innerHTML = ''; numberCountDisplay.innerHTML = ''; const currentWinners = Array.from(document.querySelectorAll('#kenoGame #boardContainer span.winning')).map(el => el.textContent); const currentLosers = Array.from(document.querySelectorAll('#kenoGame #boardContainer span.lost')).map(el => el.textContent); if (currentWinners.length === 0 && currentLosers.length === 0) { winningList.textContent = 'No numbers drawn yet.'; lostList.textContent = 'No numbers drawn yet.'; } else { if (currentWinners.length > 0) { currentWinners.sort((a, b) => parseInt(a) - parseInt(b)).forEach(number => { const li = document.createElement('li'); li.textContent = number; li.style.color = '#38b2ac'; winningList.appendChild(li); }); } else { winningList.textContent = 'No winning numbers.'; } if (currentLosers.length > 0) { currentLosers.sort((a, b) => parseInt(a) - parseInt(b)).forEach(number => { const li = document.createElement('li'); li.textContent = number; li.style.color = '#e53e3e'; lostList.appendChild(li); }); } else { lostList.textContent = 'No lost numbers.'; } } const sortedNumberKeys = Object.keys(numberPickCounts); if (countSortOrder === 'byPick') { sortedNumberKeys.sort((a, b) => numberPickCounts[b] - numberPickCounts[a]); if(sortButton) sortButton.textContent = 'Sort by #'; } else { // 'byNumber' sortedNumberKeys.sort((a, b) => parseInt(a) - parseInt(b)); if(sortButton) sortButton.textContent = 'Sort by Pick'; } if (sortedNumberKeys.length > 0) { sortedNumberKeys.forEach(number => { const span = document.createElement('span'); span.textContent = `${number}: ${numberPickCounts[number]}`; span.style.padding = '3px 5px'; span.style.backgroundColor = '#4a5568'; span.style.borderRadius = '3px'; numberCountDisplay.appendChild(span); }); } else { numberCountDisplay.textContent = 'No numbers picked yet'; } } function _processKenoRoundResults(winnersSnapshot, losersSnapshot) { if (winnersSnapshot.length === 0 && losersSnapshot.length === 0) { return; } let numbersProcessed = false; [...winnersSnapshot, ...losersSnapshot].forEach(number => { numberPickCounts[number] = (numberPickCounts[number] || 0) + 1; numbersProcessed = true; }); if (numbersProcessed) { localStorage.setItem(LS_KEY_PICK_COUNTS, JSON.stringify(numberPickCounts)); updateDisplay(); roundsProcessedInSession++; } } const debounce = (func, delay) => { let t; return (...a) => { clearTimeout(t); t = setTimeout(() => func.apply(this, a), delay); }; }; const processKenoRoundResultsDebounced = debounce(_processKenoRoundResults, 500); const observer = new MutationObserver(() => { if (roundsProcessedInSession >= roundsToPlay) { updateDisplay(); return; } const currentWinnersSnapshot = Array.from(document.querySelectorAll('#kenoGame #boardContainer span.winning')).map(el => el.textContent); const currentLosersSnapshot = Array.from(document.querySelectorAll('#kenoGame #boardContainer span.lost')).map(el => el.textContent); if (currentWinnersSnapshot.length > 0 || currentLosersSnapshot.length > 0) { processKenoRoundResultsDebounced(currentWinnersSnapshot, currentLosersSnapshot); } else { updateDisplay(); } }); function initializeKenoTrackerAfterElementsLoaded() { numberPickCounts = JSON.parse(localStorage.getItem(LS_KEY_PICK_COUNTS)) || {}; countSortOrder = localStorage.getItem(LS_KEY_SORT_ORDER) || 'byPick'; createUI(); updateDisplay(); observer.observe(document.querySelector('#kenoGame #boardContainer'), { attributes: true, attributeFilter: ['class'], childList: true, subtree: true }); document.body.addEventListener('click', (e) => { if (e.target.matches('#playBtn') || e.target.matches('#repeatBtn')) { roundsToPlay = parseInt(document.getElementById('roundsAmount').textContent) || 1; roundsProcessedInSession = 0; } else if (e.target.matches('#clearBtn')) { roundsToPlay = 0; roundsProcessedInSession = 0; } }); const initialWinners = Array.from(document.querySelectorAll('#kenoGame #boardContainer span.winning')).map(el => el.textContent); const initialLosers = Array.from(document.querySelectorAll('#kenoGame #boardContainer span.lost')).map(el => el.textContent); if (initialWinners.length > 0 || initialLosers.length > 0) { roundsToPlay = 1; roundsProcessedInSession = 0; processKenoRoundResultsDebounced(initialWinners, initialLosers); } } const initInterval = setInterval(() => { if (document.querySelector('#kenoGame #boardContainer')) { clearInterval(initInterval); initializeKenoTrackerAfterElementsLoaded(); } }, 500); })();
QingJ © 2025
镜像随时可能失效,请加Q群300939539或关注我们的公众号极客氢云获取最新地址