Torn Keno Hot Numbers

Tracks Keno number frequency during a session and displays the top 10 hottest numbers. Waits for dynamic content.

当前为 2025-06-25 提交的版本,查看 最新版本

// ==UserScript==
// @name         Torn Keno Hot Numbers
// @namespace    torn.keno.hotnumbers.tracker
// @version      2.7
// @description  Tracks Keno number frequency during a session and displays the top 10 hottest numbers. Waits for dynamic content.
// @author       Torn City Userscript Development Assistant
// @match        https://www.torn.com/page.php?sid=keno
// @icon         https://www.google.com/s2/favicons?sz=64&domain=torn.com
// @grant        none
// @run-at       document-start
// @license      MIT
// ==/UserScript==

(function() {
    'use strict';

    // This is the main function that runs the entire script.
    // It will only be called after we are certain the DOM is ready.
    function main() {

        // --- CONFIGURATION & STATE ---
        const KENO_DATA_URL_FRAGMENT = 'page.php?rfcv=';
        let numberFrequency = {};
        let isInitialized = false;

        // --- CORE LOGIC ---
        function addCustomStyles() {
            const style = document.createElement('style');
            style.innerHTML = `
                #keno-hot-numbers-container { background-color: #f2f2f2; border-radius: 5px; margin-bottom: 10px; border: 1px solid #ccc; }
                .dark-mode #keno-hot-numbers-container { background-color: #3d3d3d; border-color: #444; }
                .keno-hot-numbers-header { padding: 10px; background-color: #e8e8e8; border-top-left-radius: 5px; border-top-right-radius: 5px; font-weight: bold; color: #333; border-bottom: 1px solid #ddd; }
                .dark-mode .keno-hot-numbers-header { background-color: #2e2e2e; border-color: #444; color: #ccc; }
                #keno-hot-numbers-body { padding: 10px; display: flex; flex-wrap: wrap; gap: 8px; justify-content: center; min-height: 40px; align-items: center; color: #666; }
                .dark-mode #keno-hot-numbers-body { color: #999; }
                .hot-number-pill { background-color: #fff; border: 1px solid #ccc; border-radius: 20px; padding: 5px 10px; font-size: 14px; display: flex; align-items: center; gap: 5px; box-shadow: 0 1px 1px rgba(0,0,0,0.05); }
                .dark-mode .hot-number-pill { background-color: #222; border-color: #555; box-shadow: none; }
                .hot-number-pill strong { color: #000; }
                .dark-mode .hot-number-pill strong { color: #eee; }
                .hot-number-pill span { font-size: 12px; color: #555; }
                .dark-mode .hot-number-pill span { color: #888; }
            `;
            document.head.appendChild(style);
        }

        function createHotNumbersUI() {
            const kenoBoard = document.getElementById('boardContainer');
            if (!kenoBoard) return;
            if (document.getElementById('keno-hot-numbers-container')) return;

            const container = document.createElement('div');
            container.id = 'keno-hot-numbers-container';
            container.innerHTML = `<div class="keno-hot-numbers-header">Hot Numbers (Session)</div><div id="keno-hot-numbers-body">Play a round to start tracking numbers...</div>`;

            kenoBoard.parentNode.insertBefore(container, kenoBoard);
        }

        function updateHotNumbersDisplay(drawnNumbers) {
            drawnNumbers.forEach(num => { numberFrequency[num] = (numberFrequency[num] || 0) + 1; });
            const sortedNumbers = Object.entries(numberFrequency).sort(([, a], [, b]) => b - a).slice(0, 10);
            const displayBody = document.getElementById('keno-hot-numbers-body');

            if (displayBody) {
                displayBody.innerHTML = sortedNumbers.length === 0 ? 'Waiting for results...' : sortedNumbers.map(([number, count]) => `<div class="hot-number-pill"><strong>${number}</strong><span>(x${count})</span></div>`).join('');
            }
        }

        function interceptFetch() {
            const originalFetch = window.fetch;
            if (originalFetch.name === 'hotNumbersFetch') return;

            window.fetch = async function hotNumbersFetch(...args) {
                const response = await originalFetch(...args);
                if (args[0] && typeof args[0] === 'string' && args[0].includes(KENO_DATA_URL_FRAGMENT)) {
                    response.clone().json()
                        .then(data => data && data.randomNumbers && updateHotNumbersDisplay(data.randomNumbers))
                        .catch(err => console.error("[Keno Tracker] Error parsing game JSON:", err));
                }
                return response;
            };
        }

        function initializeScript() {
            if (isInitialized) return;
            isInitialized = true;
            console.log("[Keno Tracker] Initializing...");
            addCustomStyles();
            createHotNumbersUI();
            interceptFetch();
            console.log("[Keno Tracker] Script fully active.");
        }

        // Setup the observer to watch for the board.
        const observer = new MutationObserver(() => {
            if (document.getElementById('boardContainer')) {
                initializeScript();
                observer.disconnect();
            }
        });

        observer.observe(document.body, { childList: true, subtree: true });

        // A final check in case the board was already there when the script ran.
        if (document.getElementById('boardContainer')) {
            initializeScript();
            observer.disconnect();
        }
    }

    // --- SCRIPT ENTRY POINT ---
    // This bootstrap logic waits for the DOM to be ready before calling main().
    // This is the key fix for the "parameter 1 is not of type 'Node'" error.
    if (document.readyState === 'loading') {
        document.addEventListener('DOMContentLoaded', main, { once: true });
    } else {
        // The DOM is already ready, so we can run our main function immediately.
        main();
    }
})();

QingJ © 2025

镜像随时可能失效,请加Q群300939539或关注我们的公众号极客氢云获取最新地址